From 7aa9a6fce611cfaf79b9c8f1934942f58ce4f7c6 Mon Sep 17 00:00:00 2001 From: Lee Saferite Date: Sun, 5 Feb 2012 13:44:44 -0500 Subject: [PATCH] Import Magento Release 1.7.0.0-alpha1 --- RELEASE_NOTES.txt | 627 + app/Mage.php | 25 +- app/code/core/Mage/Admin/Model/Observer.php | 6 +- app/code/core/Mage/Admin/Model/Roles.php | 60 +- app/code/core/Mage/Admin/Model/Session.php | 16 +- app/code/core/Mage/Admin/Model/User.php | 8 +- app/code/core/Mage/Admin/etc/config.xml | 2 +- .../Admin/sql/admin_setup/install-1.6.0.0.php | 12 +- .../Mage/AdminNotification/Helper/Data.php | 2 + .../Mage/AdminNotification/Model/Survey.php | 2 + .../Adminhtml/Block/Api/Tab/Rolesedit.php | 4 +- app/code/core/Mage/Adminhtml/Block/Backup.php | 64 +- .../Mage/Adminhtml/Block/Backup/Dialogs.php | 53 + .../core/Mage/Adminhtml/Block/Backup/Grid.php | 58 +- .../Form/Renderer/Fieldset/Element.php | 27 +- .../Product/Attribute/Set/Main/Formset.php | 39 +- .../Edit/Action/Attribute/Tab/Attributes.php | 1 + .../Catalog/Product/Edit/Tab/Attributes.php | 84 +- .../Catalog/Product/Edit/Tab/Categories.php | 96 +- .../Catalog/Product/Edit/Tab/Price/Group.php | 96 + .../Product/Edit/Tab/Price/Group/Abstract.php | 347 + .../Catalog/Product/Edit/Tab/Price/Tier.php | 300 +- .../Catalog/Product/Edit/Tab/Super/Config.php | 23 +- .../Product/Edit/Tab/Super/Config/Simple.php | 18 +- .../Adminhtml/Block/Catalog/Product/Grid.php | 46 +- .../Catalog/Product/Helper/Form/Gallery.php | 14 +- .../Catalog/Product/Helper/Form/Weight.php | 45 + .../Block/Catalog/Product/Widget/Chooser.php | 4 +- .../Block/Catalog/Search/Edit/Form.php | 51 +- .../Adminhtml/Block/Checkout/Agreement.php | 2 +- .../Block/Checkout/Agreement/Edit.php | 2 +- .../Mage/Adminhtml/Block/Cms/Page/Edit.php | 27 +- .../Edit/Renderer/Attribute/Group.php | 105 + .../Block/Customer/Edit/Tab/Account.php | 72 +- .../Block/Customer/Edit/Tab/Cart.php | 12 +- .../Block/Customer/Edit/Tab/Newsletter.php | 11 +- .../Block/Customer/Edit/Tab/View.php | 20 +- .../Customer/Edit/Tab/View/Accordion.php | 4 +- .../Block/Customer/Group/Edit/Form.php | 3 +- .../Address/Form/Billing/Renderer/Vat.php | 97 + .../Customer/System/Config/Validatevat.php | 79 + .../Mage/Adminhtml/Block/Media/Uploader.php | 7 +- .../Block/Promo/Quote/Edit/Tab/Coupons.php | 88 + .../Promo/Quote/Edit/Tab/Coupons/Form.php | 143 + .../Promo/Quote/Edit/Tab/Coupons/Grid.php | 144 + .../Tab/Coupons/Grid/Column/Renderer/Used.php | 42 + .../Block/Promo/Quote/Edit/Tab/Main.php | 30 +- .../Quote/Edit/Tab/Main/Renderer/Checkbox.php | 59 + .../Adminhtml/Block/Promo/Widget/Chooser.php | 4 + .../Block/Report/Customer/Totals/Grid.php | 7 +- .../core/Mage/Adminhtml/Block/Report/Grid.php | 16 +- .../Adminhtml/Block/Report/Grid/Abstract.php | 55 +- .../Adminhtml/Block/Report/Grid/Shopcart.php | 84 + .../Block/Report/Product/Ordered/Grid.php | 13 +- .../Adminhtml/Block/Report/Product/Viewed.php | 16 + .../Block/Report/Product/Viewed/Grid.php | 97 +- .../Block/Report/Refresh/Statistics/Grid.php | 8 +- .../Block/Report/Sales/Bestsellers/Grid.php | 3 +- .../Block/Report/Sales/Coupons/Grid.php | 45 +- .../Block/Report/Sales/Invoiced/Grid.php | 10 +- .../Block/Report/Sales/Refunded/Grid.php | 10 +- .../Block/Report/Sales/Sales/Grid.php | 112 +- .../Block/Report/Sales/Shipping/Grid.php | 13 +- .../Adminhtml/Block/Report/Sales/Tax/Grid.php | 6 +- .../Block/Report/Shopcart/Abandoned/Grid.php | 43 +- .../Block/Report/Shopcart/Customer/Grid.php | 10 +- .../Block/Report/Shopcart/Product/Grid.php | 9 +- .../Mage/Adminhtml/Block/Review/Edit/Form.php | 10 +- .../Block/Sales/Items/Column/Name.php | 17 + .../Adminhtml/Block/Sales/Order/Create.php | 26 +- .../Sales/Order/Create/Billing/Address.php | 1 + .../Block/Sales/Order/Create/Form/Address.php | 9 + .../Block/Sales/Order/Create/Sidebar/Cart.php | 22 +- .../Block/Sales/Order/Creditmemo/Create.php | 14 +- .../Block/Sales/Order/View/Tab/History.php | 128 +- .../Block/Sales/Transactions/Detail.php | 5 +- .../Block/Sales/Transactions/Detail/Grid.php | 3 +- .../Block/System/Config/Form/Fieldset.php | 19 +- .../Email/Template/Grid/Filter/Type.php | 17 +- .../Email/Template/Grid/Renderer/Type.php | 7 +- .../Block/System/Email/Template/Preview.php | 15 +- .../Mage/Adminhtml/Block/Tag/Edit/Form.php | 14 +- .../core/Mage/Adminhtml/Block/Widget/Form.php | 1 + .../Widget/Form/Renderer/Fieldset/Element.php | 3 +- .../core/Mage/Adminhtml/Block/Widget/Grid.php | 4 +- .../Adminhtml/Controller/Report/Abstract.php | 124 + .../core/Mage/Adminhtml/Model/Sales/Order.php | 4 +- .../Adminhtml/Model/Sales/Order/Create.php | 98 +- .../Admin/Password/Link/Expirationperiod.php | 3 +- .../Backend/Customer/GroupAutoAssign.php | 57 + .../Password/Link/Expirationperiod.php | 3 +- .../Model/System/Config/Backend/Filename.php | 37 + .../Model/System/Config/Backend/Locale.php | 9 +- .../System/Config/Source/Dev/Dbautoup.php | 7 +- .../Mage/Adminhtml/Model/System/Store.php | 2 +- .../Catalog/CategoryController.php | 2 +- .../Product/Action/AttributeController.php | 14 +- .../Catalog/Product/AttributeController.php | 23 +- .../controllers/Catalog/ProductController.php | 29 +- .../controllers/Catalog/SearchController.php | 4 +- .../Checkout/AgreementController.php | 8 +- .../System/Config/ValidatevatController.php | 85 + .../controllers/CustomerController.php | 145 +- .../controllers/DashboardController.php | 21 +- .../Adminhtml/controllers/IndexController.php | 66 +- .../Newsletter/SubscriberController.php | 8 +- .../Permissions/RoleController.php | 17 +- .../controllers/Promo/CatalogController.php | 12 +- .../controllers/Promo/QuoteController.php | 141 +- .../controllers/Report/ProductController.php | 48 +- .../controllers/Report/SalesController.php | 73 +- .../Report/StatisticsController.php | 13 +- .../Sales/Order/CreateController.php | 3 +- .../Sales/Order/ShipmentController.php | 23 +- .../controllers/Sales/OrderController.php | 11 +- .../Sales/TransactionsController.php | 10 +- .../controllers/System/BackupController.php | 248 +- .../controllers/System/VariableController.php | 6 +- .../Adminhtml/controllers/TagController.php | 16 +- .../core/Mage/Authorizenet/etc/system.xml | 1 + app/code/core/Mage/Backup/Helper/Data.php | 240 + app/code/core/Mage/Backup/Model/Backup.php | 62 +- .../Mage/Backup/Model/Config/Backend/Cron.php | 89 + .../Mage/Backup/Model/Config/Source/Type.php | 52 + .../core/Mage/Backup/Model/Fs/Collection.php | 10 +- app/code/core/Mage/Backup/Model/Observer.php | 95 + .../core/Mage/Backup/Model/Resource/Db.php | 47 +- .../Backup/Model/Resource/Helper/Mysql4.php | 51 +- app/code/core/Mage/Backup/etc/adminhtml.xml | 5 + app/code/core/Mage/Backup/etc/config.xml | 9 + app/code/core/Mage/Backup/etc/system.xml | 95 + .../Catalog/Product/Edit/Tab/Attributes.php | 21 +- .../Catalog/Product/View/Type/Bundle.php | 54 +- .../core/Mage/Bundle/Model/Product/Price.php | 189 +- .../Bundle/Model/Resource/Indexer/Price.php | 185 +- .../Bundle/Model/Resource/Indexer/Stock.php | 12 - app/code/core/Mage/Bundle/etc/config.xml | 2 +- .../mysql4-upgrade-1.6.0.0-1.6.0.0.1.php | 44 + .../upgrade-1.6.0.0-1.6.0.0.1.php | 84 + app/code/core/Mage/Captcha/Block/Captcha.php | 48 + .../core/Mage/Captcha/Block/Captcha/Zend.php | 87 + app/code/core/Mage/Captcha/Helper/Data.php | 138 + app/code/core/Mage/Captcha/Model/Captcha.php | 149 + .../core/Mage/Captcha/Model/Config/Font.php | 49 + .../Captcha/Model/Config/Form/Abstract.php | 61 + .../Captcha/Model/Config/Form/Backend.php | 40 + .../Captcha/Model/Config/Form/Frontend.php | 40 + .../core/Mage/Captcha/Model/Config/Mode.php | 54 + .../core/Mage/Captcha/Model/Interface.php | 60 + app/code/core/Mage/Captcha/Model/Observer.php | 282 + .../core/Mage/Captcha/Model/Resource/Log.php | 157 + .../Captcha/Model/Resource/LoginAttempt.php | 160 + .../Resource/LoginAttempt/Collection.php | 45 + app/code/core/Mage/Captcha/Model/Zend.php | 496 + .../Adminhtml/RefreshController.php | 50 + .../Captcha/controllers/RefreshController.php | 50 + app/code/core/Mage/Captcha/etc/config.xml | 262 + app/code/core/Mage/Captcha/etc/system.xml | 250 + .../sql/captcha_setup/install-1.7.0.0.0.php | 51 + .../Mage/Catalog/Block/Product/Abstract.php | 15 +- .../core/Mage/Catalog/Block/Product/New.php | 13 +- .../core/Mage/Catalog/Block/Product/Price.php | 17 +- .../core/Mage/Catalog/Block/Product/View.php | 41 +- .../Mage/Catalog/Block/Product/View/Media.php | 28 +- .../Catalog/Block/Product/View/Options.php | 6 +- app/code/core/Mage/Catalog/Helper/Image.php | 199 +- app/code/core/Mage/Catalog/Helper/Product.php | 33 +- .../core/Mage/Catalog/Model/Category/Api.php | 2 +- .../Catalog/Model/Category/Indexer/Flat.php | 35 +- .../Model/Category/Indexer/Product.php | 22 +- .../Catalog/Model/Convert/Adapter/Product.php | 32 +- .../core/Mage/Catalog/Model/Indexer/Url.php | 36 +- .../Catalog/Model/Layer/Filter/Attribute.php | 4 +- .../Mage/Catalog/Model/Layer/Filter/Price.php | 151 +- .../Model/Layer/Filter/Price/Algorithm.php | 420 + app/code/core/Mage/Catalog/Model/Observer.php | 13 + app/code/core/Mage/Catalog/Model/Product.php | 183 +- .../Product/Attribute/Backend/Groupprice.php | 57 + .../Attribute/Backend/Groupprice/Abstract.php | 369 + .../Product/Attribute/Backend/Tierprice.php | 277 +- .../Attribute/Source/Countryofmanufacture.php | 8 +- .../Catalog/Model/Product/Flat/Indexer.php | 44 +- .../Catalog/Model/Product/Indexer/Flat.php | 77 +- .../Catalog/Model/Product/Indexer/Price.php | 31 +- .../Model/Product/Option/Type/File.php | 16 +- .../Mage/Catalog/Model/Product/Type/Price.php | 76 +- .../Mage/Catalog/Model/Resource/Abstract.php | 2 +- .../Catalog/Model/Resource/Category/Flat.php | 57 +- .../Resource/Category/Indexer/Product.php | 307 +- .../Eav/Mysql4/Product/Indexer/Abstract.php | 2 +- .../Mysql4/Product/Indexer/Eav/Abstract.php | 2 +- .../Product/Indexer/Price/Interface.php | 2 +- .../Model/Resource/Layer/Filter/Price.php | 89 +- .../Product/Attribute/Backend/Groupprice.php | 46 + .../Attribute/Backend/Groupprice/Abstract.php | 147 + .../Product/Attribute/Backend/Tierprice.php | 90 +- .../Resource/Product/Attribute/Collection.php | 6 +- .../Model/Resource/Product/Collection.php | 6 +- .../Model/Resource/Product/Flat/Indexer.php | 40 +- .../Resource/Product/Indexer/Eav/Abstract.php | 25 +- .../Model/Resource/Product/Indexer/Price.php | 90 +- .../Product/Indexer/Price/Configurable.php | 56 +- .../Product/Indexer/Price/Default.php | 101 +- .../Product/Indexer/Price/Grouped.php | 23 +- .../Catalog/Model/Resource/Product/Option.php | 14 +- .../Mage/Catalog/Model/Resource/Setup.php | 13 +- .../core/Mage/Catalog/Model/Resource/Url.php | 30 +- app/code/core/Mage/Catalog/Model/Url.php | 94 +- .../data-upgrade-1.6.0.0.4-1.6.0.0.5.php | 27 +- .../data-upgrade-1.6.0.0.8-1.6.0.0.9.php | 36 + app/code/core/Mage/Catalog/etc/config.xml | 11 +- app/code/core/Mage/Catalog/etc/system.xml | 16 + .../sql/catalog_setup/install-1.6.0.0.php | 40 +- .../mysql4-upgrade-1.6.0.0.8-1.6.0.0.9.php | 53 + .../upgrade-1.6.0.0.6-1.6.0.0.7.php | 39 + .../upgrade-1.6.0.0.7-1.6.0.0.8.php | 97 + .../upgrade-1.6.0.0.9-1.6.0.0.10.php | 214 + .../CatalogInventory/Model/Indexer/Stock.php | 21 +- .../Model/Mysql4/Indexer/Stock/Interface.php | 2 +- .../Mage/CatalogInventory/Model/Observer.php | 21 +- .../Model/Resource/Indexer/Stock.php | 19 +- .../Resource/Indexer/Stock/Configurable.php | 12 - .../Model/Resource/Indexer/Stock/Default.php | 9 +- .../Model/Resource/Indexer/Stock/Grouped.php | 12 - .../CatalogInventory/Model/Stock/Item.php | 62 +- .../Model/System/Config/Backend/Minqty.php | 48 + .../core/Mage/CatalogInventory/etc/config.xml | 28 +- .../core/Mage/CatalogInventory/etc/system.xml | 4 +- .../mysql4-upgrade-1.6.0.0-1.6.0.0.1.php | 34 + app/code/core/Mage/CatalogRule/Model/Rule.php | 6 +- .../core/Mage/CatalogSearch/Helper/Data.php | 5 +- .../Mage/CatalogSearch/Model/Fulltext.php | 21 +- .../CatalogSearch/Model/Indexer/Fulltext.php | 84 +- .../CatalogSearch/Model/Resource/Fulltext.php | 35 +- .../Centinel/Block/Adminhtml/Validation.php | 6 +- .../Mage/Checkout/Block/Cart/Shipping.php | 88 +- app/code/core/Mage/Checkout/Block/Onepage.php | 13 +- .../Mage/Checkout/Block/Onepage/Abstract.php | 11 + .../Mage/Checkout/Block/Onepage/Progress.php | 24 + app/code/core/Mage/Checkout/Helper/Data.php | 13 +- app/code/core/Mage/Checkout/Helper/Url.php | 10 + app/code/core/Mage/Checkout/Model/Cart.php | 10 +- .../Checkout/Model/Type/Multishipping.php | 21 +- .../core/Mage/Checkout/Model/Type/Onepage.php | 42 +- .../Checkout/controllers/CartController.php | 41 +- .../controllers/OnepageController.php | 19 + app/code/core/Mage/Checkout/etc/config.xml | 1 + app/code/core/Mage/Checkout/etc/system.xml | 10 + app/code/core/Mage/Cms/Helper/Page.php | 10 +- .../Cms/sql/cms_setup/install-1.6.0.0.php | 4 +- .../Extension/Custom/Edit/Tab/Abstract.php | 4 +- .../core/Mage/Connect/Model/Extension.php | 10 +- .../Adminhtml/Extension/CustomController.php | 2 - app/code/core/Mage/Core/Block/Html/Select.php | 103 +- .../Mage/Core/Controller/Varien/Action.php | 33 +- app/code/core/Mage/Core/Helper/Data.php | 104 +- app/code/core/Mage/Core/Helper/Translate.php | 34 +- app/code/core/Mage/Core/Model/App.php | 24 + app/code/core/Mage/Core/Model/Calculator.php | 87 + app/code/core/Mage/Core/Model/Config.php | 10 +- app/code/core/Mage/Core/Model/Cookie.php | 1 + .../File/Validator/NotProtectedExtension.php | 5 +- app/code/core/Mage/Core/Model/Locale.php | 17 +- .../Mage/Core/Model/Resource/Abstract.php | 2 +- .../Model/Resource/Db/Collection/Abstract.php | 13 +- .../Core/Model/Resource/Design/Collection.php | 2 +- .../Mage/Core/Model/Resource/Url/Rewrite.php | 3 + .../Mage/Core/Model/Resource/Variable.php | 9 +- .../core/Mage/Core/Model/Session/Abstract.php | 11 + .../Core/Model/Source/Email/Variables.php | 27 +- app/code/core/Mage/Core/Model/Store.php | 10 +- .../core/Mage/Core/Model/Translate/Inline.php | 55 +- app/code/core/Mage/Core/Model/Url.php | 32 + .../Mage/Core/controllers/AjaxController.php | 5 +- app/code/core/Mage/Core/etc/config.xml | 3 + app/code/core/Mage/Core/etc/system.xml | 31 + .../core/Mage/Customer/Block/Widget/Name.php | 11 +- .../core/Mage/Customer/Helper/Address.php | 39 + app/code/core/Mage/Customer/Helper/Data.php | 256 + app/code/core/Mage/Customer/Model/Address.php | 4 +- .../Mage/Customer/Model/Address/Abstract.php | 36 +- .../Model/Attribute/Backend/Data/Boolean.php | 52 + .../core/Mage/Customer/Model/Customer.php | 48 +- app/code/core/Mage/Customer/Model/Form.php | 11 + .../core/Mage/Customer/Model/Observer.php | 172 +- .../Mage/Customer/Model/Resource/Customer.php | 2 +- .../Model/Resource/Customer/Collection.php | 13 +- .../Mage/Customer/Model/Resource/Group.php | 3 +- app/code/core/Mage/Customer/Model/Session.php | 40 +- .../controllers/AccountController.php | 22 +- app/code/core/Mage/Customer/etc/config.xml | 124 +- app/code/core/Mage/Customer/etc/system.xml | 113 +- .../upgrade-1.6.1.0-1.6.2.0.php | 94 + .../upgrade-1.6.2.0-1.6.2.0.1.php | 36 + .../Model/Convert/Adapter/Http/Curl.php | 4 +- .../Model/Convert/Validator/Column.php | 4 +- .../Dataflow/Model/Mysql4/Batch/Abstract.php | 2 +- app/code/core/Mage/Dataflow/Model/Profile.php | 4 +- .../sql/directory_setup/install-1.6.0.0.php | 34 +- .../core/Mage/Downloadable/Model/Observer.php | 6 + .../Model/Resource/Indexer/Price.php | 30 +- .../Adminhtml/Downloadable/FileController.php | 6 + .../controllers/DownloadController.php | 5 +- .../core/Mage/Downloadable/etc/config.xml | 2 +- .../mysql4-upgrade-1.6.0.0.1-1.6.0.0.2.php | 39 + .../upgrade-1.6.0.0.1-1.6.0.0.2.php | 34 + .../Attribute/Edit/Main/Abstract.php | 3 +- .../Mage/Eav/Model/Attribute/Data/Date.php | 16 +- .../core/Mage/Eav/Model/Entity/Abstract.php | 6 +- .../Entity/Attribute/Backend/Abstract.php | 46 +- .../Entity/Attribute/Backend/Interface.php | 15 + app/code/core/Mage/Eav/Model/Entity/Setup.php | 22 +- .../Eav/sql/eav_setup/install-1.6.0.0.php | 30 +- .../core/Mage/GoogleAnalytics/Block/Ga.php | 32 +- .../core/Mage/GoogleBase/Model/Observer.php | 6 +- .../GoogleCheckout/Model/Api/Xml/Abstract.php | 15 +- .../GoogleCheckout/Model/Api/Xml/Checkout.php | 5 +- .../Mage/GoogleCheckout/Model/Payment.php | 36 +- .../Model/Source/Shipping/Carrier.php | 43 +- .../Model/Source/Shipping/Category.php | 5 +- .../Model/Source/Shipping/Units.php | 3 +- .../Model/Source/Shipping/Virtual/Method.php | 7 +- .../Source/Shipping/Virtual/Schedule.php | 5 +- .../Model/Export/Entity/Abstract.php | 10 +- .../core/Mage/ImportExport/Model/Import.php | 3 +- .../Model/Import/Entity/Abstract.php | 4 +- .../Model/Import/Entity/Product.php | 109 +- .../Import/Entity/Product/Type/Abstract.php | 2 + .../Index/Block/Adminhtml/Notifications.php | 7 +- .../Index/Block/Adminhtml/Process/Grid.php | 70 +- .../Adminhtml/Process/Grid/Massaction.php | 54 + app/code/core/Mage/Index/Model/Event.php | 102 +- app/code/core/Mage/Index/Model/Indexer.php | 176 +- .../Mage/Index/Model/Indexer/Abstract.php | 30 +- app/code/core/Mage/Index/Model/Observer.php | 2 +- app/code/core/Mage/Index/Model/Process.php | 210 +- .../Mage/Index/Model/Resource/Abstract.php | 26 +- .../core/Mage/Index/Model/Resource/Event.php | 55 +- .../Index/Model/Resource/Event/Collection.php | 9 +- .../Mage/Index/Model/Resource/Process.php | 52 +- .../Model/Resource/Process/Collection.php | 23 + .../core/Mage/Install/Model/Installer/Db.php | 22 +- .../core/Mage/Log/Model/Resource/Visitor.php | 20 + .../Log/sql/log_setup/install-1.6.0.0.php | 12 +- .../mysql4-upgrade-1.5.9.9-1.6.0.0.php | 4 +- .../Model/Resource/Problem/Collection.php | 1 + .../Model/Resource/Queue/Collection.php | 33 +- .../data-upgrade-1.6.0.0-1.6.0.1.php | 43 + app/code/core/Mage/Newsletter/etc/config.xml | 2 +- .../sql/newsletter_setup/install-1.6.0.0.php | 10 +- app/code/core/Mage/Paygate/Helper/Data.php | 45 +- .../core/Mage/Paygate/Model/Authorizenet.php | 44 + .../Mage/Payment/Block/Form/Container.php | 2 +- app/code/core/Mage/Payment/Block/Info/Cc.php | 10 + .../Mage/Payment/Model/Method/Abstract.php | 91 +- .../core/Mage/Payment/Model/Method/Cc.php | 50 +- .../core/Mage/Payment/Model/Method/Free.php | 1 - app/code/core/Mage/Payment/etc/system.xml | 4 + .../System/Config/Payflowlink/Advanced.php | 43 + app/code/core/Mage/Paypal/Block/Iframe.php | 3 +- .../Paypal/Block/Payflow/Advanced/Form.php | 56 + .../Paypal/Block/Payflow/Advanced/Iframe.php | 67 + .../Paypal/Block/Payflow/Advanced/Info.php | 37 + .../Paypal/Block/Payflow/Advanced/Review.php | 38 + .../Mage/Paypal/Block/Payflow/Link/Form.php | 2 +- .../Mage/Paypal/Block/Payflow/Link/Iframe.php | 27 +- .../Mage/Paypal/Block/Payflow/Link/Review.php | 47 + .../Paypal/Controller/Express/Abstract.php | 2 +- app/code/core/Mage/Paypal/Helper/Hss.php | 4 +- app/code/core/Mage/Paypal/Model/Api/Nvp.php | 10 +- app/code/core/Mage/Paypal/Model/Cart.php | 14 +- app/code/core/Mage/Paypal/Model/Config.php | 21 +- app/code/core/Mage/Paypal/Model/Ipn.php | 9 +- app/code/core/Mage/Paypal/Model/Observer.php | 27 +- .../Mage/Paypal/Model/Payflowadvanced.php | 57 + .../core/Mage/Paypal/Model/Payflowlink.php | 491 +- .../core/Mage/Paypal/Model/Payflowpro.php | 7 +- .../Mage/Paypal/Model/Report/Settlement.php | 142 +- .../Model/Resource/Payment/Transaction.php | 1 + .../Payment/Transaction/Collection.php | 1 + .../Config/Source/AuthorizationAmounts.php | 9 +- .../Paypal/controllers/PayflowController.php | 103 +- .../controllers/PayflowadvancedController.php | 170 + app/code/core/Mage/Paypal/etc/config.xml | 23 +- app/code/core/Mage/Paypal/etc/system.xml | 200 +- .../paypal_setup/upgrade-1.6.0.1-1.6.0.2.php | 35 + app/code/core/Mage/Poll/Model/Poll/Answer.php | 4 +- app/code/core/Mage/Reports/Model/Flag.php | 1 + .../Model/Mysql4/Product/Index/Abstract.php | 3 +- .../Product/Index/Collection/Abstract.php | 2 +- .../Reports/Model/Mysql4/Report/Abstract.php | 2 +- .../Model/Resource/Helper/Interface.php | 52 + .../Reports/Model/Resource/Helper/Mysql4.php | 68 + .../Resource/Product/Viewed/Collection.php | 46 + .../Model/Resource/Quote/Collection.php | 4 +- .../Model/Resource/Report/Abstract.php | 12 +- .../Resource/Report/Collection/Abstract.php | 287 + .../Model/Resource/Report/Product/Viewed.php | 212 + .../Report/Product/Viewed/Collection.php | 367 + .../Reports/Model/Resource/Tag/Collection.php | 3 +- app/code/core/Mage/Reports/etc/config.xml | 11 +- .../upgrade-1.6.0.0-1.6.0.0.1.php | 99 + app/code/core/Mage/Review/Model/Review.php | 8 +- .../Review/controllers/ProductController.php | 8 +- app/code/core/Mage/Rss/Block/Catalog/New.php | 27 +- .../Mage/Rss/Block/Catalog/NotifyStock.php | 6 +- .../core/Mage/Rss/Block/Catalog/Review.php | 6 +- app/code/core/Mage/Rss/Block/Wishlist.php | 25 +- app/code/core/Mage/Rss/etc/config.xml | 10 +- app/code/core/Mage/Rule/Block/Editable.php | 38 +- .../Mage/Rule/Model/Condition/Combine.php | 52 +- .../Adminhtml/Report/Filter/Form/Coupon.php | 89 + .../Address/Attribute/Frontend/Discount.php | 5 +- .../core/Mage/Sales/Model/Mysql4/Abstract.php | 2 +- .../Sales/Model/Mysql4/Report/Abstract.php | 2 +- app/code/core/Mage/Sales/Model/Observer.php | 110 +- .../core/Mage/Sales/Model/Order/Config.php | 6 +- .../Mage/Sales/Model/Order/Creditmemo.php | 46 +- .../Sales/Model/Order/Creditmemo/Item.php | 58 +- .../Model/Order/Creditmemo/Total/Discount.php | 28 +- .../Model/Order/Creditmemo/Total/Subtotal.php | 4 +- .../Model/Order/Creditmemo/Total/Tax.php | 65 +- .../core/Mage/Sales/Model/Order/Invoice.php | 26 + .../Mage/Sales/Model/Order/Invoice/Item.php | 25 +- .../Model/Order/Invoice/Total/Discount.php | 17 +- .../Model/Order/Invoice/Total/Subtotal.php | 29 +- .../Sales/Model/Order/Invoice/Total/Tax.php | 31 +- app/code/core/Mage/Sales/Model/Order/Item.php | 8 +- .../core/Mage/Sales/Model/Order/Payment.php | 7 +- .../Mage/Sales/Model/Order/Pdf/Abstract.php | 6 +- .../Sales/Model/Order/Pdf/Items/Abstract.php | 2 +- app/code/core/Mage/Sales/Model/Quote.php | 56 +- .../core/Mage/Sales/Model/Quote/Address.php | 2 + .../Model/Quote/Address/Total/Discount.php | 3 +- .../Mage/Sales/Model/Quote/Item/Abstract.php | 5 +- .../Sales/Model/Resource/Helper/Interface.php | 49 + .../Sales/Model/Resource/Helper/Mysql4.php | 54 +- .../Address/Attribute/Frontend/Discount.php | 5 +- .../Report/Bestsellers/Collection.php | 5 +- .../Resource/Report/Collection/Abstract.php | 240 +- .../core/Mage/Sales/Model/Service/Order.php | 13 +- .../core/Mage/Sales/Model/Service/Quote.php | 9 +- .../data-upgrade-1.6.0.4-1.6.0.5.php | 76 + app/code/core/Mage/Sales/etc/config.xml | 20 +- .../sales_setup/upgrade-1.6.0.4-1.6.0.5.php | 49 + .../sales_setup/upgrade-1.6.0.5-1.6.0.6.php | 47 + .../sales_setup/upgrade-1.6.0.6-1.6.0.7.php | 36 + .../core/Mage/SalesRule/Helper/Coupon.php | 147 + app/code/core/Mage/SalesRule/Model/Coupon.php | 17 +- .../SalesRule/Model/Coupon/Massgenerator.php | 188 + .../core/Mage/SalesRule/Model/Observer.php | 38 + .../Mage/SalesRule/Model/Quote/Discount.php | 2 +- .../Mage/SalesRule/Model/Resource/Coupon.php | 67 +- .../Model/Resource/Coupon/Collection.php | 36 +- .../Model/Resource/Report/Collection.php | 57 + .../SalesRule/Model/Resource/Report/Rule.php | 29 + .../Model/Resource/Report/Rule/Createdat.php | 7 + .../Mage/SalesRule/Model/Resource/Rule.php | 4 +- .../Model/Resource/Rule/Collection.php | 25 +- app/code/core/Mage/SalesRule/Model/Rule.php | 93 +- .../Model/Rule/Condition/Product/Found.php | 10 +- .../Rule/Condition/Product/Subselect.php | 35 +- .../System/Config/Source/Coupon/Format.php | 54 + .../core/Mage/SalesRule/Model/Validator.php | 29 +- app/code/core/Mage/SalesRule/etc/config.xml | 28 +- app/code/core/Mage/SalesRule/etc/system.xml | 95 + .../upgrade-1.6.0.1-1.6.0.2.php | 145 + app/code/core/Mage/Sendfriend/etc/system.xml | 2 + .../Mage/Shipping/Model/Carrier/Abstract.php | 85 +- .../Shipping/Model/Carrier/Freeshipping.php | 8 +- .../Model/Resource/Carrier/Tablerate.php | 21 +- .../core/Mage/Shipping/Model/Shipping.php | 13 +- .../Tag/Model/Resource/Indexer/Summary.php | 12 +- app/code/core/Mage/Tag/Model/Resource/Tag.php | 6 +- .../Mage/Tag/controllers/IndexController.php | 19 +- app/code/core/Mage/Tag/etc/config.xml | 2 +- app/code/core/Mage/Tax/Helper/Data.php | 4 +- app/code/core/Mage/Tax/Model/Calculation.php | 2 +- .../core/Mage/Tax/Model/Resource/Class.php | 2 +- .../Tax/Model/Sales/Total/Quote/Subtotal.php | 18 +- .../Mage/Tax/Model/Sales/Total/Quote/Tax.php | 12 +- .../Usa/Block/Adminhtml/Dhl/Unitofmeasure.php | 82 + .../Usa/Model/Shipping/Carrier/Abstract.php | 19 +- .../Mage/Usa/Model/Shipping/Carrier/Dhl.php | 31 +- .../Shipping/Carrier/Dhl/International.php | 1490 ++ .../Dhl/International/Source/Contenttype.php | 50 + .../Dhl/International/Source/Freemethod.php | 41 + .../Dhl/International/Source/Method.php | 43 + .../International/Source/Method/Abstract.php | 72 + .../Dhl/International/Source/Method/All.php | 46 + .../Dhl/International/Source/Method/Doc.php | 43 + .../International/Source/Method/Freedoc.php | 50 + .../Source/Method/Freenondoc.php | 50 + .../International/Source/Method/Nondoc.php | 43 + .../Dhl/International/Source/Method/Size.php | 51 + .../Source/Method/Unitofmeasure.php | 51 + .../Mage/Usa/Model/Shipping/Carrier/Fedex.php | 37 +- .../Mage/Usa/Model/Shipping/Carrier/Ups.php | 42 +- .../Mage/Usa/Model/Shipping/Carrier/Usps.php | 2 + app/code/core/Mage/Usa/etc/config.xml | 24 +- .../Usa/etc/dhl/international/countries.xml | 1553 ++ app/code/core/Mage/Usa/etc/system.xml | 289 +- app/code/core/Mage/Weee/Helper/Data.php | 75 +- app/code/core/Mage/Weee/Model/Observer.php | 2 +- .../Widget/Block/Adminhtml/Widget/Chooser.php | 24 +- .../Widget/Instance/Edit/Chooser/Layout.php | 7 +- .../Widget/Instance/Edit/Tab/Main/Layout.php | 20 +- .../Resource/Widget/Instance/Collection.php | 8 + .../Adminhtml/Widget/InstanceController.php | 2 +- .../Mage/Wishlist/Block/Share/Email/Items.php | 15 + .../Wishlist/controllers/IndexController.php | 7 +- app/code/core/Mage/XmlConnect/Block/Cart.php | 14 +- .../Mage/XmlConnect/Block/Cart/Crosssell.php | 7 +- .../XmlConnect/Block/Cart/Item/Renderer.php | 15 +- .../Mage/XmlConnect/Block/Cart/Totals.php | 4 +- .../core/Mage/XmlConnect/Block/Catalog.php | 2 +- .../XmlConnect/Block/Catalog/Category.php | 2 +- .../Block/Catalog/Category/Info.php | 2 +- .../Mage/XmlConnect/Block/Catalog/Filters.php | 8 +- .../Mage/XmlConnect/Block/Catalog/Product.php | 7 +- .../Block/Catalog/Product/Options.php | 4 +- .../Block/Catalog/Product/Options/Bundle.php | 4 +- .../Catalog/Product/Options/Configurable.php | 6 +- .../Block/Catalog/Product/Options/Grouped.php | 2 +- .../Block/Catalog/Product/Price/Bundle.php | 16 +- .../Block/Catalog/Product/Price/Default.php | 18 +- .../Block/Catalog/Product/Review.php | 6 +- .../Mage/XmlConnect/Block/Catalog/Search.php | 6 +- .../Block/Catalog/Search/Suggest.php | 2 +- .../Block/Checkout/Address/Billing.php | 2 +- .../Block/Checkout/Address/Shipping.php | 2 +- .../XmlConnect/Block/Checkout/Agreements.php | 9 +- .../Block/Checkout/Order/Review/Info.php | 11 +- .../Block/Checkout/Payment/Method/List.php | 2 +- .../Checkout/Shipping/Method/Available.php | 6 +- .../Block/Customer/Address/Form.php | 10 +- .../Block/Customer/Address/List.php | 8 +- .../Mage/XmlConnect/Block/Customer/Form.php | 30 +- .../XmlConnect/Block/Customer/Order/List.php | 2 +- .../XmlConnect/Block/Customer/Storecredit.php | 14 +- app/code/core/Mage/XmlConnect/Block/Home.php | 2 +- .../Mage/XmlConnect/Block/Review/Form.php | 2 +- .../core/Mage/XmlConnect/Block/Wishlist.php | 11 +- .../Helper/Customer/Form/Renderer.php | 4 +- .../Mage/XmlConnect/Helper/Customer/Order.php | 12 +- app/code/core/Mage/XmlConnect/Helper/Data.php | 59 +- .../core/Mage/XmlConnect/Model/Observer.php | 1 - .../XmlConnect/Model/Simplexml/Element.php | 39 +- .../Model/Simplexml/Form/Abstract.php | 2 +- .../Form/Element/Validator/Abstract.php | 29 +- .../Adminhtml/MobileController.php | 38 +- .../controllers/CatalogController.php | 9 +- .../controllers/CheckoutController.php | 6 +- .../controllers/CustomerController.php | 1 - .../controllers/PbridgeController.php | 4 +- app/code/core/Mage/XmlConnect/etc/config.xml | 3 +- app/code/core/Zend/Date.php | 5008 ++++++ .../default/default/layout/captcha.xml | 48 + .../default/default/layout/customer.xml | 66 + .../adminhtml/default/default/layout/main.xml | 35 +- .../default/default/layout/promo.xml | 11 + .../default/default/layout/report.xml | 20 + .../default/default/layout/sales.xml | 2 +- .../adminhtml/default/default/layout/tag.xml | 36 + .../default/template/backup/dialogs.phtml | 125 + .../default/template/backup/list.phtml | 7 +- .../default/template/captcha/zend.phtml | 57 + .../catalog/product/attribute/js.phtml | 41 +- .../catalog/product/attribute/set/main.phtml | 14 +- .../product/edit/action/inventory.phtml | 22 +- .../product/edit/options/type/file.phtml | 2 +- .../catalog/product/edit/price/group.phtml | 169 + .../catalog/product/edit/price/tier.phtml | 6 +- .../catalog/product/edit/super/config.phtml | 8 +- .../template/catalog/product/price.phtml | 14 +- .../catalog/product/tab/inventory.phtml | 22 +- .../tab/account/form/renderer/group.phtml | 52 + .../create/billing/form/renderer/vat.phtml | 52 + .../customer/system/config/validatevat.phtml | 65 + .../template/customer/tab/addresses.phtml | 6 +- .../default/template/customer/tab/cart.phtml | 4 +- .../product/edit/downloadable.phtml | 2 +- .../product/edit/downloadable/links.phtml | 4 +- .../product/edit/downloadable/samples.phtml | 2 +- .../default/template/forgotpassword.phtml | 36 +- .../default/default/template/login.phtml | 2 + .../template/notification/toolbar.phtml | 12 +- .../default/default/template/page/head.phtml | 1 - .../default/template/page/js/translate.phtml | 14 +- .../default/template/page/notices.phtml | 2 +- .../system/config/fieldset/global.phtml | 63 +- .../system/config/payflowlink/advanced.phtml | 102 + .../system/config/payflowlink/info.phtml | 145 +- .../default/template/poll/answers/list.phtml | 2 +- .../default/default/template/promo/js.phtml | 18 +- .../default/template/promo/salesrulejs.phtml | 113 + .../order/create/billing/method/form.phtml | 2 +- .../sales/order/create/sidebar/items.phtml | 3 + .../default/template/sales/order/totals.phtml | 6 +- .../sales/order/view/tab/history.phtml | 11 +- .../default/template/system/config/js.phtml | 7 +- .../template/usa/dhl/unitofmeasure.phtml | 53 + .../widget/instance/edit/layout.phtml | 80 +- .../default/template/widget/instance/js.phtml | 2 +- .../frontend/base/default/layout/captcha.xml | 106 + .../frontend/base/default/layout/checkout.xml | 4 +- .../frontend/base/default/layout/customer.xml | 4 +- .../base/default/layout/googleanalytics.xml | 2 +- .../frontend/base/default/layout/paypal.xml | 5 +- .../base/default/template/captcha/zend.phtml | 53 + .../catalog/product/compare/list.phtml | 4 +- .../template/catalog/product/price.phtml | 14 +- .../catalog/product/view/options.phtml | 21 +- .../catalog/product/view/tierprices.phtml | 4 +- .../template/catalog/seo/sitemap.phtml | 4 +- .../template/catalogsearch/form.mini.phtml | 9 +- .../base/default/template/checkout/cart.phtml | 3 +- .../template/checkout/onepage/billing.phtml | 9 +- .../template/checkout/onepage/login.phtml | 1 + .../template/checkout/onepage/progress.phtml | 21 +- .../template/checkout/total/default.phtml | 2 +- .../template/customer/address/edit.phtml | 6 + .../customer/form/forgotpassword.phtml | 1 + .../template/customer/form/login.phtml | 1 + .../template/customer/form/register.phtml | 1 + .../default/template/giftmessage/inline.phtml | 2 +- .../default/template/googleanalytics/ga.phtml | 43 + .../default/template/page/html/notices.phtml | 2 +- .../template/paypal/express/review.phtml | 49 +- .../base/default/template/paypal/hss/js.phtml | 2 +- .../template/paypal/hss/redirect.phtml | 3 + .../paypal/payflowadvanced/form.phtml | 45 + .../paypal/payflowadvanced/iframe.phtml | 48 + .../paypal/payflowadvanced/info.phtml | 22 +- .../paypal/payflowadvanced/redirect.phtml | 71 + .../template/paypal/payflowlink/iframe.phtml | 97 +- .../template/paypal/payflowlink/info.phtml | 18 +- .../paypal/payflowlink/redirect.phtml | 74 +- .../persistent/checkout/onepage/billing.phtml | 11 +- .../persistent/checkout/onepage/login.phtml | 3 + .../persistent/customer/form/login.phtml | 1 + .../persistent/customer/form/register.phtml | 1 + .../template/wishlist/email/items.phtml | 11 +- .../default/template/wishlist/shared.phtml | 4 +- .../default/iphone/layout/catalog.xml | 42 +- .../default/iphone/layout/catalogsearch.xml | 155 + .../default/iphone/layout/checkout.xml | 17 +- .../frontend/default/iphone/layout/cms.xml | 32 +- .../default/iphone/layout/contacts.xml | 62 + .../default/iphone/layout/customer.xml | 14 +- .../frontend/default/iphone/layout/page.xml | 80 +- .../default/iphone/layout/persistent.xml | 12 + .../frontend/default/iphone/layout/review.xml | 7 +- .../default/iphone/layout/sendfriend.xml | 41 + .../frontend/default/iphone/layout/tag.xml | 10 - .../default/iphone/locale/en_US/translate.csv | 1 - .../bundle/sales/order/items/renderer.phtml | 385 + .../iphone/template/catalog/layer/view.phtml | 54 + .../template/catalog/navigation/top.phtml | 54 + .../catalog/product/compare/list.phtml | 258 + .../catalog/product/compare/sidebar.phtml | 68 + .../template/catalog/product/gallery.phtml | 56 + .../template/catalog/product/list.phtml | 209 + .../catalog/product/list/toolbar.phtml | 62 +- .../catalog/product/list/upsell.phtml | 53 + .../template/catalog/product/price.phtml | 407 + .../template/catalog/product/view.phtml | 156 + .../template/catalog/product/view/addto.phtml | 43 + .../catalog/product/view/addtocart.phtml | 38 + .../template/catalog/product/view/media.phtml | 55 + .../catalog/product/view/type/grouped.phtml | 43 + .../product/view/type/grouped_grid.phtml | 76 + .../template/catalogsearch/form.mini.phtml | 23 +- .../template/catalogsearch/result.phtml | 79 + .../iphone/template/checkout/cart.phtml | 102 +- .../template/checkout/cart/item/default.phtml | 94 +- .../cart/noItemsHeader.phtml} | 26 +- .../template/checkout/cart/sidebar.phtml | 79 + .../iphone/template/checkout/cartheader.phtml | 102 + .../template/checkout/onepage/login.phtml | 121 + .../iphone/template/customer/form/edit.phtml | 127 + .../customer/form/forgotpassword.phtml | 20 +- .../iphone/template/customer/form/login.phtml | 51 +- .../template/customer/form/register.phtml | 178 + .../iphone/template/page/1column.phtml | 41 +- .../iphone/template/page/html/footer.phtml | 8 +- .../iphone/template/page/html/head.phtml | 22 +- .../iphone/template/page/html/header.phtml | 51 +- .../iphone/template/page/html/pager.phtml | 113 + .../template/page/switch/languages.phtml | 46 + .../iphone/template/page/switch/stores.phtml | 46 + .../persistent/checkout/onepage/login.phtml | 154 + .../persistent/customer/form/login.phtml | 53 +- .../persistent/customer/form/register.phtml | 189 + .../iphone/template/sales/order/items.phtml | 73 + .../iphone/template/sales/order/totals.phtml | 52 + .../iphone/template/sendfriend/send.phtml | 148 + .../default/iphone/template/tag/list.phtml | 35 + .../iphone/template/wishlist/view.phtml | 110 +- .../default/modern/layout/checkout.xml | 2 +- .../default/modern/layout/customer.xml | 6 +- .../template/catalogsearch/form.mini.phtml | 9 +- .../modern/template/checkout/cart.phtml | 3 +- .../default/template/install/config.phtml | 3 - app/etc/local.xml.additional | 14 + app/etc/modules/Mage_Captcha.xml | 39 + app/locale/en_US/Mage_Adminhtml.csv | 83 +- app/locale/en_US/Mage_Backup.csv | 32 +- app/locale/en_US/Mage_Captcha.csv | 22 + app/locale/en_US/Mage_Catalog.csv | 25 +- app/locale/en_US/Mage_CatalogInventory.csv | 9 + app/locale/en_US/Mage_CatalogSearch.csv | 1 + app/locale/en_US/Mage_Checkout.csv | 22 +- app/locale/en_US/Mage_Core.csv | 17 + app/locale/en_US/Mage_Customer.csv | 57 + app/locale/en_US/Mage_Dataflow.csv | 2 + app/locale/en_US/Mage_Directory.csv | 1 + app/locale/en_US/Mage_Downloadable.csv | 2 + app/locale/en_US/Mage_Eav.csv | 1 + app/locale/en_US/Mage_GoogleBase.csv | 1 + app/locale/en_US/Mage_GoogleCheckout.csv | 31 + app/locale/en_US/Mage_Index.csv | 6 +- app/locale/en_US/Mage_Install.csv | 5 + app/locale/en_US/Mage_Page.csv | 2 +- app/locale/en_US/Mage_Paygate.csv | 1 - app/locale/en_US/Mage_Payment.csv | 13 + app/locale/en_US/Mage_Paypal.csv | 14 +- app/locale/en_US/Mage_Persistent.csv | 1 + app/locale/en_US/Mage_Reports.csv | 2 +- app/locale/en_US/Mage_Review.csv | 4 + app/locale/en_US/Mage_Rss.csv | 2 + app/locale/en_US/Mage_Rule.csv | 4 - app/locale/en_US/Mage_Sales.csv | 11 + app/locale/en_US/Mage_SalesRule.csv | 37 +- app/locale/en_US/Mage_Shipping.csv | 7 + app/locale/en_US/Mage_Tag.csv | 2 + app/locale/en_US/Mage_Tax.csv | 2 +- app/locale/en_US/Mage_Usa.csv | 47 +- app/locale/en_US/Mage_Widget.csv | 2 + app/locale/en_US/Mage_XmlConnect.csv | 28 +- downloader/Maged/Connect.php | 260 +- downloader/Maged/Controller.php | 101 +- downloader/lib/Mage/Archive.php | 8 +- downloader/lib/Mage/Archive/Abstract.php | 9 +- downloader/lib/Mage/Archive/Bz.php | 38 +- downloader/lib/Mage/Archive/Gz.php | 38 +- downloader/lib/Mage/Archive/Helper/File.php | 271 + .../lib/Mage/Archive/Helper/File/Bz.php | 92 + .../lib/Mage/Archive/Helper/File/Gz.php | 83 + downloader/lib/Mage/Archive/Tar.php | 399 +- downloader/lib/Mage/Backup.php | 59 + downloader/lib/Mage/Backup/Abstract.php | 222 + downloader/lib/Mage/Backup/Archive/Tar.php | 82 + downloader/lib/Mage/Backup/Db.php | 128 + downloader/lib/Mage/Backup/Exception.php | 36 + .../Backup/Exception/CantLoadSnapshot.php | 36 + .../Backup/Exception/FtpConnectionFailed.php | 36 + .../Backup/Exception/FtpValidationFailed.php | 36 + .../Backup/Exception/NotEnoughFreeSpace.php | 36 + .../Backup/Exception/NotEnoughPermissions.php | 36 + downloader/lib/Mage/Backup/Filesystem.php | 284 + .../lib/Mage/Backup/Filesystem/Helper.php | 137 + .../Backup/Filesystem/Iterator/Filter.php | 77 + .../Backup/Filesystem/Rollback/Abstract.php | 57 + .../Mage/Backup/Filesystem/Rollback/Fs.php | 78 + .../Mage/Backup/Filesystem/Rollback/Ftp.php | 194 + downloader/lib/Mage/Backup/Interface.php | 88 + downloader/lib/Mage/Backup/Media.php | 99 + downloader/lib/Mage/Backup/Snapshot.php | 140 + downloader/lib/Mage/Connect/Command.php | 115 +- .../lib/Mage/Connect/Command/Config.php | 21 +- .../lib/Mage/Connect/Command/Install.php | 227 +- downloader/lib/Mage/Connect/Config.php | 179 +- downloader/lib/Mage/Connect/Frontend.php | 58 +- downloader/lib/Mage/Connect/Ftp.php | 104 +- downloader/lib/Mage/Connect/Loader.php | 7 +- downloader/lib/Mage/Connect/Packager.php | 305 +- downloader/lib/Mage/Connect/Rest.php | 144 +- downloader/lib/Mage/Connect/Singleconfig.php | 313 +- downloader/lib/Mage/Connect/Validator.php | 169 +- downloader/lib/Mage/HTTP/Client.php | 20 +- downloader/lib/Mage/System/Args.php | 4 +- downloader/lib/Mage/System/Dirs.php | 30 +- downloader/lib/Mage/System/Ftp.php | 509 + downloader/template/connect/iframe.phtml | 5 +- downloader/template/connect/packages.phtml | 26 +- index.php | 13 +- js/extjs/fix-defer-before.js | 37 + js/extjs/fix-defer.js | 2 +- js/mage/adminhtml/backup.js | 174 + js/mage/adminhtml/flexuploader.js | 16 +- js/mage/adminhtml/loader.js | 7 +- js/mage/adminhtml/sales.js | 72 +- js/mage/adminhtml/wysiwyg/tiny_mce/setup.js | 13 +- js/mage/adminhtml/wysiwyg/widget.js | 11 + js/mage/captcha.js | 87 + js/mage/translate_inline.js | 22 +- js/prototype/validation.js | 27 +- js/tiny_mce/classes/AddOnManager.js | 467 +- js/tiny_mce/classes/CommandManager.js | 57 - js/tiny_mce/classes/ControlManager.js | 31 +- js/tiny_mce/classes/Developer.js | 94 - js/tiny_mce/classes/Editor.js | 5986 ++++--- js/tiny_mce/classes/EditorCommands.js | 188 +- js/tiny_mce/classes/EditorManager.js | 50 + js/tiny_mce/classes/ForceBlocks.js | 340 +- js/tiny_mce/classes/Formatter.js | 3544 ++-- js/tiny_mce/classes/LegacyInput.js | 12 +- js/tiny_mce/classes/Popup.js | 24 +- js/tiny_mce/classes/UndoManager.js | 73 +- js/tiny_mce/classes/WindowManager.js | 48 + js/tiny_mce/classes/adapter/jquery/adapter.js | 25 +- .../classes/adapter/jquery/jquery.tinymce.js | 20 +- js/tiny_mce/classes/dom/DOMUtils.js | 827 +- js/tiny_mce/classes/dom/Element.js | 6 + js/tiny_mce/classes/dom/EventUtils.js | 22 +- js/tiny_mce/classes/dom/Range.js | 23 +- js/tiny_mce/classes/dom/RangeUtils.js | 85 +- js/tiny_mce/classes/dom/Schema.js | 185 - js/tiny_mce/classes/dom/ScriptLoader.js | 72 +- js/tiny_mce/classes/dom/Selection.js | 1869 +- js/tiny_mce/classes/dom/Serializer.js | 1194 +- js/tiny_mce/classes/dom/StringWriter.js | 212 - js/tiny_mce/classes/dom/TreeWalker.js | 2 +- js/tiny_mce/classes/dom/TridentSelection.js | 626 +- js/tiny_mce/classes/dom/XMLWriter.js | 165 - js/tiny_mce/classes/firebug/FIREBUG.LICENSE | 30 + js/tiny_mce/classes/html/DomParser.js | 577 + js/tiny_mce/classes/html/Entities.js | 253 + js/tiny_mce/classes/html/Node.js | 474 + js/tiny_mce/classes/html/SaxParser.js | 355 + js/tiny_mce/classes/html/Schema.js | 663 + js/tiny_mce/classes/html/Serializer.js | 152 + js/tiny_mce/classes/html/Styles.js | 279 + js/tiny_mce/classes/html/Writer.js | 186 + js/tiny_mce/classes/tinymce.js | 1533 +- js/tiny_mce/classes/ui/Button.js | 16 +- js/tiny_mce/classes/ui/ColorSplitButton.js | 84 +- js/tiny_mce/classes/ui/Container.js | 4 +- js/tiny_mce/classes/ui/Control.js | 28 +- js/tiny_mce/classes/ui/DropMenu.js | 137 +- js/tiny_mce/classes/ui/KeyboardNavigation.js | 183 + js/tiny_mce/classes/ui/ListBox.js | 116 +- js/tiny_mce/classes/ui/MenuButton.js | 40 +- js/tiny_mce/classes/ui/MenuItem.js | 1 + js/tiny_mce/classes/ui/NativeListBox.js | 17 +- js/tiny_mce/classes/ui/Separator.js | 3 +- js/tiny_mce/classes/ui/SplitButton.js | 80 +- js/tiny_mce/classes/ui/Toolbar.js | 8 +- js/tiny_mce/classes/ui/ToolbarGroup.js | 81 + js/tiny_mce/classes/util/Cookie.js | 9 + js/tiny_mce/classes/util/Dispatcher.js | 6 + js/tiny_mce/classes/util/JSON.js | 88 +- js/tiny_mce/classes/util/JSONRequest.js | 25 +- js/tiny_mce/classes/util/Quirks.js | 229 + js/tiny_mce/classes/util/URI.js | 19 +- js/tiny_mce/classes/util/VK.js | 15 + js/tiny_mce/classes/util/XHR.js | 8 + js/tiny_mce/jquery.tinymce.js | 2 +- js/tiny_mce/langs/en.js | 171 +- js/tiny_mce/plugins/advhr/langs/en_dlg.js | 6 +- js/tiny_mce/plugins/advhr/rule.htm | 59 +- js/tiny_mce/plugins/advimage/editor_plugin.js | 2 +- .../plugins/advimage/editor_plugin_src.js | 2 +- js/tiny_mce/plugins/advimage/image.htm | 65 +- js/tiny_mce/plugins/advimage/js/image.js | 45 +- js/tiny_mce/plugins/advimage/langs/en_dlg.js | 44 +- js/tiny_mce/plugins/advlink/js/advlink.js | 38 +- js/tiny_mce/plugins/advlink/langs/en_dlg.js | 53 +- js/tiny_mce/plugins/advlink/link.htm | 65 +- js/tiny_mce/plugins/advlist/editor_plugin.js | 2 +- .../plugins/advlist/editor_plugin_src.js | 40 +- js/tiny_mce/plugins/autolink/editor_plugin.js | 1 + .../plugins/autolink/editor_plugin_src.js | 172 + .../plugins/autoresize/editor_plugin.js | 2 +- .../plugins/autoresize/editor_plugin_src.js | 28 +- js/tiny_mce/plugins/autosave/editor_plugin.js | 2 +- .../plugins/autosave/editor_plugin_src.js | 23 +- js/tiny_mce/plugins/bbcode/editor_plugin.js | 2 +- .../plugins/bbcode/editor_plugin_src.js | 2 +- .../plugins/contextmenu/editor_plugin.js | 2 +- .../plugins/contextmenu/editor_plugin_src.js | 55 +- js/tiny_mce/plugins/emotions/emotions.htm | 62 +- .../emotions/img/smiley-foot-in-mouth.gif | Bin 344 -> 342 bytes .../plugins/emotions/img/smiley-laughing.gif | Bin 344 -> 343 bytes .../plugins/emotions/img/smiley-sealed.gif | Bin 325 -> 323 bytes .../plugins/emotions/img/smiley-smile.gif | Bin 345 -> 344 bytes .../plugins/emotions/img/smiley-surprised.gif | Bin 342 -> 338 bytes .../plugins/emotions/img/smiley-wink.gif | Bin 351 -> 350 bytes js/tiny_mce/plugins/emotions/js/emotions.js | 21 + js/tiny_mce/plugins/emotions/langs/en_dlg.js | 21 +- .../example_dependency/editor_plugin.js | 1 + .../example_dependency/editor_plugin_src.js | 50 + js/tiny_mce/plugins/fullpage/css/fullpage.css | 45 +- js/tiny_mce/plugins/fullpage/editor_plugin.js | 2 +- .../plugins/fullpage/editor_plugin_src.js | 378 +- js/tiny_mce/plugins/fullpage/fullpage.htm | 348 +- js/tiny_mce/plugins/fullpage/js/fullpage.js | 637 +- js/tiny_mce/plugins/fullpage/langs/en_dlg.js | 86 +- .../plugins/fullscreen/editor_plugin.js | 2 +- .../plugins/fullscreen/editor_plugin_src.js | 16 +- js/tiny_mce/plugins/fullscreen/fullscreen.htm | 3 +- .../plugins/inlinepopups/editor_plugin.js | 2 +- .../plugins/inlinepopups/editor_plugin_src.js | 96 +- .../skins/clearlooks2/img/alert.gif | Bin 818 -> 810 bytes .../skins/clearlooks2/img/button.gif | Bin 280 -> 272 bytes .../skins/clearlooks2/img/confirm.gif | Bin 915 -> 907 bytes .../skins/clearlooks2/img/corners.gif | Bin 911 -> 909 bytes .../skins/clearlooks2/img/vertical.gif | Bin 92 -> 84 bytes .../inlinepopups/skins/clearlooks2/window.css | 2 +- js/tiny_mce/plugins/layer/editor_plugin.js | 2 +- .../plugins/layer/editor_plugin_src.js | 60 +- .../plugins/legacyoutput/editor_plugin.js | 2 +- .../plugins/legacyoutput/editor_plugin_src.js | 59 +- js/tiny_mce/plugins/lists/editor_plugin.js | 1 + .../plugins/lists/editor_plugin_src.js | 925 + js/tiny_mce/plugins/media/css/content.css | 6 - js/tiny_mce/plugins/media/css/media.css | 9 +- js/tiny_mce/plugins/media/editor_plugin.js | 2 +- .../plugins/media/editor_plugin_src.js | 1068 +- js/tiny_mce/plugins/media/js/media.js | 1012 +- js/tiny_mce/plugins/media/langs/en_dlg.js | 104 +- js/tiny_mce/plugins/media/media.htm | 677 +- js/tiny_mce/plugins/media/moxieplayer.swf | Bin 0 -> 19980 bytes .../plugins/nonbreaking/editor_plugin.js | 2 +- .../plugins/nonbreaking/editor_plugin_src.js | 7 +- .../plugins/noneditable/editor_plugin.js | 2 +- .../plugins/noneditable/editor_plugin_src.js | 9 +- .../plugins/pagebreak/editor_plugin.js | 2 +- .../plugins/pagebreak/editor_plugin_src.js | 5 +- js/tiny_mce/plugins/paste/editor_plugin.js | 2 +- .../plugins/paste/editor_plugin_src.js | 393 +- js/tiny_mce/plugins/paste/langs/en_dlg.js | 6 +- .../plugins/searchreplace/editor_plugin.js | 2 +- .../searchreplace/editor_plugin_src.js | 4 + .../plugins/searchreplace/js/searchreplace.js | 24 +- .../plugins/searchreplace/langs/en_dlg.js | 17 +- .../plugins/searchreplace/searchreplace.htm | 33 +- .../plugins/spellchecker/editor_plugin.js | 2 +- .../plugins/spellchecker/editor_plugin_src.js | 153 +- js/tiny_mce/plugins/style/js/props.js | 12 +- js/tiny_mce/plugins/style/langs/en_dlg.js | 64 +- js/tiny_mce/plugins/style/props.htm | 913 +- js/tiny_mce/plugins/tabfocus/editor_plugin.js | 2 +- .../plugins/tabfocus/editor_plugin_src.js | 234 +- js/tiny_mce/plugins/table/cell.htm | 30 +- js/tiny_mce/plugins/table/editor_plugin.js | 2 +- .../plugins/table/editor_plugin_src.js | 2533 +-- js/tiny_mce/plugins/table/js/cell.js | 55 +- js/tiny_mce/plugins/table/js/row.js | 36 +- js/tiny_mce/plugins/table/js/table.js | 79 +- js/tiny_mce/plugins/table/langs/en_dlg.js | 75 +- js/tiny_mce/plugins/table/merge_cells.htm | 22 +- js/tiny_mce/plugins/table/row.htm | 21 +- js/tiny_mce/plugins/table/table.htm | 107 +- js/tiny_mce/plugins/template/js/template.js | 2 +- js/tiny_mce/plugins/template/langs/en_dlg.js | 16 +- .../plugins/visualchars/editor_plugin.js | 2 +- .../plugins/visualchars/editor_plugin_src.js | 33 +- .../plugins/wordcount/editor_plugin.js | 2 +- .../plugins/wordcount/editor_plugin_src.js | 56 +- js/tiny_mce/plugins/xhtmlxtras/abbr.htm | 11 +- js/tiny_mce/plugins/xhtmlxtras/acronym.htm | 11 +- js/tiny_mce/plugins/xhtmlxtras/attributes.htm | 11 +- js/tiny_mce/plugins/xhtmlxtras/cite.htm | 9 +- js/tiny_mce/plugins/xhtmlxtras/del.htm | 17 +- .../plugins/xhtmlxtras/editor_plugin.js | 2 +- .../plugins/xhtmlxtras/editor_plugin_src.js | 24 +- js/tiny_mce/plugins/xhtmlxtras/ins.htm | 21 +- .../plugins/xhtmlxtras/js/attributes.js | 17 +- js/tiny_mce/plugins/xhtmlxtras/js/del.js | 14 +- .../plugins/xhtmlxtras/js/element_common.js | 8 +- js/tiny_mce/plugins/xhtmlxtras/js/ins.js | 17 +- .../plugins/xhtmlxtras/langs/en_dlg.js | 33 +- js/tiny_mce/themes/advanced/about.htm | 8 +- js/tiny_mce/themes/advanced/anchor.htm | 10 +- js/tiny_mce/themes/advanced/charmap.htm | 85 +- js/tiny_mce/themes/advanced/color_picker.htm | 19 +- .../themes/advanced/editor_template.js | 2 +- .../themes/advanced/editor_template_src.js | 292 +- js/tiny_mce/themes/advanced/image.htm | 102 +- .../themes/advanced/img/colorpicker.jpg | Bin 3189 -> 2584 bytes js/tiny_mce/themes/advanced/img/flash.gif | Bin 0 -> 239 bytes js/tiny_mce/themes/advanced/img/icons.gif | Bin 11794 -> 11790 bytes js/tiny_mce/themes/advanced/img/iframe.gif | Bin 0 -> 600 bytes js/tiny_mce/themes/advanced/img/pagebreak.gif | Bin 0 -> 325 bytes js/tiny_mce/themes/advanced/img/quicktime.gif | Bin 0 -> 301 bytes js/tiny_mce/themes/advanced/img/realmedia.gif | Bin 0 -> 439 bytes js/tiny_mce/themes/advanced/img/shockwave.gif | Bin 0 -> 384 bytes js/tiny_mce/themes/advanced/img/trans.gif | Bin 0 -> 43 bytes js/tiny_mce/themes/advanced/img/video.gif | Bin 0 -> 597 bytes .../themes/advanced/img/windowsmedia.gif | Bin 0 -> 415 bytes js/tiny_mce/themes/advanced/js/about.js | 1 + js/tiny_mce/themes/advanced/js/anchor.js | 10 +- js/tiny_mce/themes/advanced/js/charmap.js | 40 +- .../themes/advanced/js/color_picker.js | 144 +- js/tiny_mce/themes/advanced/js/image.js | 18 +- js/tiny_mce/themes/advanced/js/link.js | 11 +- .../themes/advanced/js/source_editor.js | 18 +- js/tiny_mce/themes/advanced/langs/en.js | 63 +- js/tiny_mce/themes/advanced/langs/en_dlg.js | 52 +- js/tiny_mce/themes/advanced/link.htm | 55 +- js/tiny_mce/themes/advanced/shortcuts.htm | 47 + .../themes/advanced/skins/default/content.css | 19 +- .../advanced/skins/default/img/buttons.png | Bin 3274 -> 3133 bytes .../advanced/skins/default/img/items.gif | Bin 70 -> 64 bytes .../advanced/skins/default/img/tabs.gif | Bin 1326 -> 1322 bytes .../themes/advanced/skins/default/ui.css | 9 +- .../advanced/skins/highcontrast/content.css | 24 + .../advanced/skins/highcontrast/dialog.css | 105 + .../themes/advanced/skins/highcontrast/ui.css | 102 + .../themes/advanced/skins/o2k7/content.css | 15 +- .../themes/advanced/skins/o2k7/dialog.css | 1 + .../advanced/skins/o2k7/img/button_bg.png | Bin 5859 -> 2766 bytes .../skins/o2k7/img/button_bg_black.png | Bin 3736 -> 651 bytes .../skins/o2k7/img/button_bg_silver.png | Bin 5358 -> 2084 bytes js/tiny_mce/themes/advanced/skins/o2k7/ui.css | 14 +- .../themes/advanced/skins/o2k7/ui_black.css | 2 +- .../themes/advanced/skins/o2k7/ui_silver.css | 2 +- js/tiny_mce/themes/advanced/source_editor.htm | 6 +- js/tiny_mce/themes/simple/editor_template.js | 2 +- .../themes/simple/editor_template_src.js | 3 +- js/tiny_mce/themes/simple/img/icons.gif | Bin 1440 -> 806 bytes js/tiny_mce/themes/simple/langs/en.js | 12 +- js/tiny_mce/tiny_mce.js | 2 +- js/tiny_mce/tiny_mce_dev.js | 253 +- js/tiny_mce/tiny_mce_jquery.js | 2 +- js/tiny_mce/tiny_mce_jquery_src.js | 12399 ++++++++------ js/tiny_mce/tiny_mce_popup.js | 2 +- js/tiny_mce/tiny_mce_prototype.js | 2 +- js/tiny_mce/tiny_mce_prototype_src.js | 14200 +++++++++------- js/tiny_mce/tiny_mce_src.js | 14200 +++++++++------- js/tiny_mce/utils/editable_selects.js | 2 +- js/tiny_mce/utils/form_utils.js | 18 +- js/tiny_mce/utils/mctabs.js | 105 +- js/tiny_mce/utils/validate.js | 38 +- js/varien/js.js | 21 +- js/varien/product.js | 26 + .../LinLibertine_Re-4.4.1.ttf | Bin 0 -> 1693712 bytes lib/Mage/Archive.php | 11 +- lib/Mage/Archive/Abstract.php | 13 +- lib/Mage/Archive/Bz.php | 42 +- lib/Mage/Archive/Gz.php | 42 +- lib/Mage/Archive/Helper/File.php | 271 + lib/Mage/Archive/Helper/File/Bz.php | 92 + lib/Mage/Archive/Helper/File/Gz.php | 83 + lib/Mage/Archive/Interface.php | 4 +- lib/Mage/Archive/Tar.php | 401 +- lib/Mage/Backup.php | 59 + lib/Mage/Backup/Abstract.php | 250 + lib/Mage/Backup/Archive/Tar.php | 82 + lib/Mage/Backup/Db.php | 128 + lib/Mage/Backup/Exception.php | 36 + .../Backup/Exception/CantLoadSnapshot.php | 36 + .../Backup/Exception/FtpConnectionFailed.php | 36 + .../Backup/Exception/FtpValidationFailed.php | 36 + .../Backup/Exception/NotEnoughFreeSpace.php | 36 + .../Backup/Exception/NotEnoughPermissions.php | 36 + lib/Mage/Backup/Filesystem.php | 284 + lib/Mage/Backup/Filesystem/Helper.php | 137 + .../Backup/Filesystem/Iterator/Filter.php | 77 + .../Backup/Filesystem/Rollback/Abstract.php | 57 + lib/Mage/Backup/Filesystem/Rollback/Fs.php | 78 + lib/Mage/Backup/Filesystem/Rollback/Ftp.php | 194 + lib/Mage/Backup/Interface.php | 88 + lib/Mage/Backup/Media.php | 99 + lib/Mage/Backup/Snapshot.php | 140 + lib/Mage/Connect/Ftp.php | 462 +- lib/Mage/Exception.php | 5 +- lib/Mage/System/Args.php | 8 +- lib/Mage/System/Dirs.php | 2 +- lib/Mage/System/Ftp.php | 509 + lib/Varien/Autoload.php | 4 +- lib/Varien/Convert/Adapter/Http/Curl.php | 2 + lib/Varien/Convert/Parser/Xml/Excel.php | 4 +- lib/Varien/Data/Collection/Filesystem.php | 2 +- lib/Varien/Data/Form/Abstract.php | 15 +- lib/Varien/Data/Form/Element/Collection.php | 7 +- lib/Varien/Data/Form/Element/Label.php | 25 +- lib/Varien/Date.php | 2 +- lib/Varien/Db/Adapter/Pdo/Mysql.php | 11 +- lib/Varien/Db/Select.php | 16 +- lib/Varien/File/Object.php | 13 +- lib/Varien/Http/Adapter/Curl.php | 78 +- lib/Zend/Captcha/Word.php | 16 +- skin/adminhtml/default/default/boxes.css | 45 +- .../default/default/images/btn_gr_bg.gif | Bin 0 -> 148 bytes .../default/default/images/btn_gr_on.gif | Bin 0 -> 137 bytes .../default/default/images/btn_gr_over.gif | Bin 0 -> 147 bytes .../default/default/images/reload.png | Bin 0 -> 1538 bytes skin/frontend/base/default/images/reload.png | Bin 0 -> 1538 bytes skin/frontend/base/default/js/opcheckout.js | 41 +- skin/frontend/default/blank/css/styles.css | 28 +- skin/frontend/default/blue/css/styles.css | 26 +- skin/frontend/default/default/css/styles.css | 30 +- skin/frontend/default/iphone/css/iphone.css | 3330 +++- .../default/iphone/images/bg_breadcrumb.png | Bin 0 -> 2864 bytes .../iphone/images/bg_breadcrumb_active.png | Bin 0 -> 2840 bytes .../default/iphone/images/bg_checkbox.png | Bin 0 -> 489 bytes .../iphone/images/bg_checkout_step_passed.png | Bin 0 -> 1098 bytes .../default/iphone/images/bg_drop.jpg | Bin 0 -> 12907 bytes .../default/iphone/images/bg_nav_brd.png | Bin 0 -> 259 bytes .../iphone/images/bg_nav_brd_inactive.png | Bin 0 -> 869 bytes .../frontend/default/iphone/images/bg_qty.png | Bin 0 -> 283 bytes .../default/iphone/images/bg_rating_star.png | Bin 0 -> 1081 bytes .../default/iphone/images/bkg_rating.gif | Bin 404 -> 0 bytes .../iphone/images/btn_google_checkout.gif | Bin 2940 -> 0 bytes .../iphone/images/btn_paypal_checkout.gif | Bin 3062 -> 0 bytes .../default/iphone/images/btn_place_order.gif | Bin 2929 -> 0 bytes .../iphone/images/btn_proceed_to_checkout.gif | Bin 3048 -> 0 bytes .../iphone/images/btn_proceed_to_checkout.png | Bin 2777 -> 0 bytes .../images/btn_proceed_to_checkout_bg.gif | Bin 2474 -> 0 bytes .../images/btn_proceed_to_checkout_rad.gif | Bin 1126 -> 0 bytes .../default/iphone/images/btn_trash.gif | Bin 643 -> 1353 bytes .../default/iphone/images/btn_trash.png | Bin 0 -> 1227 bytes .../iphone/images/btn_window_close.gif | Bin 226 -> 0 bytes .../default/iphone/images/chart_remove.gif | Bin 531 -> 0 bytes .../default/iphone/images/error_msg_icon.gif | Bin 1013 -> 0 bytes .../frontend/default/iphone/images/fabric.jpg | Bin 0 -> 57452 bytes skin/frontend/default/iphone/images/flip.png | Bin 0 -> 3124 bytes .../default/iphone/images/footer-bg.gif | Bin 153 -> 0 bytes .../iphone/images/free_shipping_callout.jpg | Bin 14639 -> 0 bytes .../default/iphone/images/front_banner.png | Bin 0 -> 7355 bytes .../iphone/images/gift-message-close.gif | Bin 122 -> 0 bytes skin/frontend/default/iphone/images/grid.png | Bin 0 -> 946 bytes .../default/iphone/images/header-bg.gif | Bin 144 -> 0 bytes .../default/iphone/images/i_add_to_cart.png | Bin 0 -> 1345 bytes .../iphone/images/i_add_to_wishlist.png | Bin 0 -> 1240 bytes .../default/iphone/images/i_arrow.png | Bin 0 -> 240 bytes .../default/iphone/images/i_arrow_small.png | Bin 0 -> 1304 bytes .../default/iphone/images/i_arrow_white.png | Bin 0 -> 1134 bytes .../default/iphone/images/i_dropdown.png | Bin 0 -> 1081 bytes .../default/iphone/images/i_gallery.png | Bin 0 -> 1129 bytes .../default/iphone/images/i_menu_arrow.png | Bin 0 -> 1036 bytes .../default/iphone/images/i_menu_cart.png | Bin 0 -> 1155 bytes .../default/iphone/images/i_menu_search.png | Bin 0 -> 1602 bytes .../default/iphone/images/i_notice.gif | Bin 802 -> 0 bytes .../default/iphone/images/i_search.png | Bin 0 -> 408 bytes .../frontend/default/iphone/images/i_star.png | Bin 0 -> 4117 bytes .../default/iphone/images/i_star_black.png | Bin 0 -> 3540 bytes .../default/iphone/images/i_tell_a_friend.png | Bin 0 -> 1865 bytes .../default/iphone/images/i_view_details.png | Bin 0 -> 1694 bytes .../default/iphone/images/link_separator.gif | Bin 44 -> 0 bytes skin/frontend/default/iphone/images/list.png | Bin 0 -> 946 bytes .../default/iphone/images/list_remove_btn.gif | Bin 204 -> 0 bytes .../frontend/default/iphone/images/loader.gif | Bin 0 -> 10107 bytes skin/frontend/default/iphone/images/logo.gif | Bin 2493 -> 2603 bytes skin/frontend/default/iphone/images/logo.png | Bin 0 -> 3660 bytes .../default/iphone/images/logo_email.gif | Bin 3407 -> 0 bytes .../default/iphone/images/logo_print.gif | Bin 3407 -> 0 bytes .../default/iphone/images/menu-arrow.gif | Bin 167 -> 0 bytes .../default/iphone/images/menu-bg.gif | Bin 153 -> 0 bytes .../frontend/default/iphone/images/nav_bg.png | Bin 0 -> 958 bytes .../default/iphone/images/nextlabel.png | Bin 0 -> 1374 bytes .../default/iphone/images/np_cart_thumb.gif | Bin 1372 -> 0 bytes .../default/iphone/images/np_more_img.gif | Bin 908 -> 0 bytes .../default/iphone/images/np_product_main.gif | Bin 4683 -> 0 bytes .../default/iphone/images/np_thumb.gif | Bin 1945 -> 0 bytes .../default/iphone/images/np_thumb2.gif | Bin 1779 -> 0 bytes .../default/iphone/images/opc-ajax-loader.gif | Bin 1849 -> 1737 bytes .../default/iphone/images/opc_off_head_bg.gif | Bin 49 -> 0 bytes .../default/iphone/images/opc_on_box_bg.gif | Bin 63 -> 0 bytes .../default/iphone/images/page-bg.gif | Bin 152 -> 0 bytes .../iphone/images/pager_arrow_left.gif | Bin 155 -> 0 bytes .../iphone/images/pager_arrow_right.gif | Bin 155 -> 0 bytes .../default/iphone/images/pager_bg.gif | Bin 150 -> 0 bytes skin/frontend/default/iphone/images/ph.gif | Bin 43 -> 0 bytes .../images/place_order_container_bg.gif | Bin 1503 -> 0 bytes .../default/iphone/images/prevlabel.png | Bin 0 -> 1241 bytes .../images/product_rating_blank_star.gif | Bin 304 -> 0 bytes .../images/product_rating_full_star.gif | Bin 306 -> 0 bytes .../default/iphone/images/search-bg.gif | Bin 273 -> 0 bytes .../iphone/images/search-close-but.gif | Bin 682 -> 0 bytes .../default/iphone/images/search-end.gif | Bin 690 -> 0 bytes .../default/iphone/images/search-go-but.gif | Bin 669 -> 0 bytes .../default/iphone/images/search-md.gif | Bin 165 -> 0 bytes .../default/iphone/images/search-st.gif | Bin 842 -> 0 bytes .../default/iphone/images/search_criteria.gif | Bin 268 -> 0 bytes .../default/iphone/images/search_icon.png | Bin 0 -> 1663 bytes .../iphone/images/shipping_method_pointer.gif | Bin 1144 -> 0 bytes .../default/iphone/images/sort_asc_arrow.gif | Bin 115 -> 0 bytes .../default/iphone/images/sort_desc_arrow.gif | Bin 114 -> 0 bytes .../iphone/images/success_msg_icon.gif | Bin 1024 -> 0 bytes .../iphone/images/validation_advice_bg.gif | Bin 134 -> 0 bytes skin/frontend/default/iphone/js/dnd.js | 688 + skin/frontend/default/iphone/js/iphone.js | 1044 ++ skin/frontend/default/iphone/js/opcheckout.js | 872 - skin/frontend/default/modern/css/styles.css | 27 +- .../Interface_Adminhtml_Default-1.6.1.0.xml | 18 - ...rface_Adminhtml_Default-1.7.0.0-alpha1.xml | 18 + ...nterface_Frontend_Base_Default-1.6.1.0.xml | 18 - ...e_Frontend_Base_Default-1.7.0.0-alpha1.xml | 18 + ...rface_Frontend_Default-1.7.0.0-alpha1.xml} | 14 +- ...erface_Install_Default-1.7.0.0-alpha1.xml} | 14 +- var/package/Lib_Google_Checkout-1.5.0.0.xml | 4 +- var/package/Lib_Js_Calendar-1.51.1.xml | 4 +- var/package/Lib_Js_Ext-1.6.0.0.xml | 18 - var/package/Lib_Js_Ext-1.7.0.0-alpha1.xml | 18 + ...1.0.xml => Lib_Js_Mage-1.7.0.0-alpha1.xml} | 14 +- ... => Lib_Js_Prototype-1.7.0.0.3-alpha1.xml} | 12 +- var/package/Lib_Js_TinyMCE-3.3.7.0.xml | 18 - var/package/Lib_Js_TinyMCE-3.4.7.0-alpha1.xml | 18 + ... Lib_LinLibertineFont-2.8.14.1-alpha1.xml} | 12 +- var/package/Lib_Mage-1.6.1.0.xml | 18 - var/package/Lib_Mage-1.7.0.0-alpha1.xml | 18 + var/package/Lib_Phpseclib-1.5.0.0.xml | 4 +- ....1.0.xml => Lib_Varien-1.7.0.0-alpha1.xml} | 12 +- var/package/Lib_ZF-1.11.1.0.xml | 4 +- var/package/Lib_ZF_Locale-1.11.1.0.xml | 4 +- var/package/Mage_All_Latest-1.6.1.0.xml | 18 - .../Mage_All_Latest-1.7.0.0-alpha1.xml | 18 + ...0.xml => Mage_Centinel-1.7.0.0-alpha1.xml} | 14 +- ...0.xml => Mage_Compiler-1.7.0.0-alpha1.xml} | 12 +- var/package/Mage_Core_Adminhtml-1.6.1.0.xml | 18 - .../Mage_Core_Adminhtml-1.7.0.0-alpha1.xml | 18 + var/package/Mage_Core_Modules-1.6.1.0.xml | 18 - .../Mage_Core_Modules-1.7.0.0-alpha1.xml | 18 + var/package/Mage_Downloader-1.6.1.0.xml | 18 - .../Mage_Downloader-1.7.0.0-alpha1.xml | 18 + var/package/Mage_Locale_en_US-1.6.1.0.xml | 18 - .../Mage_Locale_en_US-1.7.0.0-alpha1.xml | 18 + ....0.xml => Magento_Mobile-1.6.0.0.22.1.xml} | 12 +- ... => Phoenix_Moneybookers-1.3.1-alpha1.xml} | 12 +- 1223 files changed, 93395 insertions(+), 39655 deletions(-) create mode 100644 app/code/core/Mage/Adminhtml/Block/Backup/Dialogs.php create mode 100644 app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Price/Group.php create mode 100644 app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Price/Group/Abstract.php create mode 100644 app/code/core/Mage/Adminhtml/Block/Catalog/Product/Helper/Form/Weight.php create mode 100644 app/code/core/Mage/Adminhtml/Block/Customer/Edit/Renderer/Attribute/Group.php create mode 100644 app/code/core/Mage/Adminhtml/Block/Customer/Sales/Order/Address/Form/Billing/Renderer/Vat.php create mode 100644 app/code/core/Mage/Adminhtml/Block/Customer/System/Config/Validatevat.php create mode 100644 app/code/core/Mage/Adminhtml/Block/Promo/Quote/Edit/Tab/Coupons.php create mode 100644 app/code/core/Mage/Adminhtml/Block/Promo/Quote/Edit/Tab/Coupons/Form.php create mode 100644 app/code/core/Mage/Adminhtml/Block/Promo/Quote/Edit/Tab/Coupons/Grid.php create mode 100644 app/code/core/Mage/Adminhtml/Block/Promo/Quote/Edit/Tab/Coupons/Grid/Column/Renderer/Used.php create mode 100644 app/code/core/Mage/Adminhtml/Block/Promo/Quote/Edit/Tab/Main/Renderer/Checkbox.php create mode 100644 app/code/core/Mage/Adminhtml/Block/Report/Grid/Shopcart.php create mode 100644 app/code/core/Mage/Adminhtml/Controller/Report/Abstract.php create mode 100644 app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Customer/GroupAutoAssign.php create mode 100644 app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Filename.php create mode 100644 app/code/core/Mage/Adminhtml/controllers/Customer/System/Config/ValidatevatController.php create mode 100644 app/code/core/Mage/Backup/Model/Config/Backend/Cron.php create mode 100644 app/code/core/Mage/Backup/Model/Config/Source/Type.php create mode 100644 app/code/core/Mage/Backup/Model/Observer.php create mode 100644 app/code/core/Mage/Backup/etc/system.xml create mode 100644 app/code/core/Mage/Bundle/sql/bundle_setup/mysql4-upgrade-1.6.0.0-1.6.0.0.1.php create mode 100644 app/code/core/Mage/Bundle/sql/bundle_setup/upgrade-1.6.0.0-1.6.0.0.1.php create mode 100755 app/code/core/Mage/Captcha/Block/Captcha.php create mode 100755 app/code/core/Mage/Captcha/Block/Captcha/Zend.php create mode 100755 app/code/core/Mage/Captcha/Helper/Data.php create mode 100755 app/code/core/Mage/Captcha/Model/Captcha.php create mode 100755 app/code/core/Mage/Captcha/Model/Config/Font.php create mode 100755 app/code/core/Mage/Captcha/Model/Config/Form/Abstract.php create mode 100755 app/code/core/Mage/Captcha/Model/Config/Form/Backend.php create mode 100755 app/code/core/Mage/Captcha/Model/Config/Form/Frontend.php create mode 100755 app/code/core/Mage/Captcha/Model/Config/Mode.php create mode 100755 app/code/core/Mage/Captcha/Model/Interface.php create mode 100755 app/code/core/Mage/Captcha/Model/Observer.php create mode 100755 app/code/core/Mage/Captcha/Model/Resource/Log.php create mode 100755 app/code/core/Mage/Captcha/Model/Resource/LoginAttempt.php create mode 100755 app/code/core/Mage/Captcha/Model/Resource/LoginAttempt/Collection.php create mode 100755 app/code/core/Mage/Captcha/Model/Zend.php create mode 100755 app/code/core/Mage/Captcha/controllers/Adminhtml/RefreshController.php create mode 100755 app/code/core/Mage/Captcha/controllers/RefreshController.php create mode 100755 app/code/core/Mage/Captcha/etc/config.xml create mode 100755 app/code/core/Mage/Captcha/etc/system.xml create mode 100644 app/code/core/Mage/Captcha/sql/captcha_setup/install-1.7.0.0.0.php create mode 100644 app/code/core/Mage/Catalog/Model/Layer/Filter/Price/Algorithm.php create mode 100644 app/code/core/Mage/Catalog/Model/Product/Attribute/Backend/Groupprice.php create mode 100644 app/code/core/Mage/Catalog/Model/Product/Attribute/Backend/Groupprice/Abstract.php create mode 100644 app/code/core/Mage/Catalog/Model/Resource/Product/Attribute/Backend/Groupprice.php create mode 100644 app/code/core/Mage/Catalog/Model/Resource/Product/Attribute/Backend/Groupprice/Abstract.php create mode 100644 app/code/core/Mage/Catalog/data/catalog_setup/data-upgrade-1.6.0.0.8-1.6.0.0.9.php create mode 100644 app/code/core/Mage/Catalog/sql/catalog_setup/mysql4-upgrade-1.6.0.0.8-1.6.0.0.9.php create mode 100644 app/code/core/Mage/Catalog/sql/catalog_setup/upgrade-1.6.0.0.6-1.6.0.0.7.php create mode 100644 app/code/core/Mage/Catalog/sql/catalog_setup/upgrade-1.6.0.0.7-1.6.0.0.8.php create mode 100644 app/code/core/Mage/Catalog/sql/catalog_setup/upgrade-1.6.0.0.9-1.6.0.0.10.php create mode 100644 app/code/core/Mage/CatalogInventory/Model/System/Config/Backend/Minqty.php create mode 100644 app/code/core/Mage/CatalogInventory/sql/cataloginventory_setup/mysql4-upgrade-1.6.0.0-1.6.0.0.1.php create mode 100644 app/code/core/Mage/Core/Model/Calculator.php create mode 100644 app/code/core/Mage/Customer/Model/Attribute/Backend/Data/Boolean.php create mode 100644 app/code/core/Mage/Customer/sql/customer_setup/upgrade-1.6.1.0-1.6.2.0.php create mode 100644 app/code/core/Mage/Customer/sql/customer_setup/upgrade-1.6.2.0-1.6.2.0.1.php create mode 100644 app/code/core/Mage/Downloadable/sql/downloadable_setup/mysql4-upgrade-1.6.0.0.1-1.6.0.0.2.php create mode 100644 app/code/core/Mage/Downloadable/sql/downloadable_setup/upgrade-1.6.0.0.1-1.6.0.0.2.php create mode 100644 app/code/core/Mage/Index/Block/Adminhtml/Process/Grid/Massaction.php create mode 100644 app/code/core/Mage/Newsletter/data/newsletter_setup/data-upgrade-1.6.0.0-1.6.0.1.php create mode 100644 app/code/core/Mage/Paypal/Block/Adminhtml/System/Config/Payflowlink/Advanced.php create mode 100644 app/code/core/Mage/Paypal/Block/Payflow/Advanced/Form.php create mode 100644 app/code/core/Mage/Paypal/Block/Payflow/Advanced/Iframe.php create mode 100644 app/code/core/Mage/Paypal/Block/Payflow/Advanced/Info.php create mode 100644 app/code/core/Mage/Paypal/Block/Payflow/Advanced/Review.php create mode 100644 app/code/core/Mage/Paypal/Block/Payflow/Link/Review.php create mode 100644 app/code/core/Mage/Paypal/Model/Payflowadvanced.php create mode 100644 app/code/core/Mage/Paypal/controllers/PayflowadvancedController.php create mode 100644 app/code/core/Mage/Paypal/sql/paypal_setup/upgrade-1.6.0.1-1.6.0.2.php create mode 100644 app/code/core/Mage/Reports/Model/Resource/Helper/Interface.php create mode 100644 app/code/core/Mage/Reports/Model/Resource/Report/Collection/Abstract.php create mode 100644 app/code/core/Mage/Reports/Model/Resource/Report/Product/Viewed.php create mode 100644 app/code/core/Mage/Reports/Model/Resource/Report/Product/Viewed/Collection.php create mode 100644 app/code/core/Mage/Reports/sql/reports_setup/upgrade-1.6.0.0-1.6.0.0.1.php create mode 100644 app/code/core/Mage/Sales/Block/Adminhtml/Report/Filter/Form/Coupon.php create mode 100644 app/code/core/Mage/Sales/Model/Resource/Helper/Interface.php create mode 100644 app/code/core/Mage/Sales/data/sales_setup/data-upgrade-1.6.0.4-1.6.0.5.php create mode 100644 app/code/core/Mage/Sales/sql/sales_setup/upgrade-1.6.0.4-1.6.0.5.php create mode 100644 app/code/core/Mage/Sales/sql/sales_setup/upgrade-1.6.0.5-1.6.0.6.php create mode 100644 app/code/core/Mage/Sales/sql/sales_setup/upgrade-1.6.0.6-1.6.0.7.php create mode 100644 app/code/core/Mage/SalesRule/Helper/Coupon.php create mode 100644 app/code/core/Mage/SalesRule/Model/Coupon/Massgenerator.php create mode 100644 app/code/core/Mage/SalesRule/Model/System/Config/Source/Coupon/Format.php create mode 100644 app/code/core/Mage/SalesRule/etc/system.xml create mode 100644 app/code/core/Mage/SalesRule/sql/salesrule_setup/upgrade-1.6.0.1-1.6.0.2.php create mode 100644 app/code/core/Mage/Usa/Block/Adminhtml/Dhl/Unitofmeasure.php create mode 100644 app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International.php create mode 100644 app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Contenttype.php create mode 100644 app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Freemethod.php create mode 100644 app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method.php create mode 100644 app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Abstract.php create mode 100644 app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/All.php create mode 100644 app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Doc.php create mode 100644 app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Freedoc.php create mode 100644 app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Freenondoc.php create mode 100644 app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Nondoc.php create mode 100644 app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Size.php create mode 100644 app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Unitofmeasure.php create mode 100644 app/code/core/Mage/Usa/etc/dhl/international/countries.xml create mode 100644 app/code/core/Zend/Date.php create mode 100755 app/design/adminhtml/default/default/layout/captcha.xml create mode 100644 app/design/adminhtml/default/default/template/backup/dialogs.phtml create mode 100644 app/design/adminhtml/default/default/template/captcha/zend.phtml create mode 100644 app/design/adminhtml/default/default/template/catalog/product/edit/price/group.phtml create mode 100644 app/design/adminhtml/default/default/template/customer/edit/tab/account/form/renderer/group.phtml create mode 100644 app/design/adminhtml/default/default/template/customer/sales/order/create/billing/form/renderer/vat.phtml create mode 100644 app/design/adminhtml/default/default/template/customer/system/config/validatevat.phtml create mode 100644 app/design/adminhtml/default/default/template/paypal/system/config/payflowlink/advanced.phtml create mode 100644 app/design/adminhtml/default/default/template/promo/salesrulejs.phtml create mode 100644 app/design/adminhtml/default/default/template/usa/dhl/unitofmeasure.phtml create mode 100755 app/design/frontend/base/default/layout/captcha.xml create mode 100644 app/design/frontend/base/default/template/captcha/zend.phtml create mode 100644 app/design/frontend/base/default/template/googleanalytics/ga.phtml create mode 100644 app/design/frontend/base/default/template/paypal/payflowadvanced/form.phtml create mode 100644 app/design/frontend/base/default/template/paypal/payflowadvanced/iframe.phtml rename skin/frontend/default/iphone/js/search.js => app/design/frontend/base/default/template/paypal/payflowadvanced/info.phtml (73%) create mode 100644 app/design/frontend/base/default/template/paypal/payflowadvanced/redirect.phtml create mode 100644 app/design/frontend/default/iphone/layout/catalogsearch.xml create mode 100644 app/design/frontend/default/iphone/layout/contacts.xml create mode 100644 app/design/frontend/default/iphone/layout/sendfriend.xml create mode 100644 app/design/frontend/default/iphone/template/bundle/sales/order/items/renderer.phtml create mode 100644 app/design/frontend/default/iphone/template/catalog/layer/view.phtml create mode 100644 app/design/frontend/default/iphone/template/catalog/navigation/top.phtml create mode 100644 app/design/frontend/default/iphone/template/catalog/product/compare/list.phtml create mode 100644 app/design/frontend/default/iphone/template/catalog/product/compare/sidebar.phtml create mode 100644 app/design/frontend/default/iphone/template/catalog/product/gallery.phtml create mode 100644 app/design/frontend/default/iphone/template/catalog/product/list.phtml create mode 100644 app/design/frontend/default/iphone/template/catalog/product/list/upsell.phtml create mode 100644 app/design/frontend/default/iphone/template/catalog/product/price.phtml create mode 100644 app/design/frontend/default/iphone/template/catalog/product/view.phtml create mode 100644 app/design/frontend/default/iphone/template/catalog/product/view/addto.phtml create mode 100644 app/design/frontend/default/iphone/template/catalog/product/view/addtocart.phtml create mode 100644 app/design/frontend/default/iphone/template/catalog/product/view/media.phtml create mode 100644 app/design/frontend/default/iphone/template/catalog/product/view/type/grouped.phtml create mode 100644 app/design/frontend/default/iphone/template/catalog/product/view/type/grouped_grid.phtml create mode 100644 app/design/frontend/default/iphone/template/catalogsearch/result.phtml rename app/design/frontend/default/iphone/template/{page/one-column.phtml => checkout/cart/noItemsHeader.phtml} (59%) create mode 100644 app/design/frontend/default/iphone/template/checkout/cart/sidebar.phtml create mode 100644 app/design/frontend/default/iphone/template/checkout/cartheader.phtml create mode 100644 app/design/frontend/default/iphone/template/checkout/onepage/login.phtml create mode 100644 app/design/frontend/default/iphone/template/customer/form/edit.phtml create mode 100644 app/design/frontend/default/iphone/template/customer/form/register.phtml create mode 100644 app/design/frontend/default/iphone/template/page/html/pager.phtml create mode 100644 app/design/frontend/default/iphone/template/page/switch/languages.phtml create mode 100644 app/design/frontend/default/iphone/template/page/switch/stores.phtml create mode 100644 app/design/frontend/default/iphone/template/persistent/checkout/onepage/login.phtml create mode 100644 app/design/frontend/default/iphone/template/persistent/customer/form/register.phtml create mode 100644 app/design/frontend/default/iphone/template/sales/order/items.phtml create mode 100644 app/design/frontend/default/iphone/template/sales/order/totals.phtml create mode 100644 app/design/frontend/default/iphone/template/sendfriend/send.phtml create mode 100644 app/design/frontend/default/iphone/template/tag/list.phtml create mode 100755 app/etc/modules/Mage_Captcha.xml create mode 100644 app/locale/en_US/Mage_Captcha.csv create mode 100755 downloader/lib/Mage/Archive/Helper/File.php create mode 100755 downloader/lib/Mage/Archive/Helper/File/Bz.php create mode 100755 downloader/lib/Mage/Archive/Helper/File/Gz.php create mode 100755 downloader/lib/Mage/Backup.php create mode 100755 downloader/lib/Mage/Backup/Abstract.php create mode 100644 downloader/lib/Mage/Backup/Archive/Tar.php create mode 100755 downloader/lib/Mage/Backup/Db.php create mode 100755 downloader/lib/Mage/Backup/Exception.php create mode 100755 downloader/lib/Mage/Backup/Exception/CantLoadSnapshot.php create mode 100755 downloader/lib/Mage/Backup/Exception/FtpConnectionFailed.php create mode 100755 downloader/lib/Mage/Backup/Exception/FtpValidationFailed.php create mode 100755 downloader/lib/Mage/Backup/Exception/NotEnoughFreeSpace.php create mode 100755 downloader/lib/Mage/Backup/Exception/NotEnoughPermissions.php create mode 100755 downloader/lib/Mage/Backup/Filesystem.php create mode 100755 downloader/lib/Mage/Backup/Filesystem/Helper.php create mode 100755 downloader/lib/Mage/Backup/Filesystem/Iterator/Filter.php create mode 100755 downloader/lib/Mage/Backup/Filesystem/Rollback/Abstract.php create mode 100755 downloader/lib/Mage/Backup/Filesystem/Rollback/Fs.php create mode 100755 downloader/lib/Mage/Backup/Filesystem/Rollback/Ftp.php create mode 100755 downloader/lib/Mage/Backup/Interface.php create mode 100644 downloader/lib/Mage/Backup/Media.php create mode 100755 downloader/lib/Mage/Backup/Snapshot.php create mode 100755 downloader/lib/Mage/System/Ftp.php create mode 100644 js/extjs/fix-defer-before.js create mode 100644 js/mage/adminhtml/backup.js create mode 100644 js/mage/captcha.js delete mode 100644 js/tiny_mce/classes/CommandManager.js delete mode 100644 js/tiny_mce/classes/Developer.js delete mode 100644 js/tiny_mce/classes/dom/Schema.js delete mode 100644 js/tiny_mce/classes/dom/StringWriter.js delete mode 100644 js/tiny_mce/classes/dom/XMLWriter.js create mode 100644 js/tiny_mce/classes/firebug/FIREBUG.LICENSE create mode 100644 js/tiny_mce/classes/html/DomParser.js create mode 100644 js/tiny_mce/classes/html/Entities.js create mode 100644 js/tiny_mce/classes/html/Node.js create mode 100644 js/tiny_mce/classes/html/SaxParser.js create mode 100644 js/tiny_mce/classes/html/Schema.js create mode 100644 js/tiny_mce/classes/html/Serializer.js create mode 100644 js/tiny_mce/classes/html/Styles.js create mode 100644 js/tiny_mce/classes/html/Writer.js create mode 100644 js/tiny_mce/classes/ui/KeyboardNavigation.js create mode 100644 js/tiny_mce/classes/ui/ToolbarGroup.js create mode 100644 js/tiny_mce/classes/util/Quirks.js create mode 100644 js/tiny_mce/classes/util/VK.js create mode 100644 js/tiny_mce/plugins/autolink/editor_plugin.js create mode 100644 js/tiny_mce/plugins/autolink/editor_plugin_src.js create mode 100644 js/tiny_mce/plugins/example_dependency/editor_plugin.js create mode 100644 js/tiny_mce/plugins/example_dependency/editor_plugin_src.js create mode 100644 js/tiny_mce/plugins/lists/editor_plugin.js create mode 100644 js/tiny_mce/plugins/lists/editor_plugin_src.js delete mode 100644 js/tiny_mce/plugins/media/css/content.css create mode 100644 js/tiny_mce/plugins/media/moxieplayer.swf create mode 100644 js/tiny_mce/themes/advanced/img/flash.gif create mode 100644 js/tiny_mce/themes/advanced/img/iframe.gif create mode 100644 js/tiny_mce/themes/advanced/img/pagebreak.gif create mode 100644 js/tiny_mce/themes/advanced/img/quicktime.gif create mode 100644 js/tiny_mce/themes/advanced/img/realmedia.gif create mode 100644 js/tiny_mce/themes/advanced/img/shockwave.gif create mode 100644 js/tiny_mce/themes/advanced/img/trans.gif create mode 100644 js/tiny_mce/themes/advanced/img/video.gif create mode 100644 js/tiny_mce/themes/advanced/img/windowsmedia.gif create mode 100644 js/tiny_mce/themes/advanced/shortcuts.htm create mode 100644 js/tiny_mce/themes/advanced/skins/highcontrast/content.css create mode 100644 js/tiny_mce/themes/advanced/skins/highcontrast/dialog.css create mode 100644 js/tiny_mce/themes/advanced/skins/highcontrast/ui.css create mode 100644 lib/LinLibertineFont/LinLibertine_Re-4.4.1.ttf create mode 100644 lib/Mage/Archive/Helper/File.php create mode 100644 lib/Mage/Archive/Helper/File/Bz.php create mode 100644 lib/Mage/Archive/Helper/File/Gz.php create mode 100644 lib/Mage/Backup.php create mode 100644 lib/Mage/Backup/Abstract.php create mode 100644 lib/Mage/Backup/Archive/Tar.php create mode 100644 lib/Mage/Backup/Db.php create mode 100644 lib/Mage/Backup/Exception.php create mode 100644 lib/Mage/Backup/Exception/CantLoadSnapshot.php create mode 100644 lib/Mage/Backup/Exception/FtpConnectionFailed.php create mode 100644 lib/Mage/Backup/Exception/FtpValidationFailed.php create mode 100644 lib/Mage/Backup/Exception/NotEnoughFreeSpace.php create mode 100644 lib/Mage/Backup/Exception/NotEnoughPermissions.php create mode 100644 lib/Mage/Backup/Filesystem.php create mode 100644 lib/Mage/Backup/Filesystem/Helper.php create mode 100644 lib/Mage/Backup/Filesystem/Iterator/Filter.php create mode 100644 lib/Mage/Backup/Filesystem/Rollback/Abstract.php create mode 100644 lib/Mage/Backup/Filesystem/Rollback/Fs.php create mode 100644 lib/Mage/Backup/Filesystem/Rollback/Ftp.php create mode 100644 lib/Mage/Backup/Interface.php create mode 100644 lib/Mage/Backup/Media.php create mode 100644 lib/Mage/Backup/Snapshot.php create mode 100644 lib/Mage/System/Ftp.php create mode 100644 skin/adminhtml/default/default/images/btn_gr_bg.gif create mode 100644 skin/adminhtml/default/default/images/btn_gr_on.gif create mode 100644 skin/adminhtml/default/default/images/btn_gr_over.gif create mode 100644 skin/adminhtml/default/default/images/reload.png create mode 100644 skin/frontend/base/default/images/reload.png create mode 100644 skin/frontend/default/iphone/images/bg_breadcrumb.png create mode 100644 skin/frontend/default/iphone/images/bg_breadcrumb_active.png create mode 100644 skin/frontend/default/iphone/images/bg_checkbox.png create mode 100644 skin/frontend/default/iphone/images/bg_checkout_step_passed.png create mode 100644 skin/frontend/default/iphone/images/bg_drop.jpg create mode 100644 skin/frontend/default/iphone/images/bg_nav_brd.png create mode 100644 skin/frontend/default/iphone/images/bg_nav_brd_inactive.png create mode 100644 skin/frontend/default/iphone/images/bg_qty.png create mode 100644 skin/frontend/default/iphone/images/bg_rating_star.png delete mode 100644 skin/frontend/default/iphone/images/bkg_rating.gif delete mode 100644 skin/frontend/default/iphone/images/btn_google_checkout.gif delete mode 100644 skin/frontend/default/iphone/images/btn_paypal_checkout.gif delete mode 100644 skin/frontend/default/iphone/images/btn_place_order.gif delete mode 100644 skin/frontend/default/iphone/images/btn_proceed_to_checkout.gif delete mode 100644 skin/frontend/default/iphone/images/btn_proceed_to_checkout.png delete mode 100644 skin/frontend/default/iphone/images/btn_proceed_to_checkout_bg.gif delete mode 100644 skin/frontend/default/iphone/images/btn_proceed_to_checkout_rad.gif create mode 100644 skin/frontend/default/iphone/images/btn_trash.png delete mode 100644 skin/frontend/default/iphone/images/btn_window_close.gif delete mode 100644 skin/frontend/default/iphone/images/chart_remove.gif delete mode 100644 skin/frontend/default/iphone/images/error_msg_icon.gif create mode 100644 skin/frontend/default/iphone/images/fabric.jpg create mode 100644 skin/frontend/default/iphone/images/flip.png delete mode 100644 skin/frontend/default/iphone/images/footer-bg.gif delete mode 100644 skin/frontend/default/iphone/images/free_shipping_callout.jpg create mode 100644 skin/frontend/default/iphone/images/front_banner.png delete mode 100644 skin/frontend/default/iphone/images/gift-message-close.gif create mode 100644 skin/frontend/default/iphone/images/grid.png delete mode 100644 skin/frontend/default/iphone/images/header-bg.gif create mode 100644 skin/frontend/default/iphone/images/i_add_to_cart.png create mode 100644 skin/frontend/default/iphone/images/i_add_to_wishlist.png create mode 100644 skin/frontend/default/iphone/images/i_arrow.png create mode 100644 skin/frontend/default/iphone/images/i_arrow_small.png create mode 100644 skin/frontend/default/iphone/images/i_arrow_white.png create mode 100644 skin/frontend/default/iphone/images/i_dropdown.png create mode 100644 skin/frontend/default/iphone/images/i_gallery.png create mode 100644 skin/frontend/default/iphone/images/i_menu_arrow.png create mode 100644 skin/frontend/default/iphone/images/i_menu_cart.png create mode 100644 skin/frontend/default/iphone/images/i_menu_search.png delete mode 100644 skin/frontend/default/iphone/images/i_notice.gif create mode 100644 skin/frontend/default/iphone/images/i_search.png create mode 100644 skin/frontend/default/iphone/images/i_star.png create mode 100644 skin/frontend/default/iphone/images/i_star_black.png create mode 100644 skin/frontend/default/iphone/images/i_tell_a_friend.png create mode 100644 skin/frontend/default/iphone/images/i_view_details.png delete mode 100644 skin/frontend/default/iphone/images/link_separator.gif create mode 100644 skin/frontend/default/iphone/images/list.png delete mode 100644 skin/frontend/default/iphone/images/list_remove_btn.gif create mode 100644 skin/frontend/default/iphone/images/loader.gif create mode 100644 skin/frontend/default/iphone/images/logo.png delete mode 100644 skin/frontend/default/iphone/images/logo_email.gif delete mode 100644 skin/frontend/default/iphone/images/logo_print.gif delete mode 100644 skin/frontend/default/iphone/images/menu-arrow.gif delete mode 100644 skin/frontend/default/iphone/images/menu-bg.gif create mode 100644 skin/frontend/default/iphone/images/nav_bg.png create mode 100644 skin/frontend/default/iphone/images/nextlabel.png delete mode 100644 skin/frontend/default/iphone/images/np_cart_thumb.gif delete mode 100644 skin/frontend/default/iphone/images/np_more_img.gif delete mode 100644 skin/frontend/default/iphone/images/np_product_main.gif delete mode 100644 skin/frontend/default/iphone/images/np_thumb.gif delete mode 100644 skin/frontend/default/iphone/images/np_thumb2.gif delete mode 100644 skin/frontend/default/iphone/images/opc_off_head_bg.gif delete mode 100644 skin/frontend/default/iphone/images/opc_on_box_bg.gif delete mode 100644 skin/frontend/default/iphone/images/page-bg.gif delete mode 100644 skin/frontend/default/iphone/images/pager_arrow_left.gif delete mode 100644 skin/frontend/default/iphone/images/pager_arrow_right.gif delete mode 100644 skin/frontend/default/iphone/images/pager_bg.gif delete mode 100644 skin/frontend/default/iphone/images/ph.gif delete mode 100644 skin/frontend/default/iphone/images/place_order_container_bg.gif create mode 100644 skin/frontend/default/iphone/images/prevlabel.png delete mode 100644 skin/frontend/default/iphone/images/product_rating_blank_star.gif delete mode 100644 skin/frontend/default/iphone/images/product_rating_full_star.gif delete mode 100644 skin/frontend/default/iphone/images/search-bg.gif delete mode 100755 skin/frontend/default/iphone/images/search-close-but.gif delete mode 100644 skin/frontend/default/iphone/images/search-end.gif delete mode 100755 skin/frontend/default/iphone/images/search-go-but.gif delete mode 100644 skin/frontend/default/iphone/images/search-md.gif delete mode 100644 skin/frontend/default/iphone/images/search-st.gif delete mode 100644 skin/frontend/default/iphone/images/search_criteria.gif create mode 100644 skin/frontend/default/iphone/images/search_icon.png delete mode 100644 skin/frontend/default/iphone/images/shipping_method_pointer.gif delete mode 100644 skin/frontend/default/iphone/images/sort_asc_arrow.gif delete mode 100644 skin/frontend/default/iphone/images/sort_desc_arrow.gif delete mode 100644 skin/frontend/default/iphone/images/success_msg_icon.gif delete mode 100644 skin/frontend/default/iphone/images/validation_advice_bg.gif create mode 100644 skin/frontend/default/iphone/js/dnd.js create mode 100644 skin/frontend/default/iphone/js/iphone.js delete mode 100644 skin/frontend/default/iphone/js/opcheckout.js delete mode 100644 var/package/Interface_Adminhtml_Default-1.6.1.0.xml create mode 100644 var/package/Interface_Adminhtml_Default-1.7.0.0-alpha1.xml delete mode 100644 var/package/Interface_Frontend_Base_Default-1.6.1.0.xml create mode 100644 var/package/Interface_Frontend_Base_Default-1.7.0.0-alpha1.xml rename var/package/{Interface_Frontend_Default-1.6.0.0.xml => Interface_Frontend_Default-1.7.0.0-alpha1.xml} (97%) rename var/package/{Interface_Install_Default-1.6.1.0.xml => Interface_Install_Default-1.7.0.0-alpha1.xml} (91%) delete mode 100644 var/package/Lib_Js_Ext-1.6.0.0.xml create mode 100644 var/package/Lib_Js_Ext-1.7.0.0-alpha1.xml rename var/package/{Lib_Js_Mage-1.6.1.0.xml => Lib_Js_Mage-1.7.0.0-alpha1.xml} (58%) rename var/package/{Lib_Js_Prototype-1.7.0.0.2.xml => Lib_Js_Prototype-1.7.0.0.3-alpha1.xml} (98%) delete mode 100644 var/package/Lib_Js_TinyMCE-3.3.7.0.xml create mode 100644 var/package/Lib_Js_TinyMCE-3.4.7.0-alpha1.xml rename var/package/{Lib_LinLibertineFont-2.8.14.0.xml => Lib_LinLibertineFont-2.8.14.1-alpha1.xml} (68%) delete mode 100644 var/package/Lib_Mage-1.6.1.0.xml create mode 100644 var/package/Lib_Mage-1.7.0.0-alpha1.xml rename var/package/{Lib_Varien-1.6.1.0.xml => Lib_Varien-1.7.0.0-alpha1.xml} (92%) delete mode 100644 var/package/Mage_All_Latest-1.6.1.0.xml create mode 100644 var/package/Mage_All_Latest-1.7.0.0-alpha1.xml rename var/package/{Mage_Centinel-1.6.1.0.xml => Mage_Centinel-1.7.0.0-alpha1.xml} (92%) rename var/package/{Mage_Compiler-1.6.0.0.xml => Mage_Compiler-1.7.0.0-alpha1.xml} (86%) delete mode 100644 var/package/Mage_Core_Adminhtml-1.6.1.0.xml create mode 100644 var/package/Mage_Core_Adminhtml-1.7.0.0-alpha1.xml delete mode 100644 var/package/Mage_Core_Modules-1.6.1.0.xml create mode 100644 var/package/Mage_Core_Modules-1.7.0.0-alpha1.xml delete mode 100644 var/package/Mage_Downloader-1.6.1.0.xml create mode 100644 var/package/Mage_Downloader-1.7.0.0-alpha1.xml delete mode 100644 var/package/Mage_Locale_en_US-1.6.1.0.xml create mode 100644 var/package/Mage_Locale_en_US-1.7.0.0-alpha1.xml rename var/package/{Magento_Mobile-1.6.0.0.22.0.xml => Magento_Mobile-1.6.0.0.22.1.xml} (89%) rename var/package/{Phoenix_Moneybookers-1.3.0.xml => Phoenix_Moneybookers-1.3.1-alpha1.xml} (97%) diff --git a/RELEASE_NOTES.txt b/RELEASE_NOTES.txt index b09f515d9a..14aaee5514 100644 --- a/RELEASE_NOTES.txt +++ b/RELEASE_NOTES.txt @@ -1,3 +1,630 @@ +==== 1.7.0.0-alpha1 ==== + +=== Major Highlights === +New Layered Navigation price bucket algorithm +Added captcha functionality +Implemented different base prices for customer groups +Added auto generation of coupon codes +Backup and Rollback functionality +VAT ID Validation added +Implemented DHL for Europe + +=== Improvements === +XmlConnect package release v22.1 +Upgraded TinyMCE to v3.4.7 +Indexers refactoring +Theme for iPhone was redesigned + +=== Changes === +Implemented localized PayPal settings for Japan +Added PayPal Advanced payment method + +=== Fixes === +Fixed Incorrect translation messages definitions +Fixed Error message isn't displayed if currency exchange rate not found (in case with DHL Int) +Fixed Handling Fee doesn't applied Per Package +Fixed User role with partial access can't edit attributes of configurable products +Fixed Letter "n" missed in the word "handling" +Fixed Buttons don't react for pressing on the "Widget Options" section in Insert Widget window +Fixed Regular price displays incorrect +Fixed {{base_url}} in (un)secure_url doesn't work since 1.6.1 +Fixed Product selection field gets cleared out with recently added products from latest page +Fixed "Change" button while checkout doesn't work +Fixed MySQL BIGINT field type is wrongly casted to integer +Fixed Magento Connect Manager proceed with installation of extension if error appears on database backup +Fixed "Set product as New from/to Date" works excluding selected dates +Fixed Function fireEvent from lib/varien/js.js does not work in IE9 +Fixed Searching with first and last name has no results +Fixed CMS Pages: Error in IE7 when select CMS page +Fixed White screen appears instead of 404 Error Page when going to review a product which doesn't exist +Fixed "Maximum Package Weight" option works incorrect in case with decimal Qty in shopping Cart +Fixed Unable to create tables for new EAV entity via SQL upgrade script +Fixed Customer group isn't revalidated on checkout if Enable Automatic Assignment to Customer Group = Yes +Fixed Mistake in PayPal Payments Advanced fieldset title +Fixed Zero Subtotal Checkout payment method is used, when it is disabled in settings +Fixed Some info lost from address when customer sets this address as default for shipping +Fixed Incorrect calculation logic during distribution products between several pieces (in case with DHL) +Fixed No ability to get shipping rates from US to another country (in case with DHL) +Fixed Incorrect calculation of pieces weight (in case with Bundle Product) +Fixed Product Flat Data index causes replication lag on MySQL master/slave model +Fixed Exception is shown, when admin user provides filtration of Newsletter problem reports by Subscriber +Fixed Typo in JavaScript error message +Fixed Unable to do mass action update for products +Fixed Error Message isn't displayed if currency exchange rate not found (in case with DHL) +Fixed Weight field is absent during Quick simple product creation +Fixed Fixed correct helper resolving +Fixed Shipping methods are shown twice in DHLs shipping quote +Fixed Unable to translate shipping and billing forms on the order creation page +Fixed Drop-down attribute with layered navigation filter doesn't work with value is set to 0 +Fixed Free Shipping options doesn't work (in case with DHL) +Fixed Handling Fee doesn't applied Per Package +Fixed Free Shipping options works incorrect +Fixed WYSIWYG Editor: It's impossible insert Widget to CMS page content +Fixed Customer's group is not changed if his billing address modified within backend +Fixed Wrong behaviour and exception while using invalid image +Fixed Layered navigation for prices displays incorrect price ranges in manual mode +Fixed Uninformative error message during saving two nodes with the same parameter URL Key +Fixed Unable to change order addresses in the admin panel +Fixed PayPal Express always uses default billing address from customer account +Fixed Unable to place order if customer selects Register on checkout +Fixed Performance Issue: Most Viewed product reports on large amount of items +Fixed In "Customer Addresses" block before and after of State name is located symbols "," +Fixed Lightbox 2.5 with IE7 returns JS error on the page +Fixed Unable to change customer status (confirmed/not confirmed) when customer logged in +Fixed Incorrect notification for empty field during creation catalog price rules +Fixed Unable to save product with non-checked multiple select attribute +Fixed "Ship Bundle Items" for bundle product works incorrect +Fixed Package Size setting for DHL +Fixed No Input Validation for Catalog Fields +Fixed Row Total Calculation in Refund +Fixed "Maximum number of price intervals" should be written with capital letters +Fixed Divide Order Weight options for DHL +Fixed Incorrect Row Total Calculation in Refund +Fixed Impossible to create new customer in the backend +Fixed Catalog data-upgrade-1.6.0.0.4-1.6.0.0.5.php is exteremly slow +Fixed Free Shipping options doesn't work (in case with DHL International) +Fixed Mage_Catalog_Helper_Product::getProduct() doesn't load product by its SKU +Fixed Preview Template doesn't work correctly +Fixed Some options of Bundle Product disappeared from the Invoice PDF +Fixed "Allow Countries" affects on "Country of Manufacture" attribute +Fixed Some phrases are not translated +Fixed Incorrect Ordered Qty in Order (in case with decimal qty) +Fixed Trademark character isn't being displayed properly in the PDF invoice +Fixed Can't search transactions by order_id in manager.paypal.com +Fixed Inline Translation: Grid headers are displayed incorrect on the Tag Edit page +Fixed "Create Extension Package" page becomes broken after compilation +Fixed Price including tax isn't displayed for some kinds of bundle products +Fixed Layered navigation for prices displays incorrect price ranges in manual mode +Fixed Pager works wrong with float page number +Fixed Incorrect foreign key for EAV entity tables +Fixed Misprint in downloader/lib/Mage/Connect/Command/Install.php +Fixed URL Rewrites must be case-sensitive +Fixed Unable to install package via uploader if author name contains dash +Fixed Fixed invoice subtotals for cases with partial invoice and discount +Fixed Catalog URL Rewrites works incorrectly on creating categories +Fixed New Oder Status setting, specified for payment method works incorrectly when only virtual products are present in Order +Fixed Rounding issue in catalog and product view if price includes tax +Fixed Wrong status of catalog event is displayed by editing catalog event +Fixed Role Resources are not saved +Fixed "Qty for Item's Status to Become Out of Stock" option works incorrect +Fixed XML parser works incorrect +Fixed Mage_Reports_Model_Mysql4_Product_Index_Abstract must be declared abstract +Fixed "Date & Time" and "Time" custom options becomes required when editing product +Fixed Unable to cancel an order for an expired Authorize.net auth +Fixed Custom options are not stored when downloadable product is duplicated +Fixed "Cannot initialize the indexer process" error during Mass "Reindex Data" Action +Fixed Search by new attribute fails, attribute is not shown in layered navigation +Fixed Exception when "Price Navigation Step Calculation" set to "Manual" mode and FPT enabled +Fixed WPPHS: Cancel URL doesn't work as should be +Fixed Error about insufficient permissions is not appears on database backup creation +Fixed After rollback admin doesn't redirected to the Log in to Admin Panel page +Fixed Database Backup must not include indexer table data +Fixed Scheduled Backup creation/failure isn't logged +Fixed Deleting backup while it is used by another process +Fixed Opening *.tar files causes an error "There are no trailing zero-filled records" +Fixed Unable to search by "Time" and "No" in Backups table +Fixed Backups are deleted without confirmation +Fixed Reports must be excluded from database snapshot backup +Fixed There are no products in filtering results, if step calculation in automatic mode +Fixed No validation for "Default Price Navigation Step" +Fixed "Back" button doesn't work on the Create New Order page +Fixed Incorrect logic of Token expiration in Two Step Password Reset flow +Fixed Tag
is present in tool-tip for field "Number of results (For the last time placed)" on the Edit Search page +Fixed Unnecessary hard code in Magento Extension +Fixed Wrong logic in Mage_Core_Model_Resource_Db_Collection_Abstract::join() +Fixed Description and Short Description are displayed incorrectly for products added with WYSIWYG +Fixed Adding product to the cart from the product review page leads to 404 page +Fixed Special symbols in Sort Order field +Fixed Text is wrong displayed with enabled Inline translation for Admin on back-end +Fixed Inline Translation: Unable to translate some customer information +Fixed Useless colon on front-end login page +Fixed Unable to continue checkout for product with zero price and non-zero shipping price +Fixed Import news_from_date field is configured poorly. It is not accepting the data from file +Fixed When updating product data through import, attributes that have a value cannot be assigned a new value that is empty +Fixed Unable to replace non-complex data for products with empty values during import +Fixed "Wrong order ID" exception in PayPal Express module under heavy load +Fixed Tax is applying on the order when creating it in the admin panel for a Customer Group with no taxes +Fixed Issue with retrieving order statuses for array of states +Fixed Wrong calculation product price with custom option type = Field and Fixed price +Fixed Back ordered downloadable product is not available even when it is set to be be accessible when order status is Pending +Fixed Missing column "position" at table catalog_category_anc_products_index_tmp +Fixed Incorrect behavior of "Save in address book" option during admin Order creation for a new customer +- refactored Mage_Adminhtml_Model_Sales_Order_Create::_prepareCustomer() +Fixed Terms and Conditions is named differently on different pages +Fixed "Apply" and "Discount Amount" fields appear twice in the Catalog Price Rule +Fixed Poll shows incorrect percentage +Fixed Added validation ability for admin configuration forms +Fixed UPS XML misprint +Fixed Misprint in uploading files form +Fixed Unnecessary check boxes for gift options +Fixed Wrong resource section declaration in Mage_Tag module +Fixed "Customers Submitted this Tag" section doesn't update when customer has deleted tag from his account +Fixed Correct product in category position +Fixed Unable to create folder in Media Storage +Fixed Translation with single quotes breaks JavaScript +Fixed Out of memory error with hundreds of thousands of coupons attached to a single sales rule +Fixed Unable to translate buttons and tabs on the "Manage Category" page +Fixed Product Categories Tree doesn't expand in Manage Products page +Fixed Incorrect products qty returns to stock after refund for configurable product +Fixed Swf Uploader problems with cross domain Flash Player Policy +Fixed Unable to translate "Layout Updates" block on create/edit widget page +Fixed IE7: "Remember Me" checkbox visible on billing information step +Fixed CMS WYSIWYG Editor - widget is inserted as new while editing in IE8 +Fixed Currency code doesn't correspond to the amount in reports +Fixed Mage_Adminhtml_Block_Sales_Order_View_Tab_History::getFullHistory() doesn't use unique keys for each message +Fixed Scope labels are shown without translate wrapper +Fixed Wrong parameters handling in Core Helper formatDate() +Fixed Apostrophe in store name breaks Google Analytics tracking +Fixed Customer attribute prefix is not shown on frontend +Fixed Incorrect rounding for product with custom options (percent price) +Fixed Invoicing only part of products results in wrong totals calculation +Fixed Incorrect Row Total Calculation in Refund +- fixed rounding issues for partial Invoice and Refund +- refactored delta rounding +Fixed Filter by "Allow Countries" not working for Customer Address Form in the Backend +- checking added for set country to be in available list +Fixed There are sql-installs with empty string used as defaults for table columns, while column is not null able +Fixed Unable to translate "Note" in "Product Stock Options" +Fixed Various warning are displayed after creating shipment for 0 items +Fixed Invalid content in Content-header in the top of page during scrolling during order creation +Fixed "Online invoice" button present in backend when using Zero Subtotal Checkout +Fixed String cast type in in_array function +Fixed Newsletter template content should not disappear when "Show" / "Hide Editor" button was clicked +Fixed Import feature doesn't validate whether super_products_sku is existing or not +Fixed Cannot place order with downloadable product and discount code using Paypal Express payment method +Fixed Product still out of stock after Stock Status reindex +Fixed No ability to reindex Catalog URL Rewrites, error is shown +Fixed Automatic reindexing based on matched events doesn't change "Status" and "Last Run" columns at process list grid +Fixed Save catalog price rule gives trace if full reindex has already started +Fixed Reindex requires notification is not shown for Stock Status when stock is updated for several products using Mass Action +Fixed Incorrect FedEx's shipping rates (in case with non-US country origin) +Fixed After partial reindex MSRP value is not applied (not viewed) in catalog during mass update action +Fixed Wrong stock reindex on catalog if partial reindex done after full reindex started +Fixed In error message string "already exists." written twice, when trying to save Product Tax Class with the same name +Fixed Slow checkout with non-flushed cache +Fixed Bundle product total is incorrect in Customization block +Fixed Special price isn't considered for bundle dynamic products in "Your Customization" block +Fixed Situation when any amount of duplicate reindex process can be running at one time +Fixed Error with Advanced Search (in case with Date Attribute) +Fixed Product Flat Data index isn't marked as "Reindex Required" after importing products when Flat Catalog is enabled +Fixed User can't change root category for the store +Fixed JS error causes the overwrite of Title field in PayPal Advanced configuration +Fixed Mass action doesn't partially reindex catalog for product name/description +Fixed Remove hint about $1 auth amount from informational message text (PayflowLink configuration) +Fixed Mass action doesn't partially reindex catalog for product prices +Fixed PayPal Payments Advanced works with $0 Auth instead of Full Auth +Fixed Impossible to place Order using "Pay with PayPal" button from PayPal Payments Advanced iframe +Fixed Mass action doesn't partially reindex catalog search for product if searchable attribute was updated +Fixed "Please wait" AJAX screen doesn't appear in the middle of the page +Fixed Checkout link on frontend is always referenced as http +Fixed GET request is incorrectly formed during category creation +- adjusted assertion to determine last viewed store +Fixed Display of Tier Pricing with Configurable Products +- added functionality to dynamically update tier prices accordingly to chosen product configuration +Fixed Google Analytics e-commerce tracking not working +Fixed URL key isn't used while product save +Fixed Added validation class to 'Qty increments' +Fixed Entered from admin customer date 1970 (or less) is saved as 2070 (or less) +Fixed cUrl resource must be closed after checking it for errors, not before +Fixed Exception while products mass update attributes in backend +Fixed No ability to reindex Catalog URL Rewrites, error is shown +Fixed Package with Core dependency +Fixed Stock Availability isn't updated if 1: Run Price Reindex 2: Update Stock Availability on product with mass action/single product +Fixed Blank page instead shopping cart page when compilation and PSC are enabled +Fixed Unable to save redirect URL with special characters in search terms +Fixed Attribute Set field should have client-side validation and appropriate information message +Fixed "Localhost" isn't a valid domain name for installation +Fixed Iframe for PayPal Payments Advanced is not loaded +Fixed Retain the selected tab on editing CMS page +Fixed Payflow Link Express Checkout (pay with PayPal button) payment +Fixed Wrong number of reindex options is displayed +Fixed Wrong phpDocs for Varien_Db_Select +Fixed JavaScript calendar date range +- fixed JS calendar behavior to use 4-digits year format +Fixed Performance issue connected with sales rules on adding product to cart +Fixed DHL same error message appears several times +Fixed Item Status says "Mixed" when an order has been completed, should say "Shipped" +Fixed Product option title is absent in backend order page +Fixed Incorrect items number during multi shipping checkout +Fixed User name displays differently in the unsuccessful message and in the text field label (log in form) +Fixed If one or more indexers have Update Required = Yes and all Status = READY for all indexers than there is no notification for user to do reindex action +Fixed No space between Address line 1 and line 2 in Shipping Label (in case with FedEx) +Fixed JS works depends on the position attributes of the product +Fixed Char set encoding is out-of-date in Settlement reports +Fixed Settlement reports can't be downloaded if in merchant's account 'Settlement file' is set to 6.0 version +Fixed Unable to login when secure and unsecure urls are different +Fixed Customer session lost when using different domain/subdomain names for secure and unsecure urls +Fixed "Most Viewed" reports ignore Store View switcher +Fixed Long FPT name (and product name) doesn't fit into "My cart" block +Fixed Paypal IPN post back failure +Fixed Customer was unable to receive newsletters when it was created via backend +Fixed Wrong Comments History in notification of order creation/cancellation +Fixed Non escaped string causes javascript error +Fixed Unable to view pictures during product editing +Fixed Ampersand is saved incorrect in attribute label +Fixed IE8: JS error appears after pressing 'Add new rule' in catalog price rules menu +Fixed Exception after sorting newsletter queue +Fixed Customer is not able to log in from URL without "www" in some cases +Fixed SQL error on category view with enabled and configured FPT +Fixed Automatic reindexing based on matched events doesn't change "Status" and "Last Run" columns at process list grid +Fixed Performance issue with Magento Compiler + APC results in too many I/O calls +Fixed Website Administrator is able to change default values +Fixed Some of wishlist blocks and templates still treat the collection of wishlist items as collection of products +Fixed Unnecessary comments in "Share Wishlist" email + + + +==== 1.6.x-devel-139014 ==== + +=== Major Highlights === +Implemented different base prices for customer groups +Added auto generation of coupon codes + +=== Fixes === +Fixed Incorrect translation messages definitions +Fixed Error message isn't displayed if currency exchange rate not found (in case with DHL Int) +Fixed Handling Fee doesn't applied Per Package +Fixed User role with partial access can't edit attributes of configurable products +Fixed Letter "n" missed in the word "handling" +Fixed Buttons don't react for pressing on the "Widget Options" section in Insert Widget window +Fixed Regular price displays incorrect +Fixed {{base_url}} in (un)secure_url doesn't work since 1.6.1 +Fixed Product selection field gets cleared out with recently added products from latest page +Fixed "Change" button while checkout doesn't work +Fixed MySQL BIGINT field type is wrongly casted to integer +Fixed Magento Connect Manager proceed with installation of extension if error appears on database backup +Fixed "Set product as New from/to Date" works excluding selected dates +Fixed Function fireEvent from lib/varien/js.js does not work in IE9 +Fixed Searching with first and last name has no results +Fixed CMS Pages: Error in IE7 when select CMS page +Fixed White screen appears instead of 404 Error Page when going to review a product which doesn't exist +Fixed "Maximum Package Weight" option works incorrect in case with decimal Qty in shopping Cart + + + +==== 1.6.x-devel-138051 ==== +Fixed Unable to create tables for new EAV entity via SQL upgrade script +Fixed Customer group isn't revalidated on checkout if Enable Automatic Assignment to Customer Group = Yes +Fixed VAT ID validation must not run if added address is outside EU +Fixed Mistake in PayPal Payments Advanced fieldset title +Fixed Zero Subtotal Checkout payment method is used, when it is disabled in settings +Fixed Some info lost from address when customer sets this address as default for shipping +Fixed Incorrect calculation logic during distribution products between several pieces (in case with DHL) +Fixed No ability to get shipping rates from US to another country (in case with DHL) +Fixed Incorrect calculation of pieces weight (in case with Bundle Product) +Fixed Divide Order Weight options for new DHL +Fixed Product Flat Data index causes replication lag on MySQL master/slave model +Fixed Exception is shown, when admin user provides filtration of Newsletter problem reports by Subscriber +Fixed Gap in the section background appears on the Login page when CAPTCHA is enabled +Fixed Typo in JavaScript error message +Fixed Unable to do mass action update for products +Fixed Error Message isn't displayed if currency exchange rate not found (in case with DHL) +Fixed Weight field is absent during Quick simple product creation +Fixed Fixed correct helper resolving +Fixed Shipping methods are shown twice in DHLs shipping quote +Fixed Unable to translate shipping and billing forms on the order creation page +Fixed Drop-down attribute with layered navigation filter doesn't work with value is set to 0 +Fixed Free Shipping options doesn't work (in case with DHL) +Fixed Handling Fee doesn't applied Per Package + + + +==== 1.6.x-devel-136760 ==== +Fixed Free Shipping options works incorrect +Fixed Wrong massage appears if VAT ID is invalid +Fixed WYSIWYG Editor: It's impossible insert Widget to CMS page content +Fixed After removing VAT Number, customer continues to be assigned to "Valid VAT ID Domestic" group +Fixed Customer's group is not changed if his billing address modified within backend +Fixed Wrong behaviour and exception while using invalid image +Fixed Layered navigation for prices displays incorrect price ranges in manual mode +Fixed Uninformative error message during saving two nodes with the same parameter URL Key +Fixed Unable to change order addresses in the admin panel +Fixed PayPal Express always uses default billing address from customer account +Fixed Unable to place order if customer selects Register on checkout +Fixed Performance Issue: Most Viewed product reports on large amount of items +Fixed In "Customer Addresses" block before and after of State name is located symbols "," +Fixed Lightbox 2.5 with IE7 returns JS error on the page +Fixed Unable to change customer status (confirmed/not confirmed) when customer logged in +Fixed Incorrect notification for empty field during creation catalog price rules +Fixed Unable to save product with non-checked multiple select attribute +Fixed "Ship Bundle Items" for bundle product works incorrect +Fixed Package Size setting for DHL +Fixed No Input Validation for Catalog Fields +Fixed Row Total Calculation in Refund +Fixed "Maximum number of price intervals" should be written with capital letters +Fixed Divide Order Weight options for DHL + + + +==== 1.6.x-devel-135464 ==== +Fixed Incorrect Row Total Calculation in Refund +Fixed Impossible to create new customer in the backend +Fixed Catalog data-upgrade-1.6.0.0.4-1.6.0.0.5.php is exteremly slow +Fixed Free Shipping options doesn't work (in case with DHL International) +Fixed Mage_Catalog_Helper_Product::getProduct() doesn't load product by its SKU +Fixed Preview Template doesn't work correctly +Fixed Some options of Bundle Product disappeared from the Invoice PDF +Fixed "Allow Countries" affects on "Country of Manufacture" attribute +Fixed Some phrases are not translated +Fixed Incorrect Ordered Qty in Order (in case with decimal qty) +Fixed Trademark character isn't being displayed properly in the PDF invoice +Fixed Can't search transactions by order_id in manager.paypal.com +Fixed Inline Translation: Grid headers are displayed incorrect on the Tag Edit page +Fixed Scheduled Backup creation/failure isn't logged +Fixed "Create Extension Package" page becomes broken after compilation +Fixed Price including tax isn't displayed for some kinds of bundle products +Fixed Layered navigation for prices displays incorrect price ranges in manual mode +Fixed Pager works wrong with float page number +Fixed VAT Number for country that isn't selected in "European Union Countries" validated +Fixed Incorrect foreign key for EAV entity tables +Fixed VAT ID: Warning message about not-valid VAT Number displayed in green frame +Fixed Misprint in downloader/lib/Mage/Connect/Command/Install.php +Fixed URL Rewrites must be case-sensitive +Fixed Incorrect notification when customer's billing address updated with VAT ID within frontend +Fixed Unable to install package via uploader if author name contains dash +Fixed Fixed invoice subtotals for cases with partial invoice and discount +Fixed Incorrect work of "Disable Automatic Group Changes Based on VAT ID Default Value" option +Fixed Catalog URL Rewrites works incorrectly on creating categories + + + +==== 1.6.x-devel-134386 ==== + +=== Major Highlights === +Added VAT ID Validation +Implemented DHL for Europe + +=== Improvements === +Theme for iPhone was redesigned + +=== Fixes === +Fixed New Oder Status setting, specified for payment method works incorrectly when only virtual products are present in Order +Fixed Rounding issue in catalog and product view if price includes tax +Fixed Wrong status of catalog event is displayed by editing catalog event +Fixed Role Resources are not saved +Fixed "Qty for Item's Status to Become Out of Stock" option works incorrect +Fixed XML parser works incorrect +Fixed Mage_Reports_Model_Mysql4_Product_Index_Abstract must be declared abstract +Fixed "Date & Time" and "Time" custom options becomes required when editing product +Fixed Unable to cancel an order for an expired Authorize.net auth +Fixed Custom options are not stored when downloadable product is duplicated +Fixed "Cannot initialize the indexer process" error during Mass "Reindex Data" Action +Fixed Search by new attribute fails, attribute is not shown in layered navigation +Fixed Exception when "Price Navigation Step Calculation" set to "Manual" mode and FPT enabled +Fixed WPPHS: Cancel URL doesn't work as should be +Fixed Error about insufficient permissions is not appears on database backup creation +Fixed After rollback admin doesn't redirected to the Log in to Admin Panel page +Fixed Database Backup must not include indexer table data +Fixed Scheduled Backup creation/failure isn't logged +Fixed Deleting backup while it is used by another process +Fixed Opening *.tar files causes an error "There are no trailing zero-filled records" +Fixed Unable to search by "Time" and "No" in Backups table +Fixed Backups are deleted without confirmation +Fixed Reports must be excluded from database snapshot backup +Fixed There are no products in filtering results, if step calculation in automatic mode +Fixed No validation for "Default Price Navigation Step" +Fixed "Back" button doesn't work on the Create New Order page +Fixed Incorrect logic of Token expiration in Two Step Password Reset flow +Fixed Tag
is present in tool-tip for field "Number of results (For the last time placed)" on the Edit Search page +Fixed Impossible to use CAPTCHA that longer than 12 symbols +Fixed CAPTCHA with space symbol in it works incorrectly +Fixed System backup doesn't complete +Fixed Unnecessary hard code in Magento Extension +Fixed It's impossible specify form where CAPTCHA could be used +Fixed Wrong logic in Mage_Core_Model_Resource_Db_Collection_Abstract::join() +Fixed Description and Short Description are displayed incorrectly for products added with WYSIWYG +Fixed Adding product to the cart from the product review page leads to 404 page +Fixed Special symbols in Sort Order field +Fixed Text is wrong displayed with enabled Inline translation for Admin on back-end +Fixed Inline Translation: Unable to translate some customer information +Fixed Useless colon on front-end login page +Fixed Unable to continue checkout for product with zero price and non-zero shipping price +Fixed Import news_from_date field is configured poorly. It is not accepting the data from file +Fixed When updating product data through import, attributes that have a value cannot be assigned a new value that is empty +Fixed Unable to replace non-complex data for products with empty values during import +Fixed Customer can't understand is CAPTCHA case sensitive or not +Fixed Incorrect CAPTCHA's default configuration values +Fixed "Wrong order ID" exception in PayPal Express module under heavy load +Fixed Tax is applying on the order when creating it in the admin panel for a Customer Group with no taxes +Fixed Issue with retrieving order statuses for array of states +Fixed Wrong calculation product price with custom option type = Field and Fixed price +Fixed Back ordered downloadable product is not available even when it is set to be be accessible when order status is Pending + + + +==== 1.6.x-devel-133001 ==== + +=== Major Highlights === +Implemented Backup and Rollback functionality + +=== Fixes === +Fixed Full Product Price reindex started instead of partial if prices updated with mass action +Fixed Missing column "position" at table catalog_category_anc_products_index_tmp +Fixed Incorrect behavior of "Save in address book" option during admin Order creation for a new customer +- refactored Mage_Adminhtml_Model_Sales_Order_Create::_prepareCustomer() +Fixed Terms and Conditions is named differently on different pages +Fixed "Apply" and "Discount Amount" fields appear twice in the Catalog Price Rule +Fixed Poll shows incorrect percentage +Fixed Added validation ability for admin configuration forms +Fixed UPS XML misprint +Fixed Misprint in uploading files form +Fixed Unnecessary check boxes for gift options +Fixed Wrong resource section declaration in Mage_Tag module +Fixed "Customers Submitted this Tag" section doesn't update when customer has deleted tag from his account +Fixed Correct product in category position +Fixed Unable to create folder in Media Storage +Fixed Translation with single quotes breaks JavaScript +Fixed Out of memory error with hundreds of thousands of coupons attached to a single sales rule +Fixed Unable to translate buttons and tabs on the "Manage Category" page +Fixed Product Categories Tree doesn't expand in Manage Products page +Fixed Incorrect products qty returns to stock after refund for configurable product +Fixed Swf Uploader problems with cross domain Flash Player Policy +Fixed Unable to translate "Layout Updates" block on create/edit widget page +Fixed IE7: "Remember Me" checkbox visible on billing information step +Fixed CMS WYSIWYG Editor - widget is inserted as new while editing in IE8 +Fixed Currency code doesn't correspond to the amount in reports +Fixed Mage_Adminhtml_Block_Sales_Order_View_Tab_History::getFullHistory() doesn't use unique keys for each message +Fixed Scope labels are shown without translate wrapper +Fixed Wrong parameters handling in Core Helper formatDate() +Fixed Apostrophe in store name breaks Google Analytics tracking +Fixed Customer attribute prefix is not shown on frontend +Fixed Incorrect rounding for product with custom options (percent price) +Fixed Invoicing only part of products results in wrong totals calculation +Fixed Incorrect Row Total Calculation in Refund +- fixed rounding issues for partial Invoice and Refund +- refactored delta rounding +Fixed Filter by "Allow Countries" not working for Customer Address Form in the Backend +- checking added for set country to be in available list +Fixed There are sql-installs with empty string used as defaults for table columns, while column is not null able +Fixed Unable to translate "Note" in "Product Stock Options" +Fixed Various warning are displayed after creating shipment for 0 items +Fixed Invalid content in Content-header in the top of page during scrolling during order creation +Fixed "Online invoice" button present in backend when using Zero Subtotal Checkout +Fixed String cast type in in_array function +Fixed Newsletter template content should not disappear when "Show" / "Hide Editor" button was clicked +Fixed Import feature doesn't validate whether super_products_sku is existing or not + + + + +==== 1.6.x-devel-131783 ==== + +=== Major Highlights === +Implemented new Layered Navigation price bucket algorithm +Added captcha functionality + +=== Improvements === +XmlConnect package release v22.1 +Upgraded TinyMCE to v3.4.7 + +=== Fixes === +Fixed Cannot place order with downloadable product and discount code using Paypal Express payment method +Fixed Product still out of stock after Stock Status reindex +Fixed No ability to reindex Catalog URL Rewrites, error is shown +Fixed Automatic reindexing based on matched events doesn't change "Status" and "Last Run" columns at process list grid +Fixed Save catalog price rule gives trace if full reindex has already started +Fixed Reindex requires notification is not shown for Stock Status when stock is updated for several products using Mass Action +Fixed Incorrect FedEx's shipping rates (in case with non-US country origin) +Fixed After partial reindex MSRP value is not applied (not viewed) in catalog during mass update action +Fixed Wrong stock reindex on catalog if partial reindex done after full reindex started +Fixed In error message string "already exists." written twice, when trying to save Product Tax Class with the same name +Fixed Wrong stock reindex on catalog if partial reindex done after full reindex started +Fixed Slow checkout with non-flushed cache + + + +==== 1.6.x-devel-130478 ==== +Fixed Bundle product total is incorrect in Customization block +Fixed Special price isn't considered for bundle dynamic products in "Your Customization" block +Fixed Situation when any amount of duplicate reindex process can be running at one time +Fixed Error with Advanced Search (in case with Date Attribute) +Fixed Product Flat Data index isn't marked as "Reindex Required" after importing products when Flat Catalog is enabled +Fixed User can't change root category for the store +Fixed JS error causes the overwrite of Title field in PayPal Advanced configuration +Fixed Mass action doesn't partially reindex catalog for product name/description +Fixed Remove hint about $1 auth amount from informational message text (PayflowLink configuration) +Fixed Mass action doesn't partially reindex catalog for product prices +Fixed PayPal Payments Advanced works with $0 Auth instead of Full Auth +Fixed Impossible to place Order using "Pay with PayPal" button from PayPal Payments Advanced iframe +Fixed Mass action doesn't partially reindex catalog search for product if searchable attribute was updated +Fixed "Please wait" AJAX screen doesn't appear in the middle of the page +Fixed Checkout link on frontend is always referenced as http +Fixed GET request is incorrectly formed during category creation +- adjusted assertion to determine last viewed store +Fixed Display of Tier Pricing with Configurable Products +- added functionality to dynamically update tier prices accordingly to chosen product configuration +Fixed Google Analytics e-commerce tracking not working +Fixed URL key isn't used while product save +Fixed Added validation class to 'Qty increments' +Fixed Entered from admin customer date 1970 (or less) is saved as 2070 (or less) +Fixed cUrl resource must be closed after checking it for errors, not before +Fixed Exception while products mass update attributes in backend +Fixed Impossible to place Order using Pay with PayPal button from PayflowLink iframe, when Payment Action is Sale +Fixed No ability to reindex Catalog URL Rewrites, error is shown +Fixed Package with Core dependency + + + +==== 1.6.x-devel-129596 ==== +Fixed Stock Availability isn't updated if 1: Run Price Reindex 2: Update Stock Availability on product with mass action/single product +Fixed PayPal Payments Advanced update descriptions +Fixed Blank page instead shopping cart page when compilation and PSC are enabled +Fixed Unable to save redirect URL with special characters in search terms +Fixed Attribute Set field should have client-side validation and appropriate information message +Fixed "Localhost" isn't a valid domain name for installation +Fixed Iframe for PayPal Payments Advanced is not loaded +Fixed Retain the selected tab on editing CMS page +Fixed Payflow Link Express Checkout (pay with PayPal button) payment +Fixed Wrong number of reindex options is displayed +Fixed Wrong phpDocs for Varien_Db_Select +Fixed JavaScript calendar date range +- fixed JS calendar behavior to use 4-digits year format +Fixed Performance issue connected with sales rules on adding product to cart +Fixed DHL same error message appears several times +Fixed Item Status says "Mixed" when an order has been completed, should say "Shipped" +Fixed Product option title is absent in backend order page +Fixed Incorrect items number during multi shipping checkout +Fixed User name displays differently in the unsuccessful message and in the text field label (log in form) +Fixed If one or more indexers have Update Required = Yes and all Status = READY for all indexers than there is no notification for user to do reindex action +Fixed No space between Address line 1 and line 2 in Shipping Label (in case with FedEx) + + + +==== 1.6.x-devel-128838 ==== + +=== Improvements === +Indexers refactoring + +=== Changes === +Implemented localized PayPal settings for Japan +Added PayPal Payments Advanced + +=== Fixes === +Fixed JS works depends on the position attributes of the product +Fixed Char set encoding is out-of-date in Settlement reports +Fixed Settlement reports can't be downloaded if in merchant's account 'Settlement file' is set to 6.0 version +Fixed Unable to login when secure and unsecure urls are different +Fixed Customer session lost when using different domain/subdomain names for secure and unsecure urls +Fixed "Most Viewed" reports ignore Store View switcher +Fixed Long FPT name (and product name) doesn't fit into "My cart" block +Fixed Paypal IPN post back failure +Fixed Customer was unable to receive newsletters when it was created via backend +Fixed Wrong Comments History in notification of order creation/cancellation +Fixed Non escaped string causes javascript error +Fixed Unable to view pictures during product editing +Fixed Ampersand is saved incorrect in attribute label +Fixed IE8: JS error appears after pressing 'Add new rule' in catalog price rules menu +Fixed Exception after sorting newsletter queue +Fixed Customer is not able to log in from URL without "www" in some cases +Fixed SQL error on category view with enabled and configured FPT +Fixed Automatic reindexing based on matched events doesn't change "Status" and "Last Run" columns at process list grid +Fixed Performance issue with Magento Compiler + APC results in too many I/O calls +Fixed Website Administrator is able to change default values +Fixed Some of wishlist blocks and templates still treat the collection of wishlist items as collection of products +Fixed Unnecessary comments in "Share Wishlist" email + + + ==== 1.6.1.0 ==== === Major Highlights === diff --git a/app/Mage.php b/app/Mage.php index 4d50995d90..f5b1b716f7 100644 --- a/app/Mage.php +++ b/app/Mage.php @@ -33,8 +33,8 @@ if (defined('COMPILER_INCLUDE_PATH')) { $appPath = COMPILER_INCLUDE_PATH; set_include_path($appPath . PS . Mage::registry('original_include_path')); - include_once "Mage_Core_functions.php"; - include_once "Varien_Autoload.php"; + include_once COMPILER_INCLUDE_PATH . DS . "Mage_Core_functions.php"; + include_once COMPILER_INCLUDE_PATH . DS . "Varien_Autoload.php"; } else { /** * Set include path @@ -138,7 +138,8 @@ final class Mage public static function getVersion() { $i = self::getVersionInfo(); - return trim("{$i['major']}.{$i['minor']}.{$i['revision']}" . ($i['patch'] != '' ? ".{$i['patch']}" : "") . "-{$i['stability']}{$i['number']}", '.-'); + return trim("{$i['major']}.{$i['minor']}.{$i['revision']}" . ($i['patch'] != '' ? ".{$i['patch']}" : "") + . "-{$i['stability']}{$i['number']}", '.-'); } /** @@ -151,11 +152,11 @@ public static function getVersionInfo() { return array( 'major' => '1', - 'minor' => '6', - 'revision' => '1', + 'minor' => '7', + 'revision' => '0', 'patch' => '0', - 'stability' => '', - 'number' => '', + 'stability' => 'alpha', + 'number' => '1', ); } @@ -166,12 +167,14 @@ public static function getVersionInfo() public static function reset() { self::$_registry = array(); + self::$_appRoot = null; self::$_app = null; self::$_config = null; self::$_events = null; self::$_objects = null; self::$_isDownloader = false; self::$_isDeveloperMode = false; + self::$_isInstalled = null; // do not reset $headersSentThrowsException } @@ -424,7 +427,7 @@ public static function dispatchEvent($name, array $data = array()) * * @link Mage_Core_Model_Config::getModelInstance * @param string $modelClass - * @param array $arguments + * @param array|object $arguments * @return Mage_Core_Model_Abstract */ public static function getModel($modelClass = '', $arguments = array()) @@ -631,6 +634,12 @@ public static function run($code = '', $type = 'store', $options = array()) Varien_Profiler::start('mage'); self::setRoot(); self::$_app = new Mage_Core_Model_App(); + if (isset($options['request'])) { + self::$_app->setRequest($options['request']); + } + if (isset($options['response'])) { + self::$_app->setResponse($options['response']); + } self::$_events = new Varien_Event_Collection(); self::$_config = new Mage_Core_Model_Config($options); self::$_app->run(array( diff --git a/app/code/core/Mage/Admin/Model/Observer.php b/app/code/core/Mage/Admin/Model/Observer.php index c685beb3c0..0d7816be5e 100644 --- a/app/code/core/Mage/Admin/Model/Observer.php +++ b/app/code/core/Mage/Admin/Model/Observer.php @@ -33,6 +33,7 @@ */ class Mage_Admin_Model_Observer { + const FLAG_NO_LOGIN = 'no-login'; /** * Handler for controller_action_predispatch event * @@ -51,7 +52,8 @@ public function actionPreDispatchAdmin($observer) 'forgotpassword', 'resetpassword', 'resetpasswordpost', - 'logout' + 'logout', + 'refresh' // captcha refresh ); if (in_array($requestedActionName, $openActions)) { $request->setDispatched(true); @@ -64,7 +66,7 @@ public function actionPreDispatchAdmin($observer) $postLogin = $request->getPost('login'); $username = isset($postLogin['username']) ? $postLogin['username'] : ''; $password = isset($postLogin['password']) ? $postLogin['password'] : ''; - $user = $session->login($username, $password, $request); + $session->login($username, $password, $request); $request->setPost('login', null); } if (!$request->getParam('forwarded')) { diff --git a/app/code/core/Mage/Admin/Model/Roles.php b/app/code/core/Mage/Admin/Model/Roles.php index 0d5db92e88..7110ed9a5d 100644 --- a/app/code/core/Mage/Admin/Model/Roles.php +++ b/app/code/core/Mage/Admin/Model/Roles.php @@ -53,42 +53,86 @@ class Mage_Admin_Model_Roles extends Mage_Core_Model_Abstract */ protected $_eventPrefix = 'admin_roles'; + /** + * Constructor + */ protected function _construct() { $this->_init('admin/roles'); } + /** + * Update object into database + * + * @return Mage_Admin_Model_Roles + */ public function update() { $this->getResource()->update($this); return $this; } + /** + * Retrieve users collection + * + * @return Mage_Admin_Model_Resource_Roles_User_Collection + */ public function getUsersCollection() { return Mage::getResourceModel('admin/roles_user_collection'); } + /** + * Return tree of acl resources + * + * @return array|null|Varien_Simplexml_Element + */ public function getResourcesTree() { return $this->_buildResourcesArray(null, null, null, null, true); } + /** + * Return list of acl resources + * + * @return array|null|Varien_Simplexml_Element + */ public function getResourcesList() { return $this->_buildResourcesArray(); } + /** + * Return list of acl resources in 2D format + * + * @return array|null|Varien_Simplexml_Element + */ public function getResourcesList2D() { return $this->_buildResourcesArray(null, null, null, true); } + /** + * Return users for role + * + * @return array|false + */ public function getRoleUsers() { return $this->getResource()->getRoleUsers($this); } + /** + * Build resources array process + * + * @param null|Varien_Simplexml_Element $resource + * @param null $parentName + * @param int $level + * @param null $represent2Darray + * @param bool $rawNodes + * @param string $module + * @return array|null|Varien_Simplexml_Element + */ protected function _buildResourcesArray(Varien_Simplexml_Element $resource = null, $parentName = null, $level = 0, $represent2Darray = null, $rawNodes = false, $module = 'adminhtml') { @@ -99,7 +143,7 @@ protected function _buildResourcesArray(Varien_Simplexml_Element $resource = nul $level = -1; } else { $resourceName = $parentName; - if ($resource->getName() != 'title' && $resource->getName() != 'sort_order' && $resource->getName() != 'children') { + if (!in_array($resource->getName(), array('title', 'sort_order', 'children', 'disabled'))) { $resourceName = (is_null($parentName) ? '' : $parentName . '/') . $resource->getName(); //assigning module for its' children nodes @@ -121,22 +165,20 @@ protected function _buildResourcesArray(Varien_Simplexml_Element $resource = nul } } + //check children and run recursion if they exists $children = $resource->children(); - if (empty($children)) { - if ($rawNodes) { - return $resource; - } else { - return $result; + foreach ($children as $key => $child) { + if (1 == $child->disabled) { + $resource->{$key} = null; + continue; } - } - foreach ($children as $child) { $this->_buildResourcesArray($child, $resourceName, $level + 1, $represent2Darray, $rawNodes, $module); } + if ($rawNodes) { return $resource; } else { return $result; } } - } diff --git a/app/code/core/Mage/Admin/Model/Session.php b/app/code/core/Mage/Admin/Model/Session.php index 3f5cecba66..7f30aca610 100644 --- a/app/code/core/Mage/Admin/Model/Session.php +++ b/app/code/core/Mage/Admin/Model/Session.php @@ -86,7 +86,7 @@ public function login($username, $password, $request = null) } try { - /* @var $user Mage_Admin_Model_User */ + /** @var $user Mage_Admin_Model_User */ $user = Mage::getModel('admin/user'); $user->login($username, $password); if ($user->getId()) { @@ -98,19 +98,19 @@ public function login($username, $password, $request = null) $this->setIsFirstPageAfterLogin(true); $this->setUser($user); $this->setAcl(Mage::getResourceModel('admin/acl')->loadAcl()); - if ($requestUri = $this->_getRequestUri($request)) { + + $requestUri = $this->_getRequestUri($request); + if ($requestUri) { Mage::dispatchEvent('admin_session_user_login_success', array('user' => $user)); header('Location: ' . $requestUri); exit; } + } else { + Mage::throwException(Mage::helper('adminhtml')->__('Invalid User Name or Password.')); } - else { - Mage::throwException(Mage::helper('adminhtml')->__('Invalid Username or Password.')); - } - } - catch (Mage_Core_Exception $e) { + } catch (Mage_Core_Exception $e) { Mage::dispatchEvent('admin_session_user_login_failed', - array('user_name' => $username, 'exception' => $e)); + array('user_name' => $username, 'exception' => $e)); if ($request && !$request->getParam('messageSent')) { Mage::getSingleton('adminhtml/session')->addError($e->getMessage()); $request->setParam('messageSent', true); diff --git a/app/code/core/Mage/Admin/Model/User.php b/app/code/core/Mage/Admin/Model/User.php index f9b37bebcd..d3e56d0097 100644 --- a/app/code/core/Mage/Admin/Model/User.php +++ b/app/code/core/Mage/Admin/Model/User.php @@ -328,6 +328,10 @@ public function authenticate($username, $password) $result = false; try { + Mage::dispatchEvent('admin_user_authenticate_before', array( + 'username' => $username, + 'user' => $this + )); $this->loadByUsername($username); $sensitive = ($config) ? $username == $this->getUsername() : true; @@ -556,7 +560,7 @@ public function changeResetPasswordLinkToken($newResetPasswordLinkToken) { throw Mage::exception('Mage_Core', Mage::helper('adminhtml')->__('Invalid password reset token.')); } $this->setRpToken($newResetPasswordLinkToken); - $currentDate = Varien_Date::now(true); + $currentDate = Varien_Date::now(); $this->setRpTokenCreatedAt($currentDate); return $this; @@ -578,7 +582,7 @@ public function isResetPasswordLinkTokenExpired() $tokenExpirationPeriod = Mage::helper('admin')->getResetPasswordLinkExpirationPeriod(); - $currentDate = Varien_Date::now(true); + $currentDate = Varien_Date::now(); $currentTimestamp = Varien_Date::toTimestamp($currentDate); $tokenTimestamp = Varien_Date::toTimestamp($resetPasswordLinkTokenCreatedAt); if ($tokenTimestamp > $currentTimestamp) { diff --git a/app/code/core/Mage/Admin/etc/config.xml b/app/code/core/Mage/Admin/etc/config.xml index 8446743297..651f398f2b 100644 --- a/app/code/core/Mage/Admin/etc/config.xml +++ b/app/code/core/Mage/Admin/etc/config.xml @@ -79,7 +79,7 @@ admin_emails_forgot_email_template general - 3 + 1 diff --git a/app/code/core/Mage/Admin/sql/admin_setup/install-1.6.0.0.php b/app/code/core/Mage/Admin/sql/admin_setup/install-1.6.0.0.php index fd167fd6f3..f222ff2398 100644 --- a/app/code/core/Mage/Admin/sql/admin_setup/install-1.6.0.0.php +++ b/app/code/core/Mage/Admin/sql/admin_setup/install-1.6.0.0.php @@ -41,8 +41,8 @@ 'primary' => true, ), 'Assert ID') ->addColumn('assert_type', Varien_Db_Ddl_Table::TYPE_TEXT, 20, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Assert Type') ->addColumn('assert_data', Varien_Db_Ddl_Table::TYPE_TEXT, '64k', array( ), 'Assert Data') @@ -85,8 +85,8 @@ 'default' => '0', ), 'User ID') ->addColumn('role_name', Varien_Db_Ddl_Table::TYPE_TEXT, 50, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Role Name') ->addIndex($installer->getIdxName('admin/role', array('parent_id', 'sort_order')), array('parent_id', 'sort_order')) @@ -112,8 +112,8 @@ 'default' => '0', ), 'Role ID') ->addColumn('resource_id', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Resource ID') ->addColumn('privileges', Varien_Db_Ddl_Table::TYPE_TEXT, 20, array( 'nullable' => true, diff --git a/app/code/core/Mage/AdminNotification/Helper/Data.php b/app/code/core/Mage/AdminNotification/Helper/Data.php index d7214748bd..a1b7b9a8e8 100644 --- a/app/code/core/Mage/AdminNotification/Helper/Data.php +++ b/app/code/core/Mage/AdminNotification/Helper/Data.php @@ -129,6 +129,8 @@ public function isReadablePopupObject() $this->_popupReadable = true; } } + + $curl->close(); } return $this->_popupReadable; } diff --git a/app/code/core/Mage/AdminNotification/Model/Survey.php b/app/code/core/Mage/AdminNotification/Model/Survey.php index b9bb7431b1..14c43fda6c 100644 --- a/app/code/core/Mage/AdminNotification/Model/Survey.php +++ b/app/code/core/Mage/AdminNotification/Model/Survey.php @@ -50,6 +50,8 @@ public static function isSurveyUrlValid() $curl->setConfig(array('timeout' => 5)) ->write(Zend_Http_Client::GET, self::getSurveyUrl(), '1.0'); $response = $curl->read(); + $curl->close(); + if (Zend_Http_Response::extractCode($response) == 200) { return true; } diff --git a/app/code/core/Mage/Adminhtml/Block/Api/Tab/Rolesedit.php b/app/code/core/Mage/Adminhtml/Block/Api/Tab/Rolesedit.php index d38cc618be..478f257fa7 100644 --- a/app/code/core/Mage/Adminhtml/Block/Api/Tab/Rolesedit.php +++ b/app/code/core/Mage/Adminhtml/Block/Api/Tab/Rolesedit.php @@ -38,7 +38,9 @@ public function __construct() { $selrids = array(); foreach ($rules_set->getItems() as $item) { - if (array_key_exists(strtolower($item->getResource_id()), $resources) && $item->getPermission() == 'allow') { + if (array_key_exists(strtolower($item->getResource_id()), $resources) + && $item->getApiPermission() == 'allow') + { $resources[$item->getResource_id()]['checked'] = true; array_push($selrids, $item->getResource_id()); } diff --git a/app/code/core/Mage/Adminhtml/Block/Backup.php b/app/code/core/Mage/Adminhtml/Block/Backup.php index 25b0a7ea9d..9e3b484244 100644 --- a/app/code/core/Mage/Adminhtml/Block/Backup.php +++ b/app/code/core/Mage/Adminhtml/Block/Backup.php @@ -31,14 +31,14 @@ * @package Mage_Adminhtml * @author Magento Core Team */ - class Mage_Adminhtml_Block_Backup extends Mage_Adminhtml_Block_Template { - public function __construct() - { - parent::__construct(); - $this->setTemplate('backup/list.phtml'); - } + /** + * Block's template + * + * @var string + */ + protected $_template = 'backup/list.phtml'; protected function _prepareLayout() { @@ -46,14 +46,32 @@ protected function _prepareLayout() $this->setChild('createButton', $this->getLayout()->createBlock('adminhtml/widget_button') ->setData(array( - 'label' => Mage::helper('backup')->__('Create Backup'), - 'onclick' => "window.location.href='" . $this->getUrl('*/*/create') . "'", + 'label' => Mage::helper('backup')->__('Database Backup'), + 'onclick' => "return backup.backup('" . Mage_Backup_Helper_Data::TYPE_DB . "')", 'class' => 'task' )) ); + $this->setChild('createSnapshotButton', + $this->getLayout()->createBlock('adminhtml/widget_button') + ->setData(array( + 'label' => Mage::helper('backup')->__('System Backup'), + 'onclick' => "return backup.backup('" . Mage_Backup_Helper_Data::TYPE_SYSTEM_SNAPSHOT . "')", + 'class' => '' + )) + ); + $this->setChild('createMediaBackupButton', + $this->getLayout()->createBlock('adminhtml/widget_button') + ->setData(array( + 'label' => Mage::helper('backup')->__('Database and Media Backup'), + 'onclick' => "return backup.backup('" . Mage_Backup_Helper_Data::TYPE_MEDIA . "')", + 'class' => '' + )) + ); $this->setChild('backupsGrid', $this->getLayout()->createBlock('adminhtml/backup_grid') ); + + $this->setChild('dialogs', $this->getLayout()->createBlock('adminhtml/backup_dialogs')); } public function getCreateButtonHtml() @@ -61,8 +79,38 @@ public function getCreateButtonHtml() return $this->getChildHtml('createButton'); } + /** + * Generate html code for "Create System Snapshot" button + * + * @return string + */ + public function getCreateSnapshotButtonHtml() + { + return $this->getChildHtml('createSnapshotButton'); + } + + /** + * Generate html code for "Create Media Backup" button + * + * @return string + */ + public function getCreateMediaBackupButtonHtml() + { + return $this->getChildHtml('createMediaBackupButton'); + } + public function getGridHtml() { return $this->getChildHtml('backupsGrid'); } + + /** + * Generate html code for pop-up messages that will appear when user click on "Rollback" link + * + * @return string + */ + public function getDialogsHtml() + { + return $this->getChildHtml('dialogs'); + } } diff --git a/app/code/core/Mage/Adminhtml/Block/Backup/Dialogs.php b/app/code/core/Mage/Adminhtml/Block/Backup/Dialogs.php new file mode 100644 index 0000000000..21f57799e5 --- /dev/null +++ b/app/code/core/Mage/Adminhtml/Block/Backup/Dialogs.php @@ -0,0 +1,53 @@ + + */ +class Mage_Adminhtml_Block_Backup_Dialogs extends Mage_Adminhtml_Block_Template +{ + /** + * Block's template + * + * @var string + */ + protected $_template = 'backup/dialogs.phtml'; + + /** + * Include backup.js file in page before rendering + * + * @see Mage_Core_Block_Abstract::_prepareLayout() + */ + protected function _prepareLayout() + { + $this->getLayout()->getBlock('head')->addJs('mage/adminhtml/backup.js'); + parent::_prepareLayout(); + } +} diff --git a/app/code/core/Mage/Adminhtml/Block/Backup/Grid.php b/app/code/core/Mage/Adminhtml/Block/Backup/Grid.php index 8b8308aa7a..bdb381c38e 100644 --- a/app/code/core/Mage/Adminhtml/Block/Backup/Grid.php +++ b/app/code/core/Mage/Adminhtml/Block/Backup/Grid.php @@ -51,8 +51,29 @@ protected function _prepareCollection() return parent::_prepareCollection(); } + /** + * Prepare mass action controls + * + * @return Mage_Adminhtml_Block_Backup_Grid + */ + protected function _prepareMassaction() + { + $this->setMassactionIdField('id'); + $this->getMassactionBlock()->setFormFieldName('ids'); + + $this->getMassactionBlock()->addItem('delete', array( + 'label'=> Mage::helper('adminhtml')->__('Delete'), + 'url' => $this->getUrl('*/*/massDelete'), + 'confirm' => Mage::helper('backup')->__('Are you sure you want to delete the selected backup(s)?') + )); + + return $this; + } + /** * Configuration of grid + * + * @return Mage_Adminhtml_Block_Backup_Grid */ protected function _prepareColumns() { @@ -68,39 +89,42 @@ protected function _prepareColumns() 'header' => Mage::helper('backup')->__('Size, Bytes'), 'index' => 'size', 'type' => 'number', - 'sortable' => false, + 'sortable' => true, 'filter' => false )); $this->addColumn('type', array( 'header' => Mage::helper('backup')->__('Type'), 'type' => 'options', - 'options' => array('db' => Mage::helper('backup')->__('DB')), + 'options' => Mage::helper('backup')->getBackupTypes(), 'index' =>'type' )); $this->addColumn('download', array( 'header' => Mage::helper('backup')->__('Download'), - 'format' => 'gz   ('.$url7zip.')', + 'format' => '$extension   ('.$url7zip.')', 'index' => 'type', 'sortable' => false, 'filter' => false )); - $this->addColumn('action', array( - 'header' => Mage::helper('backup')->__('Action'), - 'type' => 'action', - 'width' => '80px', - 'filter' => false, - 'sortable' => false, - 'actions' => array(array( - 'url' => $this->getUrl('*/*/delete', array('time' => '$time', 'type' => '$type')), - 'caption' => Mage::helper('adminhtml')->__('Delete'), - 'confirm' => Mage::helper('adminhtml')->__('Are you sure you want to do this?') - )), - 'index' => 'type', - 'sortable' => false - )); + if (Mage::helper('backup')->isRollbackAllowed()){ + $this->addColumn('action', array( + 'header' => Mage::helper('backup')->__('Action'), + 'type' => 'action', + 'width' => '80px', + 'filter' => false, + 'sortable' => false, + 'actions' => array(array( + 'url' => '#', + 'caption' => Mage::helper('backup')->__('Rollback'), + 'onclick' => 'return backup.rollback(\'$type\', \'$time\');' + )), + 'index' => 'type', + 'sortable' => false + )); + } return $this; } diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Form/Renderer/Fieldset/Element.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Form/Renderer/Fieldset/Element.php index 2fe8576125..c1f07d1a4b 100644 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Form/Renderer/Fieldset/Element.php +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Form/Renderer/Fieldset/Element.php @@ -31,7 +31,8 @@ * @package Mage_Adminhtml * @author Magento Core Team */ -class Mage_Adminhtml_Block_Catalog_Form_Renderer_Fieldset_Element extends Mage_Adminhtml_Block_Widget_Form_Renderer_Fieldset_Element +class Mage_Adminhtml_Block_Catalog_Form_Renderer_Fieldset_Element + extends Mage_Adminhtml_Block_Widget_Form_Renderer_Fieldset_Element { /** * Initialize block template @@ -101,8 +102,9 @@ public function usedDefault() if (!$this->getDataObject()->getExistsStoreValueFlag($attributeCode)) { return true; - } else if ($this->getElement()->getValue() == $defaultValue && - $this->getDataObject()->getStoreId() != $this->_getDefaultStoreId()) { + } else if ($this->getElement()->getValue() == $defaultValue && + $this->getDataObject()->getStoreId() != $this->_getDefaultStoreId() + ) { return false; } if ($defaultValue === false && !$this->getAttribute()->getIsRequired() && $this->getElement()->getValue()) { @@ -139,13 +141,11 @@ public function getScopeLabel() return $html; } if ($attribute->isScopeGlobal()) { - $html.= '[GLOBAL]'; - } - elseif ($attribute->isScopeWebsite()) { - $html.= '[WEBSITE]'; - } - elseif ($attribute->isScopeStore()) { - $html.= '[STORE VIEW]'; + $html .= Mage::helper('adminhtml')->__('[GLOBAL]'); + } elseif ($attribute->isScopeWebsite()) { + $html .= Mage::helper('adminhtml')->__('[WEBSITE]'); + } elseif ($attribute->isScopeStore()) { + $html .= Mage::helper('adminhtml')->__('[STORE VIEW]'); } return $html; @@ -158,7 +158,12 @@ public function getScopeLabel() */ public function getElementLabelHtml() { - return $this->getElement()->getLabelHtml(); + $element = $this->getElement(); + $label = $element->getLabel(); + if (empty($label)) { + $element->setLabel($this->__($label)); + } + return $element->getLabelHtml(); } /** diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Attribute/Set/Main/Formset.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Attribute/Set/Main/Formset.php index 2e2fb38f5f..5c1f274a0d 100644 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Attribute/Set/Main/Formset.php +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Attribute/Set/Main/Formset.php @@ -37,6 +37,10 @@ public function __construct() parent::__construct(); } + /** + * Prepares attribute set form + * + */ protected function _prepareForm() { $data = Mage::getModel('eav/entity_attribute_set') @@ -44,21 +48,19 @@ protected function _prepareForm() $form = new Varien_Data_Form(); $fieldset = $form->addFieldset('set_name', array('legend'=> Mage::helper('catalog')->__('Edit Set Name'))); - $fieldset->addField('attribute_set_name', 'text', - array( - 'label' => Mage::helper('catalog')->__('Name'), - 'note' => Mage::helper('catalog')->__('For internal use.'), - 'name' => 'attribute_set_name', - 'required' => true, - 'class' => 'required-entry', - 'value' => $data->getAttributeSetName() + $fieldset->addField('attribute_set_name', 'text', array( + 'label' => Mage::helper('catalog')->__('Name'), + 'note' => Mage::helper('catalog')->__('For internal use.'), + 'name' => 'attribute_set_name', + 'required' => true, + 'class' => 'required-entry validate-no-html-tags', + 'value' => $data->getAttributeSetName() )); if( !$this->getRequest()->getParam('id', false) ) { - $fieldset->addField('gotoEdit', 'hidden', - array( - 'name' => 'gotoEdit', - 'value' => '1' + $fieldset->addField('gotoEdit', 'hidden', array( + 'name' => 'gotoEdit', + 'value' => '1' )); $sets = Mage::getModel('eav/entity_attribute_set') @@ -67,13 +69,12 @@ protected function _prepareForm() ->load() ->toOptionArray(); - $fieldset->addField('skeleton_set', 'select', - array( - 'label' => Mage::helper('catalog')->__('Based On'), - 'name' => 'skeleton_set', - 'required' => true, - 'class' => 'required-entry', - 'values' => $sets, + $fieldset->addField('skeleton_set', 'select', array( + 'label' => Mage::helper('catalog')->__('Based On'), + 'name' => 'skeleton_set', + 'required' => true, + 'class' => 'required-entry', + 'values' => $sets, )); } diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Action/Attribute/Tab/Attributes.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Action/Attribute/Tab/Attributes.php index 90d3312bc6..f10283945a 100644 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Action/Attribute/Tab/Attributes.php +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Action/Attribute/Tab/Attributes.php @@ -79,6 +79,7 @@ protected function _getAdditionalElementTypes() { return array( 'price' => Mage::getConfig()->getBlockClassName('adminhtml/catalog_product_helper_form_price'), + 'weight' => Mage::getConfig()->getBlockClassName('adminhtml/catalog_product_helper_form_weight'), 'image' => Mage::getConfig()->getBlockClassName('adminhtml/catalog_product_helper_form_image'), 'boolean' => Mage::getConfig()->getBlockClassName('adminhtml/catalog_product_helper_form_boolean') ); diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Attributes.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Attributes.php index 603e5e4549..1d22ca2ff8 100644 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Attributes.php +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Attributes.php @@ -29,69 +29,79 @@ * * @category Mage * @package Mage_Adminhtml - * @author Magento Core Team + * @author Magento Core Team */ class Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Attributes extends Mage_Adminhtml_Block_Catalog_Form { /** - * Load Wysiwyg on demand and Prepare layout + * Load Wysiwyg on demand and prepare layout */ protected function _prepareLayout() { parent::_prepareLayout(); - if (Mage::helper('catalog')->isModuleEnabled('Mage_Cms')) { - if (Mage::getSingleton('cms/wysiwyg_config')->isEnabled()) { - $this->getLayout()->getBlock('head')->setCanLoadTinyMce(true); - } + if (Mage::helper('catalog')->isModuleEnabled('Mage_Cms') + && Mage::getSingleton('cms/wysiwyg_config')->isEnabled() + ) { + $this->getLayout()->getBlock('head')->setCanLoadTinyMce(true); } } + /** + * Prepare attributes form + * + * @return null + */ protected function _prepareForm() { - if ($group = $this->getGroup()) { + $group = $this->getGroup(); + if ($group) { $form = new Varien_Data_Form(); - /** - * Initialize product object as form property - * for using it in elements generation - */ + + // Initialize product object as form property to use it during elements generation $form->setDataObject(Mage::registry('product')); - $fieldset = $form->addFieldset('group_fields'.$group->getId(), - array( - 'legend'=>Mage::helper('catalog')->__($group->getAttributeGroupName()), - 'class'=>'fieldset-wide', + $fieldset = $form->addFieldset('group_fields' . $group->getId(), array( + 'legend' => Mage::helper('catalog')->__($group->getAttributeGroupName()), + 'class' => 'fieldset-wide' )); $attributes = $this->getGroupAttributes(); $this->_setFieldset($attributes, $fieldset, array('gallery')); - if ($urlKey = $form->getElement('url_key')) { + $urlKey = $form->getElement('url_key'); + if ($urlKey) { $urlKey->setRenderer( $this->getLayout()->createBlock('adminhtml/catalog_form_renderer_attribute_urlkey') ); } - if ($tierPrice = $form->getElement('tier_price')) { + $tierPrice = $form->getElement('tier_price'); + if ($tierPrice) { $tierPrice->setRenderer( $this->getLayout()->createBlock('adminhtml/catalog_product_edit_tab_price_tier') ); } - if ($recurringProfile = $form->getElement('recurring_profile')) { + $groupPrice = $form->getElement('group_price'); + if ($groupPrice) { + $groupPrice->setRenderer( + $this->getLayout()->createBlock('adminhtml/catalog_product_edit_tab_price_group') + ); + } + + $recurringProfile = $form->getElement('recurring_profile'); + if ($recurringProfile) { $recurringProfile->setRenderer( $this->getLayout()->createBlock('adminhtml/catalog_product_edit_tab_price_recurring') ); } - /** - * Add new attribute button if not image tab - */ + // Add new attribute button if it is not an image tab if (!$form->getElement('media_gallery') - && Mage::getSingleton('admin/session')->isAllowed('catalog/attributes/attributes')) { - $headerBar = $this->getLayout()->createBlock( - 'adminhtml/catalog_product_edit_tab_attributes_create' - ); + && Mage::getSingleton('admin/session')->isAllowed('catalog/attributes/attributes') + ) { + $headerBar = $this->getLayout()->createBlock('adminhtml/catalog_product_edit_tab_attributes_create'); $headerBar->getConfig() ->setTabId('group_' . $group->getId()) @@ -101,9 +111,7 @@ protected function _prepareForm() ->setTypeId($form->getDataObject()->getTypeId()) ->setProductId($form->getDataObject()->getId()); - $fieldset->setHeaderBar( - $headerBar->toHtml() - ); + $fieldset->setHeaderBar($headerBar->toHtml()); } if ($form->getElement('meta_description')) { @@ -111,9 +119,8 @@ protected function _prepareForm() } $values = Mage::registry('product')->getData(); - /** - * Set attribute default values for new product - */ + + // Set default attribute values for new product if (!Mage::registry('product')->getId()) { foreach ($attributes as $attribute) { if (!isset($values[$attribute->getAttributeCode()])) { @@ -124,7 +131,8 @@ protected function _prepareForm() if (Mage::registry('product')->hasLockedAttributes()) { foreach (Mage::registry('product')->getLockedAttributes() as $attribute) { - if ($element = $form->getElement($attribute)) { + $element = $form->getElement($attribute); + if ($element) { $element->setReadonly(true, true); } } @@ -132,16 +140,22 @@ protected function _prepareForm() $form->addValues($values); $form->setFieldNameSuffix('product'); - Mage::dispatchEvent('adminhtml_catalog_product_edit_prepare_form', array('form'=>$form)); + Mage::dispatchEvent('adminhtml_catalog_product_edit_prepare_form', array('form' => $form)); $this->setForm($form); } } + /** + * Retrieve additional element types + * + * @return array + */ protected function _getAdditionalElementTypes() { $result = array( 'price' => Mage::getConfig()->getBlockClassName('adminhtml/catalog_product_helper_form_price'), + 'weight' => Mage::getConfig()->getBlockClassName('adminhtml/catalog_product_helper_form_weight'), 'gallery' => Mage::getConfig()->getBlockClassName('adminhtml/catalog_product_helper_form_gallery'), 'image' => Mage::getConfig()->getBlockClassName('adminhtml/catalog_product_helper_form_image'), 'boolean' => Mage::getConfig()->getBlockClassName('adminhtml/catalog_product_helper_form_boolean'), @@ -150,9 +164,9 @@ protected function _getAdditionalElementTypes() $response = new Varien_Object(); $response->setTypes(array()); - Mage::dispatchEvent('adminhtml_catalog_product_edit_element_types', array('response'=>$response)); + Mage::dispatchEvent('adminhtml_catalog_product_edit_element_types', array('response' => $response)); - foreach ($response->getTypes() as $typeName=>$typeClass) { + foreach ($response->getTypes() as $typeName => $typeClass) { $result[$typeName] = $typeClass; } diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Categories.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Categories.php index 8047c61e42..f10e04f1c9 100644 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Categories.php +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Categories.php @@ -29,13 +29,16 @@ * * @category Mage * @package Mage_Adminhtml - * @author Magento Core Team + * @author Magento Core Team */ class Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Categories extends Mage_Adminhtml_Block_Catalog_Category_Tree { protected $_categoryIds; protected $_selectedNodes = null; + /** + * Specify template to use + */ public function __construct() { parent::__construct(); @@ -62,19 +65,33 @@ public function isReadonly() return $this->getProduct()->getCategoriesReadonly(); } + /** + * Return array with category IDs which the product is assigned to + * + * @return array + */ protected function getCategoryIds() { return $this->getProduct()->getCategoryIds(); } + /** + * Forms string out of getCategoryIds() + * + * @return string + */ public function getIdsString() { return implode(',', $this->getCategoryIds()); } + /** + * Returns root node and sets 'checked' flag (if necessary) + * + * @return Varien_Data_Tree_Node + */ public function getRootNode() { -// $root = parent::getRoot(); $root = $this->getRoot(); if ($root && in_array($root->getId(), $this->getCategoryIds())) { $root->setChecked(true); @@ -82,7 +99,14 @@ public function getRootNode() return $root; } - public function getRoot($parentNodeCategory=null, $recursionLevel=3) + /** + * Returns root node + * + * @param Mage_Catalog_Model_Category|null $parentNodeCategory + * @param int $recursionLevel + * @return Varien_Data_Tree_Node + */ + public function getRoot($parentNodeCategory = null, $recursionLevel = 3) { if (!is_null($parentNodeCategory) && $parentNodeCategory->getId()) { return $this->getNode($parentNodeCategory, $recursionLevel); @@ -127,21 +151,21 @@ public function getRoot($parentNodeCategory=null, $recursionLevel=3) return $root; } - protected function _getNodeJson($node, $level=1) + /** + * Returns array with configuration of current node + * + * @param Varien_Data_Tree_Node $node + * @param int $level How deep is the node in the tree + * @return array + */ + protected function _getNodeJson($node, $level = 1) { $item = parent::_getNodeJson($node, $level); - $isParent = $this->_isParentSelectedCategory($node); - - if ($isParent) { + if ($this->_isParentSelectedCategory($node)) { $item['expanded'] = true; } -// if ($node->getLevel() > 1 && !$isParent && isset($item['children'])) { -// $item['children'] = array(); -// } - - if (in_array($node->getId(), $this->getCategoryIds())) { $item['checked'] = true; } @@ -149,23 +173,41 @@ protected function _getNodeJson($node, $level=1) if ($this->isReadonly()) { $item['disabled'] = true; } + return $item; } + /** + * Returns whether $node is a parent (not exactly direct) of a selected node + * + * @param Varien_Data_Tree_Node $node + * @return bool + */ protected function _isParentSelectedCategory($node) { - foreach ($this->_getSelectedNodes() as $selected) { - if ($selected) { - $pathIds = explode('/', $selected->getPathId()); - if (in_array($node->getId(), $pathIds)) { - return true; + $result = false; + // Contains string with all category IDs of children (not exactly direct) of the node + $allChildren = $node->getAllChildren(); + if ($allChildren) { + $selectedCategoryIds = $this->getCategoryIds(); + $allChildrenArr = explode(',', $allChildren); + for ($i = 0, $cnt = count($selectedCategoryIds); $i < $cnt; $i++) { + $isSelf = $node->getId() == $selectedCategoryIds[$i]; + if (!$isSelf && in_array($selectedCategoryIds[$i], $allChildrenArr)) { + $result = true; + break; } } } - return false; + return $result; } + /** + * Returns array with nodes those are selected (contain current product) + * + * @return array + */ protected function _getSelectedNodes() { if ($this->_selectedNodes === null) { @@ -181,6 +223,12 @@ protected function _getSelectedNodes() return $this->_selectedNodes; } + /** + * Returns JSON-encoded array of category children + * + * @param int $categoryId + * @return string + */ public function getCategoryChildrenJson($categoryId) { $category = Mage::getModel('catalog/category')->load($categoryId); @@ -198,15 +246,21 @@ public function getCategoryChildrenJson($categoryId) return Mage::helper('core')->jsonEncode($children); } - public function getLoadTreeUrl($expanded=null) + /** + * Returns URL for loading tree + * + * @param null $expanded + * @return string + */ + public function getLoadTreeUrl($expanded = null) { - return $this->getUrl('*/*/categoriesJson', array('_current'=>true)); + return $this->getUrl('*/*/categoriesJson', array('_current' => true)); } /** * Return distinct path ids of selected categories * - * @param int $rootId Root category Id for context + * @param mixed $rootId Root category Id for context * @return array */ public function getSelectedCategoriesPathIds($rootId = false) diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Price/Group.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Price/Group.php new file mode 100644 index 0000000000..f305e78046 --- /dev/null +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Price/Group.php @@ -0,0 +1,96 @@ + + */ +class Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Price_Group + extends Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Price_Group_Abstract +{ + /** + * Initialize block + */ + public function __construct() + { + $this->setTemplate('catalog/product/edit/price/group.phtml'); + } + + /** + * Sort values + * + * @param array $data + * @return array + */ + protected function _sortValues($data) + { + usort($data, array($this, '_sortGroupPrices')); + return $data; + } + + /** + * Sort group price values callback method + * + * @param array $a + * @param array $b + * @return int + */ + protected function _sortGroupPrices($a, $b) + { + if ($a['website_id'] != $b['website_id']) { + return $a['website_id'] < $b['website_id'] ? -1 : 1; + } + if ($a['cust_group'] != $b['cust_group']) { + return $this->getCustomerGroups($a['cust_group']) < $this->getCustomerGroups($b['cust_group']) ? -1 : 1; + } + return 0; + } + + /** + * Prepare global layout + * + * Add "Add Group Price" button to layout + * + * @return Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Price_Group + */ + protected function _prepareLayout() + { + $button = $this->getLayout()->createBlock('adminhtml/widget_button') + ->setData(array( + 'label' => Mage::helper('catalog')->__('Add Group Price'), + 'onclick' => 'return groupPriceControl.addItem()', + 'class' => 'add' + )); + $button->setName('add_group_price_item_button'); + + $this->setChild('add_button', $button); + return parent::_prepareLayout(); + } + +} diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Price/Group/Abstract.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Price/Group/Abstract.php new file mode 100644 index 0000000000..89cfbe4488 --- /dev/null +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Price/Group/Abstract.php @@ -0,0 +1,347 @@ + + */ +abstract class Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Price_Group_Abstract + extends Mage_Adminhtml_Block_Widget + implements Varien_Data_Form_Element_Renderer_Interface +{ + /** + * Form element instance + * + * @var Varien_Data_Form_Element_Abstract + */ + protected $_element; + + /** + * Customer groups cache + * + * @var array + */ + protected $_customerGroups; + + /** + * Websites cache + * + * @var array + */ + protected $_websites; + + /** + * Retrieve current product instance + * + * @return Mage_Catalog_Model_Product + */ + public function getProduct() + { + return Mage::registry('product'); + } + + /** + * Render HTML + * + * @param Varien_Data_Form_Element_Abstract $element + * @return string + */ + public function render(Varien_Data_Form_Element_Abstract $element) + { + $this->setElement($element); + return $this->toHtml(); + } + + /** + * Set form element instance + * + * @param Varien_Data_Form_Element_Abstract $element + * @return Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Price_Group_Abstract + */ + public function setElement(Varien_Data_Form_Element_Abstract $element) + { + $this->_element = $element; + return $this; + } + + /** + * Retrieve form element instance + * + * @return Varien_Data_Form_Element_Abstract + */ + public function getElement() + { + return $this->_element; + } + + /** + * Prepare group price values + * + * @return array + */ + public function getValues() + { + $values = array(); + $data = $this->getElement()->getValue(); + + if (is_array($data)) { + $values = $this->_sortValues($data); + } + + foreach ($values as &$value) { + $value['readonly'] = ($value['website_id'] == 0) + && $this->isShowWebsiteColumn() + && !$this->isAllowChangeWebsite(); + } + + return $values; + } + + /** + * Sort values + * + * @param array $data + * @return array + */ + protected function _sortValues($data) + { + return $data; + } + + /** + * Retrieve allowed customer groups + * + * @param int|null $groupId return name by customer group id + * @return array|string + */ + public function getCustomerGroups($groupId = null) + { + if ($this->_customerGroups === null) { + if (!Mage::helper('catalog')->isModuleEnabled('Mage_Customer')) { + return array(); + } + $collection = Mage::getModel('customer/group')->getCollection(); + $this->_customerGroups = $this->_getInitialCustomerGroups(); + + foreach ($collection as $item) { + /** @var $item Mage_Customer_Model_Group */ + $this->_customerGroups[$item->getId()] = $item->getCustomerGroupCode(); + } + } + + if ($groupId !== null) { + return isset($this->_customerGroups[$groupId]) ? $this->_customerGroups[$groupId] : array(); + } + + return $this->_customerGroups; + } + + /** + * Retrieve list of initial customer groups + * + * @return array + */ + protected function _getInitialCustomerGroups() + { + return array(); + } + + /** + * Retrieve number of websites + * + * @return int + */ + public function getWebsiteCount() + { + return count($this->getWebsites()); + } + + /** + * Show website column and switcher for group price table + * + * @return bool + */ + public function isMultiWebsites() + { + return !Mage::app()->isSingleStoreMode(); + } + + /** + * Retrieve allowed for edit websites + * + * @return array + */ + public function getWebsites() + { + if (!is_null($this->_websites)) { + return $this->_websites; + } + + $this->_websites = array( + 0 => array( + 'name' => Mage::helper('catalog')->__('All Websites'), + 'currency' => Mage::app()->getBaseCurrencyCode() + ) + ); + + if (!$this->isScopeGlobal() && $this->getProduct()->getStoreId()) { + /** @var $website Mage_Core_Model_Website */ + $website = Mage::app()->getStore($this->getProduct()->getStoreId())->getWebsite(); + + $this->_websites[$website->getId()] = array( + 'name' => $website->getName(), + 'currency' => $website->getBaseCurrencyCode() + ); + } elseif (!$this->isScopeGlobal()) { + $websites = Mage::app()->getWebsites(false); + $productWebsiteIds = $this->getProduct()->getWebsiteIds(); + foreach ($websites as $website) { + /** @var $website Mage_Core_Model_Website */ + if (!in_array($website->getId(), $productWebsiteIds)) { + continue; + } + $this->_websites[$website->getId()] = array( + 'name' => $website->getName(), + 'currency' => $website->getBaseCurrencyCode() + ); + } + } + + return $this->_websites; + } + + /** + * Retrieve default value for customer group + * + * @return int + */ + public function getDefaultCustomerGroup() + { + return Mage_Customer_Model_Group::CUST_GROUP_ALL; + } + + /** + * Retrieve default value for website + * + * @return int + */ + public function getDefaultWebsite() + { + if ($this->isShowWebsiteColumn() && !$this->isAllowChangeWebsite()) { + return Mage::app()->getStore($this->getProduct()->getStoreId())->getWebsiteId(); + } + return 0; + } + + /** + * Retrieve 'add group price item' button HTML + * + * @return string + */ + public function getAddButtonHtml() + { + return $this->getChildHtml('add_button'); + } + + /** + * Retrieve customized price column header + * + * @param string $default + * @return string + */ + public function getPriceColumnHeader($default) + { + if ($this->hasData('price_column_header')) { + return $this->getData('price_column_header'); + } else { + return $default; + } + } + + /** + * Retrieve customized price column header + * + * @param string $default + * @return string + */ + public function getPriceValidation($default) + { + if ($this->hasData('price_validation')) { + return $this->getData('price_validation'); + } else { + return $default; + } + } + + /** + * Retrieve Group Price entity attribute + * + * @return Mage_Catalog_Model_Resource_Eav_Attribute + */ + public function getAttribute() + { + return $this->getElement()->getEntityAttribute(); + } + + /** + * Check group price attribute scope is global + * + * @return bool + */ + public function isScopeGlobal() + { + return $this->getAttribute()->isScopeGlobal(); + } + + /** + * Show group prices grid website column + * + * @return bool + */ + public function isShowWebsiteColumn() + { + if ($this->isScopeGlobal() || Mage::app()->isSingleStoreMode()) { + return false; + } + return true; + } + + /** + * Check is allow change website value for combination + * + * @return bool + */ + public function isAllowChangeWebsite() + { + if (!$this->isShowWebsiteColumn() || $this->getProduct()->getStoreId()) { + return false; + } + return true; + } +} diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Price/Tier.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Price/Tier.php index bc048fc300..084b703704 100644 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Price/Tier.php +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Price/Tier.php @@ -32,33 +32,11 @@ * @author Magento Core Team */ class Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Price_Tier - extends Mage_Adminhtml_Block_Widget - implements Varien_Data_Form_Element_Renderer_Interface + extends Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Price_Group_Abstract { - /** - * Form element instance - * - * @var Varien_Data_Form_Element - */ - protected $_element; /** - * Customer Groups cache - * - * @var array - */ - protected $_customerGroups; - - /** - * Websites cache - * - * @var array - */ - protected $_websites; - - /** - * Define tier price template file - * + * Initialize block */ public function __construct() { @@ -66,69 +44,25 @@ public function __construct() } /** - * Retrieve current edit product instance - * - * @return Mage_Catalog_Model_Product - */ - public function getProduct() - { - return Mage::registry('product'); - } - - /** - * Render HTML + * Retrieve list of initial customer groups * - * @param Varien_Data_Form_Element_Abstract $element - * @return string - */ - public function render(Varien_Data_Form_Element_Abstract $element) - { - $this->setElement($element); - return $this->toHtml(); - } - - /** - * Set form element instance - * - * @param Varien_Data_Form_Element_Abstract $element - * @return Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Price_Tier - */ - public function setElement(Varien_Data_Form_Element_Abstract $element) - { - $this->_element = $element; - return $this; - } - - /** - * Retrieve form element instance - * - * @return Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Price_Tier + * @return array */ - public function getElement() + protected function _getInitialCustomerGroups() { - return $this->_element; + return array(Mage_Customer_Model_Group::CUST_GROUP_ALL => Mage::helper('catalog')->__('ALL GROUPS')); } /** - * Prepare Tier Price values + * Sort values * + * @param array $data * @return array */ - public function getValues() + protected function _sortValues($data) { - $values = array(); - $data = $this->getElement()->getValue(); - - if (is_array($data)) { - usort($data, array($this, '_sortTierPrices')); - $values = $data; - } - - foreach ($values as &$v) { - $v['readonly'] = $v['website_id'] == 0 && $this->isShowWebsiteColumn() && !$this->isAllowChangeWebsite(); - } - - return $values; + usort($data, array($this, '_sortTierPrices')); + return $data; } /** @@ -153,123 +87,6 @@ protected function _sortTierPrices($a, $b) return 0; } - /** - * Retrieve allowed customer groups - * - * @param int $groupId return name by customer group id - * @return array|string - */ - public function getCustomerGroups($groupId = null) - { - if ($this->_customerGroups === null) { - if (!Mage::helper('catalog')->isModuleEnabled('Mage_Customer')) { - return array(); - } - $collection = Mage::getModel('customer/group')->getCollection(); - $this->_customerGroups = array( - Mage_Customer_Model_Group::CUST_GROUP_ALL => Mage::helper('catalog')->__('ALL GROUPS') - ); - - foreach ($collection as $item) { - /* @var $item Mage_Customer_Model_Group */ - $this->_customerGroups[$item->getId()] = $item->getCustomerGroupCode(); - } - } - - if ($groupId !== null) { - return isset($this->_customerGroups[$groupId]) ? $this->_customerGroups[$groupId] : array(); - } - - return $this->_customerGroups; - } - - /** - * Retrieve count of websites - * - * @return int - */ - public function getWebsiteCount() - { - return count($this->getWebsites()); - } - - /** - * Show Website column and switcher for tier price table - * - * @return bool - */ - public function isMultiWebsites() - { - return !Mage::app()->isSingleStoreMode(); - } - - /** - * Retrieve allowed for edit websites - * - * @return array - */ - public function getWebsites() - { - if (!is_null($this->_websites)) { - return $this->_websites; - } - - $this->_websites = array( - 0 => array( - 'name' => Mage::helper('catalog')->__('All Websites'), - 'currency' => Mage::app()->getBaseCurrencyCode() - ) - ); - - if (!$this->isScopeGlobal() && $this->getProduct()->getStoreId()) { - /* @var $website Mage_Core_Model_Website */ - $website = Mage::app()->getStore($this->getProduct()->getStoreId())->getWebsite(); - - $this->_websites[$website->getId()] = array( - 'name' => $website->getName(), - 'currency' => $website->getBaseCurrencyCode() - ); - } else if (!$this->isScopeGlobal()) { - $websites = Mage::app()->getWebsites(false); - $productWebsiteIds = $this->getProduct()->getWebsiteIds(); - foreach ($websites as $website) { - /* @var $website Mage_Core_Model_Website */ - if (!in_array($website->getId(), $productWebsiteIds)) { - continue; - } - $this->_websites[$website->getId()] = array( - 'name' => $website->getName(), - 'currency' => $website->getBaseCurrencyCode() - ); - } - } - - return $this->_websites; - } - - /** - * Retrieve default value for customer group - * - * @return int - */ - public function getDefaultCustomerGroup() - { - return Mage_Customer_Model_Group::CUST_GROUP_ALL; - } - - /** - * Retrieve default value for website - * - * @return int - */ - public function getDefaultWebsite() - { - if ($this->isShowWebsiteColumn() && !$this->isAllowChangeWebsite()) { - return Mage::app()->getStore($this->getProduct()->getStoreId())->getWebsiteId(); - } - return 0; - } - /** * Prepare global layout * Add "Add tier" button to layout @@ -280,9 +97,9 @@ protected function _prepareLayout() { $button = $this->getLayout()->createBlock('adminhtml/widget_button') ->setData(array( - 'label' => Mage::helper('catalog')->__('Add Tier'), - 'onclick' => 'return tierPriceControl.addItem()', - 'class' => 'add' + 'label' => Mage::helper('catalog')->__('Add Tier'), + 'onclick' => 'return tierPriceControl.addItem()', + 'class' => 'add' )); $button->setName('add_tier_price_item_button'); @@ -290,93 +107,4 @@ protected function _prepareLayout() return parent::_prepareLayout(); } - /** - * Retrieve Add Tier Price Item button HTML - * - * @return string - */ - public function getAddButtonHtml() - { - return $this->getChildHtml('add_button'); - } - - /** - * Returns customized price column header - * that was seted through set... - * - * @param string $default - * @return string - */ - public function getPriceColumnHeader($default) - { - if ($this->hasData('price_column_header')) { - return $this->getData('price_column_header'); - } else { - return $default; - } - } - - /** - * Returns customized price column header - * that was seted through set... - * - * @param string $default - * @return string - */ - public function getPriceValidation($default) - { - if ($this->hasData('price_validation')) { - return $this->getData('price_validation'); - } else { - return $default; - } - } - - /** - * Retrieve Tier Price entity attribute - * - * @return Mage_Catalog_Model_Resource_Eav_Attribute - */ - public function getAttribute() - { - return $this->getElement()->getEntityAttribute(); - } - - /** - * Check tier price attribute scope is global - * - * @return bool - */ - public function isScopeGlobal() - { - return $this->getAttribute()->isScopeGlobal(); - } - - /** - * Show tier prices grid website column - * - * @return bool - */ - public function isShowWebsiteColumn() - { - if ($this->isScopeGlobal()) { - return false; - } else if (Mage::app()->isSingleStoreMode()) { - return false; - } - return true; - } - - /** - * Check is allow change website value for combination - * - * @return bool - */ - public function isAllowChangeWebsite() - { - if (!$this->isShowWebsiteColumn() || $this->getProduct()->getStoreId()) { - return false; - } - return true; - } } diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Super/Config.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Super/Config.php index 31a6a229ee..19a5dd01a8 100644 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Super/Config.php +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Super/Config.php @@ -66,7 +66,28 @@ public function getTabClass() */ public function isReadonly() { - return $this->_getProduct()->getCompositeReadonly(); + return (bool) $this->_getProduct()->getCompositeReadonly(); + } + + /** + * Check whether attributes of configurable products can be editable + * + * @return boolean + */ + public function isAttributesConfigurationReadonly() + { + return (bool) $this->_getProduct()->getAttributesConfigurationReadonly(); + } + + /** + * Check whether prices of configurable products can be editable + * + * @return boolean + */ + public function isAttributesPricesReadonly() + { + return $this->_getProduct()->getAttributesConfigurationReadonly() || + (Mage::helper('catalog')->isPriceGlobal() && $this->isReadonly()); } /** diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Super/Config/Simple.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Super/Config/Simple.php index f37f5ed297..e9bc8ac3ff 100644 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Super/Config/Simple.php +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Super/Config/Simple.php @@ -31,7 +31,8 @@ * @package Mage_Adminhtml * @author Magento Core Team */ -class Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Super_Config_Simple extends Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Attributes +class Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Super_Config_Simple + extends Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Attributes { /** * Link to currently editing product @@ -56,7 +57,7 @@ protected function _prepareForm() 'additional' => array('name', 'sku', 'visibility', 'status') ); - $availableTypes = array('text', 'select', 'multiselect', 'textarea', 'price'); + $availableTypes = array('text', 'select', 'multiselect', 'textarea', 'price', 'weight'); $attributes = Mage::getModel('catalog/product') ->setTypeId(Mage_Catalog_Model_Product_Type::TYPE_SIMPLE) @@ -70,10 +71,12 @@ protected function _prepareForm() // If not applied to configurable && !in_array(Mage_Catalog_Model_Product_Type::TYPE_CONFIGURABLE, $attribute->getApplyTo()) // If not used in configurable - && !in_array($attribute->getId(),$this->_getProduct()->getTypeInstance(true)->getUsedProductAttributeIds($this->_getProduct()))) + && !in_array($attribute->getId(), + $this->_getProduct()->getTypeInstance(true)->getUsedProductAttributeIds($this->_getProduct())) + ) // Or in additional - || in_array($attribute->getAttributeCode(), $attributesConfig['additional'])) { - + || in_array($attribute->getAttributeCode(), $attributesConfig['additional']) + ) { $inputType = $attribute->getFrontend()->getInputType(); if (!in_array($inputType, $availableTypes)) { continue; @@ -112,7 +115,8 @@ protected function _prepareForm() } /* Configurable attributes */ - foreach ($this->_getProduct()->getTypeInstance(true)->getUsedProductAttributes($this->_getProduct()) as $attribute) { + $usedAttributes = $this->_getProduct()->getTypeInstance(true)->getUsedProductAttributes($this->_getProduct()); + foreach ($usedAttributes as $attribute) { $attributeCode = $attribute->getAttributeCode(); $fieldset->addField( 'simple_product_' . $attributeCode, 'select', array( 'label' => $attribute->getFrontend()->getLabel(), @@ -176,8 +180,6 @@ protected function _prepareForm() ) )); - - $this->setForm($form); } diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Grid.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Grid.php index 29699db2ab..25cb6473e5 100644 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Grid.php +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Grid.php @@ -73,11 +73,46 @@ protected function _prepareCollection() //$collection->setStoreId($store->getId()); $adminStore = Mage_Core_Model_App::ADMIN_STORE_ID; $collection->addStoreFilter($store); - $collection->joinAttribute('name', 'catalog_product/name', 'entity_id', null, 'inner', $adminStore); - $collection->joinAttribute('custom_name', 'catalog_product/name', 'entity_id', null, 'inner', $store->getId()); - $collection->joinAttribute('status', 'catalog_product/status', 'entity_id', null, 'inner', $store->getId()); - $collection->joinAttribute('visibility', 'catalog_product/visibility', 'entity_id', null, 'inner', $store->getId()); - $collection->joinAttribute('price', 'catalog_product/price', 'entity_id', null, 'left', $store->getId()); + $collection->joinAttribute( + 'name', + 'catalog_product/name', + 'entity_id', + null, + 'inner', + $adminStore + ); + $collection->joinAttribute( + 'custom_name', + 'catalog_product/name', + 'entity_id', + null, + 'inner', + $store->getId() + ); + $collection->joinAttribute( + 'status', + 'catalog_product/status', + 'entity_id', + null, + 'inner', + $store->getId() + ); + $collection->joinAttribute( + 'visibility', + 'catalog_product/visibility', + 'entity_id', + null, + 'inner', + $store->getId() + ); + $collection->joinAttribute( + 'price', + 'catalog_product/price', + 'entity_id', + null, + 'left', + $store->getId() + ); } else { $collection->addAttributeToSelect('price'); @@ -273,6 +308,7 @@ protected function _prepareMassaction() )); } + Mage::dispatchEvent('adminhtml_catalog_product_grid_prepare_massaction', array('block' => $this)); return $this; } diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Helper/Form/Gallery.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Helper/Form/Gallery.php index 117ff858aa..f3a0a6d8bb 100644 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Helper/Form/Gallery.php +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Helper/Form/Gallery.php @@ -92,7 +92,7 @@ public function usedDefault($attribute) if (!$this->getDataObject()->getExistsStoreValueFlag($attributeCode)) { return true; - } else if ($this->getValue() == $defaultValue && + } else if ($this->getValue() == $defaultValue && $this->getDataObject()->getStoreId() != $this->_getDefaultStoreId()) { return false; } @@ -118,13 +118,11 @@ public function getScopeLabel($attribute) } if ($attribute->isScopeGlobal()) { - $html.= '
[GLOBAL]'; - } - elseif ($attribute->isScopeWebsite()) { - $html.= '
[WEBSITE]'; - } - elseif ($attribute->isScopeStore()) { - $html.= '
[STORE VIEW]'; + $html .= '
' . Mage::helper('adminhtml')->__('[GLOBAL]'); + } elseif ($attribute->isScopeWebsite()) { + $html .= '
' . Mage::helper('adminhtml')->__('[WEBSITE]'); + } elseif ($attribute->isScopeStore()) { + $html .= '
' . Mage::helper('adminhtml')->__('[STORE VIEW]'); } return $html; } diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Helper/Form/Weight.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Helper/Form/Weight.php new file mode 100644 index 0000000000..cd2819c8b8 --- /dev/null +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Helper/Form/Weight.php @@ -0,0 +1,45 @@ + + */ +class Mage_Adminhtml_Block_Catalog_Product_Helper_Form_Weight extends Varien_Data_Form_Element_Text +{ + /* + * Add validate-zero-or-greater css class to weigh field + * for input validation + */ + public function __construct($attributes=array()) + { + parent::__construct($attributes); + $this->addClass('validate-zero-or-greater'); + } +} diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Widget/Chooser.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Widget/Chooser.php index b986a9d212..37e25698a3 100644 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Widget/Chooser.php +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Product/Widget/Chooser.php @@ -100,8 +100,8 @@ public function prepareElementHtml(Varien_Data_Form_Element_Abstract $element) public function getCheckboxCheckCallback() { if ($this->getUseMassaction()) { - return "function (grid, event) { - $(grid.containerId).fire('product:changed', {}); + return "function (grid, element) { + $(grid.containerId).fire('product:changed', {element: element}); }"; } } diff --git a/app/code/core/Mage/Adminhtml/Block/Catalog/Search/Edit/Form.php b/app/code/core/Mage/Adminhtml/Block/Catalog/Search/Edit/Form.php index 74d01c7022..bec9f801df 100644 --- a/app/code/core/Mage/Adminhtml/Block/Catalog/Search/Edit/Form.php +++ b/app/code/core/Mage/Adminhtml/Block/Catalog/Search/Edit/Form.php @@ -104,48 +104,41 @@ protected function _prepareForm() if ($model->getId()) { $fieldset->addField('num_results', 'text', array( - 'name' => 'num_results', - 'label' => Mage::helper('catalog')->__('Number of results
(For the last time placed)'), - 'title' => Mage::helper('catalog')->__('Number of results
(For the last time placed)'), - 'required' => true, + 'name' => 'num_results', + 'label' => Mage::helper('catalog')->__('Number of results'), + 'title' => Mage::helper('catalog')->__('Number of results (For the last time placed)'), + 'note' => Mage::helper('catalog')->__('For the last time placed.'), + 'required' => true, )); $fieldset->addField('popularity', 'text', array( - 'name' => 'popularity', - 'label' => Mage::helper('catalog')->__('Number of Uses'), - 'title' => Mage::helper('catalog')->__('Number of Uses'), - 'required' => true, + 'name' => 'popularity', + 'label' => Mage::helper('catalog')->__('Number of Uses'), + 'title' => Mage::helper('catalog')->__('Number of Uses'), + 'required' => true, )); } - $afterElementHtml = '

' - . Mage::helper('catalog')->__('(Will make search for the query above return results for this search.)') - . '

'; - $fieldset->addField('synonym_for', 'text', array( - 'name' => 'synonym_for', - 'label' => Mage::helper('catalog')->__('Synonym For'), - 'title' => Mage::helper('catalog')->__('Synonym For'), - 'after_element_html' => $afterElementHtml, + 'name' => 'synonym_for', + 'label' => Mage::helper('catalog')->__('Synonym For'), + 'title' => Mage::helper('catalog')->__('Synonym For'), + 'note' => Mage::helper('catalog')->__('Will make search for the query above return results for this search.'), )); - $afterElementHtml = '

' - . Mage::helper('catalog')->__('ex. http://domain.com') - . '

'; - $fieldset->addField('redirect', 'text', array( - 'name' => 'redirect', - 'label' => Mage::helper('catalog')->__('Redirect URL'), - 'title' => Mage::helper('catalog')->__('Redirect URL'), - 'class' => 'validate-url', - 'after_element_html' => $afterElementHtml, + 'name' => 'redirect', + 'label' => Mage::helper('catalog')->__('Redirect URL'), + 'title' => Mage::helper('catalog')->__('Redirect URL'), + 'class' => 'validate-url', + 'note' => Mage::helper('catalog')->__('ex. http://domain.com'), )); $fieldset->addField('display_in_terms', 'select', array( - 'name' => 'display_in_terms', - 'label' => Mage::helper('catalog')->__('Display in Suggested Terms'), - 'title' => Mage::helper('catalog')->__('Display in Suggested Terms'), - 'values' => $yesno, + 'name' => 'display_in_terms', + 'label' => Mage::helper('catalog')->__('Display in Suggested Terms'), + 'title' => Mage::helper('catalog')->__('Display in Suggested Terms'), + 'values' => $yesno, )); $form->setValues($model->getData()); diff --git a/app/code/core/Mage/Adminhtml/Block/Checkout/Agreement.php b/app/code/core/Mage/Adminhtml/Block/Checkout/Agreement.php index 8e27585e2e..ec788e8e95 100644 --- a/app/code/core/Mage/Adminhtml/Block/Checkout/Agreement.php +++ b/app/code/core/Mage/Adminhtml/Block/Checkout/Agreement.php @@ -37,7 +37,7 @@ class Mage_Adminhtml_Block_Checkout_Agreement extends Mage_Adminhtml_Block_Widge public function __construct() { $this->_controller = 'checkout_agreement'; - $this->_headerText = Mage::helper('checkout')->__('Manage Checkout Terms and Conditions'); + $this->_headerText = Mage::helper('checkout')->__('Manage Terms and Conditions'); $this->_addButtonLabel = Mage::helper('checkout')->__('Add New Condition'); parent::__construct(); } diff --git a/app/code/core/Mage/Adminhtml/Block/Checkout/Agreement/Edit.php b/app/code/core/Mage/Adminhtml/Block/Checkout/Agreement/Edit.php index b433304825..216c4490d1 100644 --- a/app/code/core/Mage/Adminhtml/Block/Checkout/Agreement/Edit.php +++ b/app/code/core/Mage/Adminhtml/Block/Checkout/Agreement/Edit.php @@ -60,7 +60,7 @@ public function getHeaderText() return Mage::helper('checkout')->__('Edit Terms and Conditions'); } else { - return Mage::helper('checkout')->__('New Condition'); + return Mage::helper('checkout')->__('New Terms and Conditions'); } } } diff --git a/app/code/core/Mage/Adminhtml/Block/Cms/Page/Edit.php b/app/code/core/Mage/Adminhtml/Block/Cms/Page/Edit.php index 89f40cc9e7..3f592c1585 100644 --- a/app/code/core/Mage/Adminhtml/Block/Cms/Page/Edit.php +++ b/app/code/core/Mage/Adminhtml/Block/Cms/Page/Edit.php @@ -29,14 +29,18 @@ * * @category Mage * @package Mage_Adminhtml - * @author Magento Core Team + * @author Magento Core Team */ class Mage_Adminhtml_Block_Cms_Page_Edit extends Mage_Adminhtml_Block_Widget_Form_Container { - + /** + * Initialize cms page edit block + * + * @return void + */ public function __construct() { - $this->_objectId = 'page_id'; + $this->_objectId = 'page_id'; $this->_controller = 'cms_page'; parent::__construct(); @@ -57,7 +61,6 @@ public function __construct() } else { $this->_removeButton('delete'); } - } /** @@ -95,24 +98,26 @@ protected function _isAllowedAction($action) protected function _getSaveAndContinueUrl() { return $this->getUrl('*/*/save', array( - '_current' => true, - 'back' => 'edit', - 'active_tab' => '{{tab_id}}' + '_current' => true, + 'back' => 'edit', + 'active_tab' => '{{tab_id}}' )); } /** - * @see Mage_Adminhtml_Block_Widget_Container::_prepareLayout() + * Prepare layout + * + * @return Mage_Core_Block_Abstract */ protected function _prepareLayout() { $tabsBlock = $this->getLayout()->getBlock('cms_page_edit_tabs'); if ($tabsBlock) { $tabsBlockJsObject = $tabsBlock->getJsObjectName(); - $tabsBlockPrefix = $tabsBlock->getId() . '_'; + $tabsBlockPrefix = $tabsBlock->getId() . '_'; } else { $tabsBlockJsObject = 'page_tabsJsTabs'; - $tabsBlockPrefix = 'page_tabs_'; + $tabsBlockPrefix = 'page_tabs_'; } $this->_formScripts[] = " @@ -137,6 +142,4 @@ function saveAndContinueEdit(urlTemplate) { "; return parent::_prepareLayout(); } - - } diff --git a/app/code/core/Mage/Adminhtml/Block/Customer/Edit/Renderer/Attribute/Group.php b/app/code/core/Mage/Adminhtml/Block/Customer/Edit/Renderer/Attribute/Group.php new file mode 100644 index 0000000000..bd6a631514 --- /dev/null +++ b/app/code/core/Mage/Adminhtml/Block/Customer/Edit/Renderer/Attribute/Group.php @@ -0,0 +1,105 @@ + + */ +class Mage_Adminhtml_Block_Customer_Edit_Renderer_Attribute_Group + extends Mage_Adminhtml_Block_Widget_Form_Renderer_Fieldset_Element +{ + /** + * Override parent constructor just for setting custom template + */ + protected function _construct() + { + parent::_construct(); + $this->setTemplate('customer/edit/tab/account/form/renderer/group.phtml'); + } + + /** + * Retrieve disable auto group change element HTML ID + * + * @return string + */ + protected function _getDisableAutoGroupChangeElementHtmlId() + { + return $this->getDisableAutoGroupChangeAttribute()->getAttributeCode(); + } + + /** + * Retrieve disable auto group change checkbox label text + * + * @return string + */ + public function getDisableAutoGroupChangeCheckboxLabel() + { + return Mage::helper('customer')->__($this->getDisableAutoGroupChangeAttribute()->getFrontend()->getLabel()); + } + + /** + * Retrieve disable auto group change checkbox state + * + * @return string + */ + public function getDisableAutoGroupChangeCheckboxState() + { + $customer = Mage::registry('current_customer'); + $checkedByDefault = ($customer && $customer->getId()) + ? false : Mage::helper('customer/address')->getDisableAutoGroupAssignDefaultValue(); + + $value = $this->getDisableAutoGroupChangeAttributeValue(); + $state = ''; + if (!empty($value) || $checkedByDefault) { + $state = 'checked'; + } + return $state; + } + + /** + * Retrieve disable auto group change checkbox element HTML NAME + * + * @return string + */ + public function getDisableAutoGroupChangeCheckboxElementName() + { + return $this->getElement()->getForm()->getFieldNameSuffix() + . '[' . $this->_getDisableAutoGroupChangeElementHtmlId() . ']'; + } + + /** + * Retrieve disable auto group change checkbox element HTML ID + * + * @return string + */ + public function getDisableAutoGroupChangeCheckboxElementId() + { + return $this->_getDisableAutoGroupChangeElementHtmlId(); + } +} diff --git a/app/code/core/Mage/Adminhtml/Block/Customer/Edit/Tab/Account.php b/app/code/core/Mage/Adminhtml/Block/Customer/Edit/Tab/Account.php index 0776993c84..8e33cc0ffd 100644 --- a/app/code/core/Mage/Adminhtml/Block/Customer/Edit/Tab/Account.php +++ b/app/code/core/Mage/Adminhtml/Block/Customer/Edit/Tab/Account.php @@ -33,11 +33,19 @@ */ class Mage_Adminhtml_Block_Customer_Edit_Tab_Account extends Mage_Adminhtml_Block_Widget_Form { + /** + * Initialize block + */ public function __construct() { parent::__construct(); } + /** + * Initialize form + * + * @return Mage_Adminhtml_Block_Customer_Edit_Tab_Account + */ public function initForm() { $form = new Varien_Data_Form(); @@ -46,36 +54,60 @@ public function initForm() $customer = Mage::registry('current_customer'); - /* @var $customerForm Mage_Customer_Model_Form */ + /** @var $customerForm Mage_Customer_Model_Form */ $customerForm = Mage::getModel('customer/form'); $customerForm->setEntity($customer) ->setFormCode('adminhtml_customer') ->initDefaultValues(); - $fieldset = $form->addFieldset('base_fieldset', - array('legend'=>Mage::helper('customer')->__('Account Information')) - ); + $fieldset = $form->addFieldset('base_fieldset', array( + 'legend' => Mage::helper('customer')->__('Account Information') + )); $attributes = $customerForm->getAttributes(); foreach ($attributes as $attribute) { $attribute->unsIsVisible(); } - $this->_setFieldset($attributes, $fieldset); + + $disableAutoGroupChangeAttributeName = 'disable_auto_group_change'; + $this->_setFieldset($attributes, $fieldset, array($disableAutoGroupChangeAttributeName)); + + $form->getElement('group_id')->setRenderer($this->getLayout() + ->createBlock('adminhtml/customer_edit_renderer_attribute_group') + ->setDisableAutoGroupChangeAttribute($customerForm->getAttribute($disableAutoGroupChangeAttributeName)) + ->setDisableAutoGroupChangeAttributeValue($customer->getData($disableAutoGroupChangeAttributeName))); if ($customer->getId()) { $form->getElement('website_id')->setDisabled('disabled'); $form->getElement('created_in')->setDisabled('disabled'); } else { $fieldset->removeField('created_in'); - } + $form->getElement('website_id')->addClass('validate-website-has-store'); -// if (Mage::app()->isSingleStoreMode()) { -// $fieldset->removeField('website_id'); -// $fieldset->addField('website_id', 'hidden', array( -// 'name' => 'website_id' -// )); -// $customer->setWebsiteId(Mage::app()->getStore(true)->getWebsiteId()); -// } + $websites = array(); + foreach (Mage::app()->getWebsites(true) as $website) { + $websites[$website->getId()] = !is_null($website->getDefaultStore()); + } + $prefix = $form->getHtmlIdPrefix(); + + $form->getElement('website_id')->setAfterElementHtml( + '' + ); + } $customerStoreId = null; if ($customer->getId()) { @@ -119,10 +151,10 @@ public function initForm() if ($customer->getId()) { if (!$customer->isReadonly()) { - // add password management fieldset + // Add password management fieldset $newFieldset = $form->addFieldset( 'password_fieldset', - array('legend'=>Mage::helper('customer')->__('Password Management')) + array('legend' => Mage::helper('customer')->__('Password Management')) ); // New customer password $field = $newFieldset->addField('new_password', 'text', @@ -134,7 +166,7 @@ public function initForm() ); $field->setRenderer($this->getLayout()->createBlock('adminhtml/customer_edit_renderer_newpass')); - // prepare customer confirmation control (only for existing customers) + // Prepare customer confirmation control (only for existing customers) $confirmationKey = $customer->getConfirmation(); if ($confirmationKey || $customer->isConfirmationRequired()) { $confirmationAttribute = $customer->getAttribute('confirmation'); @@ -147,8 +179,8 @@ public function initForm() ))->setEntityAttribute($confirmationAttribute) ->setValues(array('' => 'Confirmed', $confirmationKey => 'Not confirmed')); - // prepare send welcome email checkbox, if customer is not confirmed - // no need to add it, if website id is empty + // Prepare send welcome email checkbox if customer is not confirmed + // no need to add it, if website ID is empty if ($customer->getConfirmation() && $customer->getWebsiteId()) { $fieldset->addField('sendemail', 'checkbox', array( 'name' => 'sendemail', @@ -173,7 +205,7 @@ public function initForm() ); $field->setRenderer($this->getLayout()->createBlock('adminhtml/customer_edit_renderer_newpass')); - // prepare send welcome email checkbox + // Prepare send welcome email checkbox $fieldset->addField('sendemail', 'checkbox', array( 'label' => Mage::helper('customer')->__('Send Welcome Email'), 'name' => 'sendemail', @@ -189,7 +221,7 @@ public function initForm() } } - // make sendemail and sendmail_store_id disabled, if website_id has empty value + // Make sendemail and sendmail_store_id disabled if website_id has empty value $isSingleMode = Mage::app()->isSingleStoreMode(); $sendEmailId = $isSingleMode ? 'sendemail' : 'sendemail_store_id'; $sendEmail = $form->getElement($sendEmailId); diff --git a/app/code/core/Mage/Adminhtml/Block/Customer/Edit/Tab/Cart.php b/app/code/core/Mage/Adminhtml/Block/Customer/Edit/Tab/Cart.php index a1aa43d28f..82830f1d57 100644 --- a/app/code/core/Mage/Adminhtml/Block/Customer/Edit/Tab/Cart.php +++ b/app/code/core/Mage/Adminhtml/Block/Customer/Edit/Tab/Cart.php @@ -37,12 +37,22 @@ class Mage_Adminhtml_Block_Customer_Edit_Tab_Cart extends Mage_Adminhtml_Block_W public function __construct($attributes=array()) { parent::__construct($attributes); - $this->setId('customer_cart_grid'.$this->getWebsiteId()); $this->setUseAjax(true); $this->_parentTemplate = $this->getTemplate(); $this->setTemplate('customer/tab/cart.phtml'); } + /** + * Prepare grid + * + * @return void + */ + protected function _prepareGrid() + { + $this->setId('customer_cart_grid' . $this->getWebsiteId()); + parent::_prepareGrid(); + } + protected function _prepareCollection() { $customer = Mage::registry('current_customer'); diff --git a/app/code/core/Mage/Adminhtml/Block/Customer/Edit/Tab/Newsletter.php b/app/code/core/Mage/Adminhtml/Block/Customer/Edit/Tab/Newsletter.php index cc33cc14a6..286fcbeadd 100644 --- a/app/code/core/Mage/Adminhtml/Block/Customer/Edit/Tab/Newsletter.php +++ b/app/code/core/Mage/Adminhtml/Block/Customer/Edit/Tab/Newsletter.php @@ -48,6 +48,11 @@ public function initForm() $subscriber = Mage::getModel('newsletter/subscriber')->loadByCustomer($customer); Mage::register('subscriber', $subscriber); + if ($customer->getWebsiteId() == 0) { + $this->setForm($form); + return $this; + } + $fieldset = $form->addFieldset('base_fieldset', array('legend'=>Mage::helper('customer')->__('Newsletter Information'))); $fieldset->addField('subscription', 'checkbox', @@ -73,7 +78,6 @@ public function initForm() ); } - $this->setForm($form); return $this; } @@ -82,7 +86,10 @@ public function getStatusChangedDate() { $subscriber = Mage::registry('subscriber'); if($subscriber->getChangeStatusAt()) { - return $this->formatDate($subscriber->getChangeStatusAt(), Mage_Core_Model_Locale::FORMAT_TYPE_MEDIUM, true); + return $this->formatDate( + $subscriber->getChangeStatusAt(), + Mage_Core_Model_Locale::FORMAT_TYPE_MEDIUM, true + ); } return null; diff --git a/app/code/core/Mage/Adminhtml/Block/Customer/Edit/Tab/View.php b/app/code/core/Mage/Adminhtml/Block/Customer/Edit/Tab/View.php index 2ceb1f3892..298d3c347b 100644 --- a/app/code/core/Mage/Adminhtml/Block/Customer/Edit/Tab/View.php +++ b/app/code/core/Mage/Adminhtml/Block/Customer/Edit/Tab/View.php @@ -71,10 +71,15 @@ public function getCustomerLog() return $this->_customerLog; } + /** + * Get customer creation date + * + * @return string + */ public function getCreateDate() { - $date = Mage::app()->getLocale()->date($this->getCustomer()->getCreatedAtTimestamp()); - return $this->formatDate($date, Mage_Core_Model_Locale::FORMAT_TYPE_MEDIUM, true); + return Mage::helper('core')->formatDate($this->getCustomer()->getCreatedAtTimestamp(), + Mage_Core_Model_Locale::FORMAT_TYPE_MEDIUM, true); } public function getStoreCreateDate() @@ -93,11 +98,16 @@ public function getStoreCreateDateTimezone() ->getConfig(Mage_Core_Model_Locale::XML_PATH_DEFAULT_TIMEZONE); } + /** + * Get customer last login date + * + * @return string + */ public function getLastLoginDate() { - if ($date = $this->getCustomerLog()->getLoginAtTimestamp()) { - $date = Mage::app()->getLocale()->date($date); - return $this->formatDate($date, Mage_Core_Model_Locale::FORMAT_TYPE_MEDIUM, true); + $date = $this->getCustomerLog()->getLoginAtTimestamp(); + if ($date) { + return Mage::helper('core')->formatDate($date, Mage_Core_Model_Locale::FORMAT_TYPE_MEDIUM, true); } return Mage::helper('customer')->__('Never'); } diff --git a/app/code/core/Mage/Adminhtml/Block/Customer/Edit/Tab/View/Accordion.php b/app/code/core/Mage/Adminhtml/Block/Customer/Edit/Tab/View/Accordion.php index a8cd1356c5..7e1d587644 100644 --- a/app/code/core/Mage/Adminhtml/Block/Customer/Edit/Tab/View/Accordion.php +++ b/app/code/core/Mage/Adminhtml/Block/Customer/Edit/Tab/View/Accordion.php @@ -58,9 +58,7 @@ protected function _prepareLayout() // prepare title for cart $title = Mage::helper('customer')->__('Shopping Cart - %d item(s)', $cartItemsCount); if (count($customer->getSharedWebsiteIds()) > 1) { - $title = Mage::helper('customer')->__('Shopping Cart of %1$s - %2$d item(s)', - $website->getName(), $cartItemsCount - ); + $title = Mage::helper('customer')->__('Shopping Cart of %1$s - %2$d item(s)', $website->getName(), $cartItemsCount); } // add cart ajax accordion diff --git a/app/code/core/Mage/Adminhtml/Block/Customer/Group/Edit/Form.php b/app/code/core/Mage/Adminhtml/Block/Customer/Group/Edit/Form.php index 5466380aec..7373461523 100644 --- a/app/code/core/Mage/Adminhtml/Block/Customer/Group/Edit/Form.php +++ b/app/code/core/Mage/Adminhtml/Block/Customer/Group/Edit/Form.php @@ -51,8 +51,7 @@ protected function _prepareLayout() 'name' => 'code', 'label' => Mage::helper('customer')->__('Group Name'), 'title' => Mage::helper('customer')->__('Group Name'), - 'note' => Mage::helper('customer')->__('Maximum length must be less then %s symbols', - Mage_Customer_Model_Group::GROUP_CODE_MAX_LENGTH), + 'note' => Mage::helper('customer')->__('Maximum length must be less then %s symbols', Mage_Customer_Model_Group::GROUP_CODE_MAX_LENGTH), 'class' => $validateClass, 'required' => true, ) diff --git a/app/code/core/Mage/Adminhtml/Block/Customer/Sales/Order/Address/Form/Billing/Renderer/Vat.php b/app/code/core/Mage/Adminhtml/Block/Customer/Sales/Order/Address/Form/Billing/Renderer/Vat.php new file mode 100644 index 0000000000..607a1fe9af --- /dev/null +++ b/app/code/core/Mage/Adminhtml/Block/Customer/Sales/Order/Address/Form/Billing/Renderer/Vat.php @@ -0,0 +1,97 @@ + + */ +class Mage_Adminhtml_Block_Customer_Sales_Order_Address_Form_Billing_Renderer_Vat + extends Mage_Adminhtml_Block_Widget_Form_Renderer_Fieldset_Element +{ + /** + * Validate button block + * + * @var null|Mage_Adminhtml_Block_Widget_Button + */ + protected $_validateButton = null; + + /** + * Set custom template for 'VAT number' + */ + protected function _construct() + { + $this->setTemplate('customer/sales/order/create/billing/form/renderer/vat.phtml'); + } + + /** + * Retrieve validate button block + * + * @return Mage_Adminhtml_Block_Widget_Button + */ + public function getValidateButton() + { + if (is_null($this->_validateButton)) { + /** @var $form Varien_Data_Form */ + $form = $this->_element->getForm(); + + $vatElementId = $this->_element->getHtmlId(); + + /** @var $formAccountBlock Mage_Adminhtml_Block_Sales_Order_Create_Form_Account */ + $formAccountBlock = $this->getLayout()->getBlock('form_account'); + $groupIdHtmlId = $formAccountBlock->getForm()->getElement('group_id')->getHtmlId(); + + $countryElementId = $form->getElement('country_id')->getHtmlId(); + $validateUrl = Mage::getSingleton('adminhtml/url') + ->getUrl('*/customer_system_config_validatevat/validateAdvanced'); + + $vatValidateOptions = Mage::helper('core')->jsonEncode(array( + 'vatElementId' => $vatElementId, + 'countryElementId' => $countryElementId, + 'groupIdHtmlId' => $groupIdHtmlId, + 'validateUrl' => $validateUrl, + 'vatValidMessage' => Mage::helper('customer')->__('The VAT ID is valid. The current Customer Group will be used.'), + 'vatValidAndGroupChangeMessage' => + Mage::helper('customer')->__('Based on the VAT ID, the customer would belong to Customer Group %s.') . "\n" + . Mage::helper('customer')->__('The customer is currently assigned to Customer Group %s.') . ' ' + . Mage::helper('customer')->__('Would you like to change the Customer Group for this order?'), + 'vatInvalidMessage' => Mage::helper('customer')->__('The VAT ID entered (%s) is not valid VAT ID.'), + 'vatValidationFailedMessage' => Mage::helper('customer')->__('There was an error validating the VAT ID. Please try again later.'), + )); + + $beforeHtml = ''; + $this->_validateButton = $this->getLayout()->createBlock('adminhtml/widget_button')->setData(array( + 'label' => Mage::helper('customer')->__('Validate VAT Number'), + 'before_html' => $beforeHtml, + 'onclick' => "order.validateVat(vatValidateOptions)" + )); + } + return $this->_validateButton; + } +} diff --git a/app/code/core/Mage/Adminhtml/Block/Customer/System/Config/Validatevat.php b/app/code/core/Mage/Adminhtml/Block/Customer/System/Config/Validatevat.php new file mode 100644 index 0000000000..43d675169a --- /dev/null +++ b/app/code/core/Mage/Adminhtml/Block/Customer/System/Config/Validatevat.php @@ -0,0 +1,79 @@ + + */ +class Mage_Adminhtml_Block_Customer_System_Config_Validatevat extends Mage_Adminhtml_Block_System_Config_Form_Field +{ + /** + * Set template to itself + * + * @return Mage_Adminhtml_Block_Customer_System_Config_Validatevat + */ + protected function _prepareLayout() + { + parent::_prepareLayout(); + if (!$this->getTemplate()) { + $this->setTemplate('customer/system/config/validatevat.phtml'); + } + return $this; + } + + /** + * Unset some non-related element parameters + * + * @param Varien_Data_Form_Element_Abstract $element + * @return string + */ + public function render(Varien_Data_Form_Element_Abstract $element) + { + $element->unsScope()->unsCanUseWebsiteValue()->unsCanUseDefaultValue(); + return parent::render($element); + } + + /** + * Get the button and scripts contents + * + * @param Varien_Data_Form_Element_Abstract $element + * @return string + */ + protected function _getElementHtml(Varien_Data_Form_Element_Abstract $element) + { + $originalData = $element->getOriginalData(); + $this->addData(array( + 'button_label' => Mage::helper('customer')->__($originalData['button_label']), + 'html_id' => $element->getHtmlId(), + 'ajax_url' => Mage::getSingleton('adminhtml/url')->getUrl('*/customer_system_config_validatevat/validate') + )); + + return $this->_toHtml(); + } +} diff --git a/app/code/core/Mage/Adminhtml/Block/Media/Uploader.php b/app/code/core/Mage/Adminhtml/Block/Media/Uploader.php index 1eff3369e0..177f6ec3c0 100644 --- a/app/code/core/Mage/Adminhtml/Block/Media/Uploader.php +++ b/app/code/core/Mage/Adminhtml/Block/Media/Uploader.php @@ -195,11 +195,12 @@ public function getDataMaxSizeInBytes() } /** - * Retrive full uploader SWF's file URL + * Retrieve full uploader SWF's file URL * Implemented to solve problem with cross domain SWFs * Now uploader can be only in the same URL where backend located * - * @param string url to uploader in current theme + * @param string $url url to uploader in current theme + * * @return string full URL */ public function getUploaderUrl($url) @@ -212,7 +213,7 @@ public function getUploaderUrl($url) if (empty($url) || !$design->validateFile($url, array('_type' => 'skin', '_theme' => $theme))) { $theme = $design->getDefaultTheme(); } - return Mage::getBaseUrl(Mage_Core_Model_Store::URL_TYPE_SKIN) . + return Mage::getBaseUrl(Mage_Core_Model_Store::URL_TYPE_WEB) . 'skin/' . $design->getArea() . '/' . $design->getPackageName() . '/' . $theme . '/' . $url; } } diff --git a/app/code/core/Mage/Adminhtml/Block/Promo/Quote/Edit/Tab/Coupons.php b/app/code/core/Mage/Adminhtml/Block/Promo/Quote/Edit/Tab/Coupons.php new file mode 100644 index 0000000000..2511c15374 --- /dev/null +++ b/app/code/core/Mage/Adminhtml/Block/Promo/Quote/Edit/Tab/Coupons.php @@ -0,0 +1,88 @@ + + */ +class Mage_Adminhtml_Block_Promo_Quote_Edit_Tab_Coupons + extends Mage_Adminhtml_Block_Text_List + implements Mage_Adminhtml_Block_Widget_Tab_Interface +{ + /** + * Prepare content for tab + * + * @return string + */ + public function getTabLabel() + { + return Mage::helper('salesrule')->__('Manage Coupons Codes'); + } + + /** + * Prepare title for tab + * + * @return string + */ + public function getTabTitle() + { + return Mage::helper('salesrule')->__('Manage Coupons Codes'); + } + + /** + * Returns status flag about this tab can be shown or not + * + * @return bool + */ + public function canShowTab() + { + return $this->_isEditing(); + } + + /** + * Returns status flag about this tab hidden or not + * + * @return bool + */ + public function isHidden() + { + return !$this->_isEditing(); + } + + /** + * Check whether we edit existing rule or adding new one + * + * @return bool + */ + protected function _isEditing() + { + $priceRule = Mage::registry('current_promo_quote_rule'); + return !is_null($priceRule->getRuleId()); + } +} diff --git a/app/code/core/Mage/Adminhtml/Block/Promo/Quote/Edit/Tab/Coupons/Form.php b/app/code/core/Mage/Adminhtml/Block/Promo/Quote/Edit/Tab/Coupons/Form.php new file mode 100644 index 0000000000..99c8fc8a3e --- /dev/null +++ b/app/code/core/Mage/Adminhtml/Block/Promo/Quote/Edit/Tab/Coupons/Form.php @@ -0,0 +1,143 @@ + + */ +class Mage_Adminhtml_Block_Promo_Quote_Edit_Tab_Coupons_Form + extends Mage_Adminhtml_Block_Widget_Form +{ + /** + * Prepare coupon codes generation parameters form + * + * @return Mage_Adminhtml_Block_Widget_Form + */ + protected function _prepareForm() + { + $form = new Varien_Data_Form(); + + /** + * @var Mage_SalesRule_Helper_Coupon $couponHelper + */ + $couponHelper = Mage::helper('salesrule/coupon'); + + $model = Mage::registry('current_promo_quote_rule'); + $ruleId = $model->getId(); + + $form->setHtmlIdPrefix('coupons_'); + + $gridBlock = $this->getLayout()->getBlock('promo_quote_edit_tab_coupons_grid'); + $gridBlockJsObject = ''; + if ($gridBlock) { + $gridBlockJsObject = $gridBlock->getJsObjectName(); + } + + $fieldset = $form->addFieldset('information_fieldset', array('legend'=>Mage::helper('salesrule')->__('Coupons Information'))); + $fieldset->addClass('ignore-validate'); + + $fieldset->addField('rule_id', 'hidden', array( + 'name' => 'rule_id', + 'value' => $ruleId + )); + + $fieldset->addField('qty', 'text', array( + 'name' => 'qty', + 'label' => Mage::helper('salesrule')->__('Coupon Qty'), + 'title' => Mage::helper('salesrule')->__('Coupon Qty'), + 'required' => true + )); + + $fieldset->addField('length', 'text', array( + 'name' => 'length', + 'label' => Mage::helper('salesrule')->__('Code Length'), + 'title' => Mage::helper('salesrule')->__('Code Length'), + 'required' => true, + 'note' => Mage::helper('salesrule')->__('Excluding prefix, suffix and separators.'), + 'value' => $couponHelper->getDefaultLength() + )); + + $fieldset->addField('format', 'select', array( + 'label' => Mage::helper('salesrule')->__('Code Format'), + 'name' => 'format', + 'options' => $couponHelper->getFormatsList(), + 'required' => true, + 'value' => $couponHelper->getDefaultFormat() + )); + + $fieldset->addField('prefix', 'text', array( + 'name' => 'prefix', + 'label' => Mage::helper('salesrule')->__('Code Prefix'), + 'title' => Mage::helper('salesrule')->__('Code Prefix'), + 'value' => $couponHelper->getDefaultPrefix() + )); + + $fieldset->addField('suffix', 'text', array( + 'name' => 'suffix', + 'label' => Mage::helper('salesrule')->__('Code Suffix'), + 'title' => Mage::helper('salesrule')->__('Code Suffix'), + 'value' => $couponHelper->getDefaultSuffix() + )); + + $fieldset->addField('dash', 'text', array( + 'name' => 'dash', + 'label' => Mage::helper('salesrule')->__('Dash Every X Characters'), + 'title' => Mage::helper('salesrule')->__('Dash Every X Characters'), + 'note' => Mage::helper('salesrule')->__('If empty no separation.'), + 'value' => $couponHelper->getDefaultDashInterval() + )); + + $idPrefix = $form->getHtmlIdPrefix(); + $generateUrl = $this->getGenerateUrl(); + + $fieldset->addField('generate_button', 'note', array( + 'text' => $this->getButtonHtml( + Mage::helper('salesrule')->__('Generate'), + "generateCouponCodes('{$idPrefix}' ,'{$generateUrl}', '{$gridBlockJsObject}')", + 'generate' + ) + )); + + $this->setForm($form); + + Mage::dispatchEvent('adminhtml_promo_quote_edit_tab_coupons_form_prepare_form', array('form' => $form)); + + return parent::_prepareForm(); + } + + /** + * Retrieve URL to Generate Action + * + * @return string + */ + public function getGenerateUrl() + { + return $this->getUrl('*/*/generate'); + } +} diff --git a/app/code/core/Mage/Adminhtml/Block/Promo/Quote/Edit/Tab/Coupons/Grid.php b/app/code/core/Mage/Adminhtml/Block/Promo/Quote/Edit/Tab/Coupons/Grid.php new file mode 100644 index 0000000000..8e8288b88a --- /dev/null +++ b/app/code/core/Mage/Adminhtml/Block/Promo/Quote/Edit/Tab/Coupons/Grid.php @@ -0,0 +1,144 @@ + + */ +class Mage_Adminhtml_Block_Promo_Quote_Edit_Tab_Coupons_Grid extends Mage_Adminhtml_Block_Widget_Grid +{ + /** + * Constructor + */ + public function __construct() + { + parent::__construct(); + $this->setId('couponCodesGrid'); + $this->setUseAjax(true); + } + + /** + * Prepare collection for grid + * + * @return Mage_Adminhtml_Block_Widget_Grid + */ + protected function _prepareCollection() + { + $priceRule = Mage::registry('current_promo_quote_rule'); + + /** + * @var Mage_SalesRule_Model_Resource_Coupon_Collection $collection + */ + $collection = Mage::getResourceModel('salesrule/coupon_collection') + ->addRuleToFilter($priceRule) + ->addGeneratedCouponsFilter(); + + $this->setCollection($collection); + + return parent::_prepareCollection(); + } + + /** + * Define grid columns + * + * @return Mage_Adminhtml_Block_Widget_Grid + */ + protected function _prepareColumns() + { + $this->addColumn('code', array( + 'header' => Mage::helper('salesrule')->__('Coupon Code'), + 'index' => 'code' + )); + + $this->addColumn('created_at', array( + 'header' => Mage::helper('salesrule')->__('Created On'), + 'index' => 'created_at', + 'type' => 'datetime', + 'align' => 'center', + 'width' => '160' + )); + + $this->addColumn('used', array( + 'header' => Mage::helper('salesrule')->__('Used'), + 'index' => 'times_used', + 'width' => '100', + 'type' => 'options', + 'options' => array( + Mage::helper('adminhtml')->__('No'), + Mage::helper('adminhtml')->__('Yes') + ), + 'renderer' => 'adminhtml/promo_quote_edit_tab_coupons_grid_column_renderer_used', + 'filter_condition_callback' => array( + Mage::getResourceModel('salesrule/coupon_collection'), 'addIsUsedFilterCallback' + ) + )); + + $this->addColumn('times_used', array( + 'header' => Mage::helper('salesrule')->__('Times Used'), + 'index' => 'times_used', + 'width' => '50', + 'type' => 'number', + )); + + $this->addExportType('*/*/exportCouponsCsv', Mage::helper('customer')->__('CSV')); + $this->addExportType('*/*/exportCouponsXml', Mage::helper('customer')->__('Excel XML')); + return parent::_prepareColumns(); + } + + /** + * Configure grid mass actions + * + * @return Mage_Adminhtml_Block_Promo_Quote_Edit_Tab_Coupons_Grid + */ + protected function _prepareMassaction() + { + $this->setMassactionIdField('coupon_id'); + $this->getMassactionBlock()->setFormFieldName('ids'); + $this->getMassactionBlock()->setUseAjax(true); + + $this->getMassactionBlock()->addItem('delete', array( + 'label'=> Mage::helper('adminhtml')->__('Delete'), + 'url' => $this->getUrl('*/*/couponsMassDelete', array('_current' => true)), + 'confirm' => Mage::helper('salesrule')->__('Are you sure you want to delete the selected coupon(s)?'), + 'complete' => 'refreshCouponCodesGrid' + )); + + return $this; + } + + /** + * Get grid url + * + * @return string + */ + public function getGridUrl() + { + return $this->getUrl('*/*/couponsGrid', array('_current'=> true)); + } +} diff --git a/app/code/core/Mage/Adminhtml/Block/Promo/Quote/Edit/Tab/Coupons/Grid/Column/Renderer/Used.php b/app/code/core/Mage/Adminhtml/Block/Promo/Quote/Edit/Tab/Coupons/Grid/Column/Renderer/Used.php new file mode 100644 index 0000000000..b289d54adf --- /dev/null +++ b/app/code/core/Mage/Adminhtml/Block/Promo/Quote/Edit/Tab/Coupons/Grid/Column/Renderer/Used.php @@ -0,0 +1,42 @@ + + */ +class Mage_Adminhtml_Block_Promo_Quote_Edit_Tab_Coupons_Grid_Column_Renderer_Used + extends Mage_Adminhtml_Block_Widget_Grid_Column_Renderer_Text +{ + public function render(Varien_Object $row) + { + $value = (int)$row->getData($this->getColumn()->getIndex()); + return empty($value) ? Mage::helper('adminhtml')->__('No') : Mage::helper('adminhtml')->__('Yes'); + } +} diff --git a/app/code/core/Mage/Adminhtml/Block/Promo/Quote/Edit/Tab/Main.php b/app/code/core/Mage/Adminhtml/Block/Promo/Quote/Edit/Tab/Main.php index d455aeec95..d474d831f0 100644 --- a/app/code/core/Mage/Adminhtml/Block/Promo/Quote/Edit/Tab/Main.php +++ b/app/code/core/Mage/Adminhtml/Block/Promo/Quote/Edit/Tab/Main.php @@ -80,12 +80,12 @@ protected function _prepareForm() { $model = Mage::registry('current_promo_quote_rule'); - //$form = new Varien_Data_Form(array('id' => 'edit_form1', 'action' => $this->getData('action'), 'method' => 'post')); $form = new Varien_Data_Form(); - $form->setHtmlIdPrefix('rule_'); - $fieldset = $form->addFieldset('base_fieldset', array('legend'=>Mage::helper('salesrule')->__('General Information'))); + $fieldset = $form->addFieldset('base_fieldset', + array('legend' => Mage::helper('salesrule')->__('General Information')) + ); if ($model->getId()) { $fieldset->addField('rule_id', 'hidden', array( @@ -153,7 +153,10 @@ protected function _prepareForm() } } if (!$found) { - array_unshift($customerGroups, array('value'=>0, 'label'=>Mage::helper('salesrule')->__('NOT LOGGED IN'))); + array_unshift($customerGroups, array( + 'value' => 0, + 'label' => Mage::helper('salesrule')->__('NOT LOGGED IN')) + ); } $fieldset->addField('customer_group_ids', 'multiselect', array( @@ -177,6 +180,18 @@ protected function _prepareForm() 'required' => true, )); + $autoGenerationCheckbox = $fieldset->addField('use_auto_generation', 'checkbox', array( + 'name' => 'use_auto_generation', + 'label' => Mage::helper('salesrule')->__('Use Auto Generation'), + 'note' => Mage::helper('salesrule')->__('If you select and save the rule you will be able to generate multiple coupon codes'), + 'onclick' => 'handleCouponsTabContentActivity()', + 'checked' => (int)$model->getUseAutoGeneration() > 0 ? 'checked' : '' + )); + + $autoGenerationCheckbox->setRenderer( + $this->getLayout()->createBlock('adminhtml/promo_quote_edit_tab_main_renderer_checkbox') + ); + $usesPerCouponFiled = $fieldset->addField('uses_per_coupon', 'text', array( 'name' => 'uses_per_coupon', 'label' => Mage::helper('salesrule')->__('Uses per Coupon'), @@ -227,6 +242,8 @@ protected function _prepareForm() $form->setValues($model->getData()); + $autoGenerationCheckbox->setValue(1); + if ($model->isReadonly()) { foreach ($fieldset->getElements() as $element) { $element->setReadonly(true, true); @@ -241,11 +258,16 @@ protected function _prepareForm() $this->setChild('form_after', $this->getLayout()->createBlock('adminhtml/widget_form_element_dependence') ->addFieldMap($couponTypeFiled->getHtmlId(), $couponTypeFiled->getName()) ->addFieldMap($couponCodeFiled->getHtmlId(), $couponCodeFiled->getName()) + ->addFieldMap($autoGenerationCheckbox->getHtmlId(), $autoGenerationCheckbox->getName()) ->addFieldMap($usesPerCouponFiled->getHtmlId(), $usesPerCouponFiled->getName()) ->addFieldDependence( $couponCodeFiled->getName(), $couponTypeFiled->getName(), Mage_SalesRule_Model_Rule::COUPON_TYPE_SPECIFIC) + ->addFieldDependence( + $autoGenerationCheckbox->getName(), + $couponTypeFiled->getName(), + Mage_SalesRule_Model_Rule::COUPON_TYPE_SPECIFIC) ->addFieldDependence( $usesPerCouponFiled->getName(), $couponTypeFiled->getName(), diff --git a/app/code/core/Mage/Adminhtml/Block/Promo/Quote/Edit/Tab/Main/Renderer/Checkbox.php b/app/code/core/Mage/Adminhtml/Block/Promo/Quote/Edit/Tab/Main/Renderer/Checkbox.php new file mode 100644 index 0000000000..d6ffbbe3de --- /dev/null +++ b/app/code/core/Mage/Adminhtml/Block/Promo/Quote/Edit/Tab/Main/Renderer/Checkbox.php @@ -0,0 +1,59 @@ + + */ +class Mage_Adminhtml_Block_Promo_Quote_Edit_Tab_Main_Renderer_Checkbox + extends Mage_Adminhtml_Block_Abstract + implements Varien_Data_Form_Element_Renderer_Interface +{ + /** + * Checkbox render function + * + * @param Varien_Data_Form_Element_Abstract $element + * @return string + */ + public function render(Varien_Data_Form_Element_Abstract $element) + { + $checkbox = new Varien_Data_Form_Element_Checkbox($element->getData()); + $checkbox->setForm($element->getForm()); + + $elementHtml = $checkbox->getElementHtml() . sprintf( + '

%s

', + $element->getHtmlId(), $element->getLabel(), $element->getNote() + ); + $html = ' '; + $html .= '' . $elementHtml . ''; + + return $html; + } + +} diff --git a/app/code/core/Mage/Adminhtml/Block/Promo/Widget/Chooser.php b/app/code/core/Mage/Adminhtml/Block/Promo/Widget/Chooser.php index 098611debf..871aec3c72 100644 --- a/app/code/core/Mage/Adminhtml/Block/Promo/Widget/Chooser.php +++ b/app/code/core/Mage/Adminhtml/Block/Promo/Widget/Chooser.php @@ -103,6 +103,10 @@ protected function _prepareCollection() $collection = Mage::getModel('salesrule/rule')->getResourceCollection(); $this->setCollection($collection); + Mage::dispatchEvent('adminhtml_block_promo_widget_chooser_prepare_collection', array( + 'collection' => $collection + )); + return parent::_prepareCollection(); } diff --git a/app/code/core/Mage/Adminhtml/Block/Report/Customer/Totals/Grid.php b/app/code/core/Mage/Adminhtml/Block/Report/Customer/Totals/Grid.php index a7eea5f854..277f1f9175 100644 --- a/app/code/core/Mage/Adminhtml/Block/Report/Customer/Totals/Grid.php +++ b/app/code/core/Mage/Adminhtml/Block/Report/Customer/Totals/Grid.php @@ -64,6 +64,7 @@ protected function _prepareColumns() )); $baseCurrencyCode = $this->getCurrentCurrencyCode(); + $rate = $this->getRate($baseCurrencyCode); $this->addColumn('orders_avg_amount', array( 'header' => $this->__('Average Order Amount'), @@ -74,7 +75,8 @@ protected function _prepareColumns() 'currency_code' => $baseCurrencyCode, 'index' => 'orders_avg_amount', 'total' => 'orders_sum_amount/orders_count', - 'renderer' => 'adminhtml/report_grid_column_renderer_currency' + 'renderer' => 'adminhtml/report_grid_column_renderer_currency', + 'rate' => $rate, )); $this->addColumn('orders_sum_amount', array( @@ -86,7 +88,8 @@ protected function _prepareColumns() 'currency_code' => $baseCurrencyCode, 'index' => 'orders_sum_amount', 'total' => 'sum', - 'renderer' => 'adminhtml/report_grid_column_renderer_currency' + 'renderer' => 'adminhtml/report_grid_column_renderer_currency', + 'rate' => $rate, )); $this->addExportType('*/*/exportTotalsCsv', Mage::helper('reports')->__('CSV')); diff --git a/app/code/core/Mage/Adminhtml/Block/Report/Grid.php b/app/code/core/Mage/Adminhtml/Block/Report/Grid.php index 5ff312f714..6d9720f780 100644 --- a/app/code/core/Mage/Adminhtml/Block/Report/Grid.php +++ b/app/code/core/Mage/Adminhtml/Block/Report/Grid.php @@ -461,7 +461,9 @@ public function getCsv() foreach ($this->_columns as $column) { $j++; if (!$column->getIsSystem()) { - $data[] = ($j==1)?'"'.$this->__('Subtotal').'"':'"'.str_replace('"', '""', $column->getRowField($this->getTotals())).'"'; + $data[] = ($j == 1) ? + '"' . $this->__('Subtotal') . '"' : + '"'.str_replace('"', '""', $column->getRowField($this->getTotals())).'"'; } } $csv.= implode(',', $data)."\n"; @@ -577,7 +579,6 @@ public function getCountTotals() public function getRefreshButtonCallback() { return "{$this->getJsObjectName()}.doFilter();"; - return "if ($('period_date_to').value == '' && $('period_date_from').value == '') {alert('".$this->__('Please specify at least start or end date.')."'); return false;}else {$this->getJsObjectName()}.doFilter();"; } /** @@ -613,4 +614,15 @@ public function getCurrentCurrencyCode() } return $this->_currentCurrencyCode; } + + /** + * Get currency rate (base to given currency) + * + * @param string|Mage_Directory_Model_Currency $currencyCode + * @return double + */ + public function getRate($toCurrency) + { + return Mage::app()->getStore()->getBaseCurrency()->getRate($toCurrency); + } } diff --git a/app/code/core/Mage/Adminhtml/Block/Report/Grid/Abstract.php b/app/code/core/Mage/Adminhtml/Block/Report/Grid/Abstract.php index e538faa9ac..53ca31c398 100644 --- a/app/code/core/Mage/Adminhtml/Block/Report/Grid/Abstract.php +++ b/app/code/core/Mage/Adminhtml/Block/Report/Grid/Abstract.php @@ -41,7 +41,7 @@ public function __construct() if (isset($this->_columnGroupBy)) { $this->isColumnGrouped($this->_columnGroupBy, true); } - $this->setEmptyCellLabel(Mage::helper('reports')->__('No records found for this period.')); + $this->setEmptyCellLabel(Mage::helper('adminhtml')->__('No records found for this period.')); } public function getResourceCollectionName() @@ -159,9 +159,11 @@ protected function _prepareCollection() ->setPeriod($filterData->getData('period_type')) ->setDateRange($filterData->getData('from', null), $filterData->getData('to', null)) ->addStoreFilter($storeIds) - ->addOrderStatusFilter($filterData->getData('order_statuses')) ->setAggregatedColumns($this->_getAggregatedColumns()); + $this->_addOrderStatusFilter($resourceCollection, $filterData); + $this->_addCustomFilter($resourceCollection, $filterData); + if ($this->_isExport) { $this->setCollection($resourceCollection); return $this; @@ -185,9 +187,11 @@ protected function _prepareCollection() ->setPeriod($filterData->getData('period_type')) ->setDateRange($filterData->getData('from', null), $filterData->getData('to', null)) ->addStoreFilter($storeIds) - ->addOrderStatusFilter($filterData->getData('order_statuses')) ->setAggregatedColumns($this->_getAggregatedColumns()) ->isTotals(true); + + $this->_addOrderStatusFilter($totalsCollection, $filterData); + foreach ($totalsCollection as $item) { $this->setTotals($item); break; @@ -208,9 +212,11 @@ public function getCountTotals() ->setPeriod($filterData->getData('period_type')) ->setDateRange($filterData->getData('from', null), $filterData->getData('to', null)) ->addStoreFilter($this->_getStoreIds()) - ->addOrderStatusFilter($filterData->getData('order_statuses')) ->setAggregatedColumns($this->_getAggregatedColumns()) ->isTotals(true); + + $this->_addOrderStatusFilter($totalsCollection, $filterData); + if (count($totalsCollection->getItems()) < 1 || !$filterData->getData('from')) { $this->setTotals(new Varien_Object()); } else { @@ -230,9 +236,11 @@ public function getSubTotals() ->setPeriod($filterData->getData('period_type')) ->setDateRange($filterData->getData('from', null), $filterData->getData('to', null)) ->addStoreFilter($this->_getStoreIds()) - ->addOrderStatusFilter($filterData->getData('order_statuses')) ->setAggregatedColumns($this->_getAggregatedColumns()) ->isSubTotals(true); + + $this->_addOrderStatusFilter($subTotalsCollection, $filterData); + $this->setSubTotals($subTotalsCollection->getItems()); return parent::getSubTotals(); } @@ -252,4 +260,41 @@ public function getCurrentCurrencyCode() } return $this->_currentCurrencyCode; } + + /** + * Get currency rate (base to given currency) + * + * @param string|Mage_Directory_Model_Currency $currencyCode + * @return double + */ + public function getRate($toCurrency) + { + return Mage::app()->getStore()->getBaseCurrency()->getRate($toCurrency); + } + + /** + * Add order status filter + * + * @param Mage_Reports_Model_Resource_Report_Collection_Abstract $collection + * @param Varien_Object $filterData + * @return Mage_Adminhtml_Block_Report_Grid_Abstract + */ + protected function _addOrderStatusFilter($collection, $filterData) + { + $collection->addOrderStatusFilter($filterData->getData('order_statuses')); + return $this; + } + + /** + * Adds custom filter to resource collection + * Can be overridden in child classes if custom filter needed + * + * @param Mage_Reports_Model_Resource_Report_Collection_Abstract $collection + * @param Varien_Object $filterData + * @return Mage_Adminhtml_Block_Report_Grid_Abstract + */ + protected function _addCustomFilter($collection, $filterData) + { + return $this; + } } diff --git a/app/code/core/Mage/Adminhtml/Block/Report/Grid/Shopcart.php b/app/code/core/Mage/Adminhtml/Block/Report/Grid/Shopcart.php new file mode 100644 index 0000000000..ad56636771 --- /dev/null +++ b/app/code/core/Mage/Adminhtml/Block/Report/Grid/Shopcart.php @@ -0,0 +1,84 @@ + + */ +class Mage_Adminhtml_Block_Report_Grid_Shopcart extends Mage_Adminhtml_Block_Widget_Grid +{ + /** + * stores current currency code + */ + protected $_currentCurrencyCode = null; + + /** + * ids of current stores + */ + protected $_storeIds = array(); + + /** + * storeIds setter + * + * @param array $storeIds + * @return Mage_Adminhtml_Block_Report_Grid_Shopcart_Abstract + */ + public function setStoreIds($storeIds) + { + $this->_storeIds = $storeIds; + return $this; + } + + /** + * Retrieve currency code based on selected store + * + * @return string + */ + public function getCurrentCurrencyCode() + { + if (is_null($this->_currentCurrencyCode)) { + reset($this->_storeIds); + $this->_currentCurrencyCode = (count($this->_storeIds) > 0) + ? Mage::app()->getStore(current($this->_storeIds))->getBaseCurrencyCode() + : Mage::app()->getStore()->getBaseCurrencyCode(); + } + return $this->_currentCurrencyCode; + } + + /** + * Get currency rate (base to given currency) + * + * @param string|Mage_Directory_Model_Currency $currencyCode + * @return double + */ + public function getRate($toCurrency) + { + return Mage::app()->getStore()->getBaseCurrency()->getRate($toCurrency); + } +} diff --git a/app/code/core/Mage/Adminhtml/Block/Report/Product/Ordered/Grid.php b/app/code/core/Mage/Adminhtml/Block/Report/Product/Ordered/Grid.php index 0fef9ee102..d70b696e34 100644 --- a/app/code/core/Mage/Adminhtml/Block/Report/Product/Ordered/Grid.php +++ b/app/code/core/Mage/Adminhtml/Block/Report/Product/Ordered/Grid.php @@ -51,12 +51,15 @@ protected function _prepareColumns() 'index' =>'name' )); + $baseCurrencyCode = $this->getCurrentCurrencyCode(); + $this->addColumn('price', array( - 'header' =>Mage::helper('reports')->__('Price'), - 'width' =>'120px', - 'type' =>'currency', - 'currency_code' => $this->getCurrentCurrencyCode(), - 'index' =>'price' + 'header' => Mage::helper('reports')->__('Price'), + 'width' => '120px', + 'type' => 'currency', + 'currency_code' => $baseCurrencyCode, + 'index' => 'price', + 'rate' => $this->getRate($baseCurrencyCode), )); $this->addColumn('ordered_qty', array( diff --git a/app/code/core/Mage/Adminhtml/Block/Report/Product/Viewed.php b/app/code/core/Mage/Adminhtml/Block/Report/Product/Viewed.php index 849f69b9fa..b7ae0c1b2c 100644 --- a/app/code/core/Mage/Adminhtml/Block/Report/Product/Viewed.php +++ b/app/code/core/Mage/Adminhtml/Block/Report/Product/Viewed.php @@ -39,6 +39,22 @@ public function __construct() $this->_controller = 'report_product_viewed'; $this->_headerText = Mage::helper('reports')->__('Most Viewed'); parent::__construct(); + $this->setTemplate('report/grid/container.phtml'); $this->_removeButton('add'); + $this->addButton('filter_form_submit', array( + 'label' => Mage::helper('reports')->__('Show Report'), + 'onclick' => 'filterFormSubmit()' + )); + } + + /** + * Get filter url + * + * @return string + */ + public function getFilterUrl() + { + $this->getRequest()->setParam('filter', null); + return $this->getUrl('*/*/viewed', array('_current' => true)); } } diff --git a/app/code/core/Mage/Adminhtml/Block/Report/Product/Viewed/Grid.php b/app/code/core/Mage/Adminhtml/Block/Report/Product/Viewed/Grid.php index 11a85d8d2e..786cb2e776 100644 --- a/app/code/core/Mage/Adminhtml/Block/Report/Product/Viewed/Grid.php +++ b/app/code/core/Mage/Adminhtml/Block/Report/Product/Viewed/Grid.php @@ -31,49 +31,94 @@ * @package Mage_Adminhtml * @author Magento Core Team */ -class Mage_Adminhtml_Block_Report_Product_Viewed_Grid extends Mage_Adminhtml_Block_Report_Grid +class Mage_Adminhtml_Block_Report_Product_Viewed_Grid extends Mage_Adminhtml_Block_Report_Grid_Abstract { + /** + * Column for grid to be grouped by + * + * @var string + */ + protected $_columnGroupBy = 'period'; + /** + * Grid resource collection name + * + * @var string + */ + protected $_resourceCollectionName = 'reports/report_product_viewed_collection'; + + /** + * Init grid parameters + */ public function __construct() { parent::__construct(); - $this->setId('gridViewedProducts'); - } - - protected function _prepareCollection() - { - parent::_prepareCollection(); - $this->getCollection()->initReport('reports/product_viewed_collection'); + $this->setCountTotals(true); } + /** + * Custom columns preparation + * + * @return Mage_Adminhtml_Block_Widget_Grid + */ protected function _prepareColumns() { - $this->addColumn('name', array( - 'header' =>Mage::helper('reports')->__('Product Name'), - 'index' =>'name', - 'total' =>Mage::helper('reports')->__('Subtotal') + $this->addColumn('period', array( + 'header' => Mage::helper('adminhtml')->__('Period'), + 'index' => 'period', + 'width' => 100, + 'sortable' => false, + 'period_type' => $this->getPeriodType(), + 'renderer' => 'adminhtml/report_sales_grid_column_renderer_date', + 'totals_label' => Mage::helper('adminhtml')->__('Total'), + 'html_decorators' => array('nobr'), + )); + + $this->addColumn('product_name', array( + 'header' => Mage::helper('adminhtml')->__('Product Name'), + 'index' => 'product_name', + 'type' => 'string', + 'sortable' => false )); - $this->addColumn('price', array( - 'header' =>Mage::helper('reports')->__('Price'), - 'width' =>'120px', - 'type' =>'currency', - 'currency_code' => $this->getCurrentCurrencyCode(), - 'index' =>'price' + if ($this->getFilterData()->getStoreIds()) { + $this->setStoreIds(explode(',', $this->getFilterData()->getStoreIds())); + } + $currencyCode = $this->getCurrentCurrencyCode(); + + $this->addColumn('product_price', array( + 'header' => Mage::helper('adminhtml')->__('Price'), + 'type' => 'currency', + 'currency_code' => $currencyCode, + 'index' => 'product_price', + 'sortable' => false, + 'rate' => $this->getRate($currencyCode), )); - $this->addColumn('views', array( - 'header' =>Mage::helper('reports')->__('Number of Views'), - 'width' =>'120px', - 'align' =>'right', - 'index' =>'views', - 'total' =>'sum' + $this->addColumn('views_num', array( + 'header' => Mage::helper('adminhtml')->__('Number of Views'), + 'index' => 'views_num', + 'type' => 'number', + 'total' => 'sum', + 'sortable' => false )); - $this->addExportType('*/*/exportViewedCsv', Mage::helper('reports')->__('CSV')); - $this->addExportType('*/*/exportViewedExcel', Mage::helper('reports')->__('Excel XML')); + + $this->addExportType('*/*/exportViewedCsv', Mage::helper('adminhtml')->__('CSV')); + $this->addExportType('*/*/exportViewedExcel', Mage::helper('adminhtml')->__('Excel XML')); return parent::_prepareColumns(); } + /** + * Don't use orders in collection + * + * @param Mage_Reports_Model_Resource_Report_Collection_Abstract $collection + * @param Varien_Object $filterData + * @return Mage_Adminhtml_Block_Report_Grid_Abstract + */ + protected function _addOrderStatusFilter($collection, $filterData) + { + return $this; + } } diff --git a/app/code/core/Mage/Adminhtml/Block/Report/Refresh/Statistics/Grid.php b/app/code/core/Mage/Adminhtml/Block/Report/Refresh/Statistics/Grid.php index d93da70851..da8e0f2da7 100644 --- a/app/code/core/Mage/Adminhtml/Block/Report/Refresh/Statistics/Grid.php +++ b/app/code/core/Mage/Adminhtml/Block/Report/Refresh/Statistics/Grid.php @@ -97,7 +97,13 @@ protected function _prepareCollection() 'report' => Mage::helper('sales')->__('Bestsellers'), 'comment' => Mage::helper('sales')->__('Products Bestsellers Report'), 'updated_at' => $this->_getUpdatedAt(Mage_Reports_Model_Flag::REPORT_BESTSELLERS_FLAG_CODE) - ) + ), + array( + 'id' => 'viewed', + 'report' => Mage::helper('sales')->__('Most Viewed'), + 'comment' => Mage::helper('sales')->__('Most Viewed Products Report'), + 'updated_at' => $this->_getUpdatedAt(Mage_Reports_Model_Flag::REPORT_PRODUCT_VIEWED_FLAG_CODE) + ), ); foreach ($data as $value) { diff --git a/app/code/core/Mage/Adminhtml/Block/Report/Sales/Bestsellers/Grid.php b/app/code/core/Mage/Adminhtml/Block/Report/Sales/Bestsellers/Grid.php index 47d624efdc..2dbc84d965 100644 --- a/app/code/core/Mage/Adminhtml/Block/Report/Sales/Bestsellers/Grid.php +++ b/app/code/core/Mage/Adminhtml/Block/Report/Sales/Bestsellers/Grid.php @@ -76,7 +76,8 @@ protected function _prepareColumns() 'type' => 'currency', 'currency_code' => $currencyCode, 'index' => 'product_price', - 'sortable' => false + 'sortable' => false, + 'rate' => $this->getRate($currencyCode), )); $this->addColumn('qty_ordered', array( diff --git a/app/code/core/Mage/Adminhtml/Block/Report/Sales/Coupons/Grid.php b/app/code/core/Mage/Adminhtml/Block/Report/Sales/Coupons/Grid.php index 8395552ab3..6052969aee 100644 --- a/app/code/core/Mage/Adminhtml/Block/Report/Sales/Coupons/Grid.php +++ b/app/code/core/Mage/Adminhtml/Block/Report/Sales/Coupons/Grid.php @@ -71,6 +71,12 @@ protected function _prepareColumns() 'index' => 'coupon_code' )); + $this->addColumn('rule_name', array( + 'header' => Mage::helper('salesrule')->__('Shopping Cart Price Rule'), + 'sortable' => false, + 'index' => 'rule_name' + )); + $this->addColumn('coupon_uses', array( 'header' => Mage::helper('salesrule')->__('Number of Uses'), 'sortable' => false, @@ -83,6 +89,7 @@ protected function _prepareColumns() $this->setStoreIds(explode(',', $this->getFilterData()->getStoreIds())); } $currencyCode = $this->getCurrentCurrencyCode(); + $rate = $this->getRate($currencyCode); $this->addColumn('subtotal_amount', array( 'header' => Mage::helper('salesrule')->__('Sales Subtotal Amount'), @@ -90,7 +97,8 @@ protected function _prepareColumns() 'type' => 'currency', 'currency_code' => $currencyCode, 'total' => 'sum', - 'index' => 'subtotal_amount' + 'index' => 'subtotal_amount', + 'rate' => $rate, )); $this->addColumn('discount_amount', array( @@ -99,7 +107,8 @@ protected function _prepareColumns() 'type' => 'currency', 'currency_code' => $currencyCode, 'total' => 'sum', - 'index' => 'discount_amount' + 'index' => 'discount_amount', + 'rate' => $rate, )); $this->addColumn('total_amount', array( @@ -108,7 +117,8 @@ protected function _prepareColumns() 'type' => 'currency', 'currency_code' => $currencyCode, 'total' => 'sum', - 'index' => 'total_amount' + 'index' => 'total_amount', + 'rate' => $rate, )); $this->addColumn('subtotal_amount_actual', array( @@ -117,7 +127,8 @@ protected function _prepareColumns() 'type' => 'currency', 'currency_code' => $currencyCode, 'total' => 'sum', - 'index' => 'subtotal_amount_actual' + 'index' => 'subtotal_amount_actual', + 'rate' => $rate, )); $this->addColumn('discount_amount_actual', array( @@ -126,7 +137,8 @@ protected function _prepareColumns() 'type' => 'currency', 'currency_code' => $currencyCode, 'total' => 'sum', - 'index' => 'discount_amount_actual' + 'index' => 'discount_amount_actual', + 'rate' => $rate, )); $this->addColumn('total_amount_actual', array( @@ -135,7 +147,8 @@ protected function _prepareColumns() 'type' => 'currency', 'currency_code' => $currencyCode, 'total' => 'sum', - 'index' => 'total_amount_actual' + 'index' => 'total_amount_actual', + 'rate' => $rate, )); $this->addExportType('*/*/exportCouponsCsv', Mage::helper('adminhtml')->__('CSV')); @@ -143,4 +156,24 @@ protected function _prepareColumns() return parent::_prepareColumns(); } + + /** + * Add price rule filter + * + * @param Mage_Reports_Model_Resource_Report_Collection_Abstract $collection + * @param Varien_Object $filterData + * @return Mage_Adminhtml_Block_Report_Grid_Abstract + */ + protected function _addCustomFilter($collection, $filterData) + { + if ($filterData->getPriceRuleType()) { + $rulesList = $filterData->getData('rules_list'); + if (isset($rulesList[0])) { + $rulesIds = explode(',', $rulesList[0]); + $collection->addRuleFilter($rulesIds); + } + } + + return parent::_addCustomFilter($filterData, $collection); + } } diff --git a/app/code/core/Mage/Adminhtml/Block/Report/Sales/Invoiced/Grid.php b/app/code/core/Mage/Adminhtml/Block/Report/Sales/Invoiced/Grid.php index 5576ddb4b7..96e94e9c56 100644 --- a/app/code/core/Mage/Adminhtml/Block/Report/Sales/Invoiced/Grid.php +++ b/app/code/core/Mage/Adminhtml/Block/Report/Sales/Invoiced/Grid.php @@ -81,6 +81,7 @@ protected function _prepareColumns() $this->setStoreIds(explode(',', $this->getFilterData()->getStoreIds())); } $currencyCode = $this->getCurrentCurrencyCode(); + $rate = $this->getRate($currencyCode); $this->addColumn('invoiced', array( 'header' => Mage::helper('sales')->__('Total Invoiced'), @@ -88,7 +89,8 @@ protected function _prepareColumns() 'currency_code' => $currencyCode, 'index' => 'invoiced', 'total' => 'sum', - 'sortable' => false + 'sortable' => false, + 'rate' => $rate, )); $this->addColumn('invoiced_captured', array( @@ -97,7 +99,8 @@ protected function _prepareColumns() 'currency_code' => $currencyCode, 'index' => 'invoiced_captured', 'total' => 'sum', - 'sortable' => false + 'sortable' => false, + 'rate' => $rate, )); $this->addColumn('invoiced_not_captured', array( @@ -106,7 +109,8 @@ protected function _prepareColumns() 'currency_code' => $currencyCode, 'index' => 'invoiced_not_captured', 'total' => 'sum', - 'sortable' => false + 'sortable' => false, + 'rate' => $rate, )); $this->addExportType('*/*/exportInvoicedCsv', Mage::helper('adminhtml')->__('CSV')); diff --git a/app/code/core/Mage/Adminhtml/Block/Report/Sales/Refunded/Grid.php b/app/code/core/Mage/Adminhtml/Block/Report/Sales/Refunded/Grid.php index 4396f61189..837881e39f 100644 --- a/app/code/core/Mage/Adminhtml/Block/Report/Sales/Refunded/Grid.php +++ b/app/code/core/Mage/Adminhtml/Block/Report/Sales/Refunded/Grid.php @@ -73,6 +73,7 @@ protected function _prepareColumns() $this->setStoreIds(explode(',', $this->getFilterData()->getStoreIds())); } $currencyCode = $this->getCurrentCurrencyCode(); + $rate = $this->getRate($currencyCode); $this->addColumn('refunded', array( 'header' => Mage::helper('sales')->__('Total Refunded'), @@ -80,7 +81,8 @@ protected function _prepareColumns() 'currency_code' => $currencyCode, 'index' => 'refunded', 'total' => 'sum', - 'sortable' => false + 'sortable' => false, + 'rate' => $rate, )); $this->addColumn('online_refunded', array( @@ -89,7 +91,8 @@ protected function _prepareColumns() 'currency_code' => $currencyCode, 'index' => 'online_refunded', 'total' => 'sum', - 'sortable' => false + 'sortable' => false, + 'rate' => $rate, )); $this->addColumn('offline_refunded', array( @@ -98,7 +101,8 @@ protected function _prepareColumns() 'currency_code' => $currencyCode, 'index' => 'offline_refunded', 'total' => 'sum', - 'sortable' => false + 'sortable' => false, + 'rate' => $rate, )); $this->addExportType('*/*/exportRefundedCsv', Mage::helper('adminhtml')->__('CSV')); diff --git a/app/code/core/Mage/Adminhtml/Block/Report/Sales/Sales/Grid.php b/app/code/core/Mage/Adminhtml/Block/Report/Sales/Sales/Grid.php index 9e9a77fc64..f887cb4756 100644 --- a/app/code/core/Mage/Adminhtml/Block/Report/Sales/Sales/Grid.php +++ b/app/code/core/Mage/Adminhtml/Block/Report/Sales/Sales/Grid.php @@ -90,6 +90,7 @@ protected function _prepareColumns() $this->setStoreIds(explode(',', $this->getFilterData()->getStoreIds())); } $currencyCode = $this->getCurrentCurrencyCode(); + $rate = $this->getRate($currencyCode); $this->addColumn('total_income_amount', array( 'header' => Mage::helper('sales')->__('Sales Total'), @@ -97,27 +98,30 @@ protected function _prepareColumns() 'currency_code' => $currencyCode, 'index' => 'total_income_amount', 'total' => 'sum', - 'sortable' => false + 'sortable' => false, + 'rate' => $rate, )); $this->addColumn('total_revenue_amount', array( - 'header' => Mage::helper('sales')->__('Revenue'), - 'type' => 'currency', - 'currency_code' => $currencyCode, - 'index' => 'total_revenue_amount', - 'total' => 'sum', - 'sortable' => false, - 'visibility_filter' => array('show_actual_columns') + 'header' => Mage::helper('sales')->__('Revenue'), + 'type' => 'currency', + 'currency_code' => $currencyCode, + 'index' => 'total_revenue_amount', + 'total' => 'sum', + 'sortable' => false, + 'visibility_filter' => array('show_actual_columns'), + 'rate' => $rate, )); $this->addColumn('total_profit_amount', array( - 'header' => Mage::helper('sales')->__('Profit'), - 'type' => 'currency', - 'currency_code' => $currencyCode, - 'index' => 'total_profit_amount', - 'total' => 'sum', - 'sortable' => false, - 'visibility_filter' => array('show_actual_columns') + 'header' => Mage::helper('sales')->__('Profit'), + 'type' => 'currency', + 'currency_code' => $currencyCode, + 'index' => 'total_profit_amount', + 'total' => 'sum', + 'sortable' => false, + 'visibility_filter' => array('show_actual_columns'), + 'rate' => $rate, )); $this->addColumn('total_invoiced_amount', array( @@ -126,17 +130,19 @@ protected function _prepareColumns() 'currency_code' => $currencyCode, 'index' => 'total_invoiced_amount', 'total' => 'sum', - 'sortable' => false + 'sortable' => false, + 'rate' => $rate, )); $this->addColumn('total_paid_amount', array( - 'header' => Mage::helper('sales')->__('Paid'), - 'type' => 'currency', - 'currency_code' => $currencyCode, - 'index' => 'total_paid_amount', - 'total' => 'sum', - 'sortable' => false, - 'visibility_filter' => array('show_actual_columns') + 'header' => Mage::helper('sales')->__('Paid'), + 'type' => 'currency', + 'currency_code' => $currencyCode, + 'index' => 'total_paid_amount', + 'total' => 'sum', + 'sortable' => false, + 'visibility_filter' => array('show_actual_columns'), + 'rate' => $rate, )); $this->addColumn('total_refunded_amount', array( @@ -145,7 +151,8 @@ protected function _prepareColumns() 'currency_code' => $currencyCode, 'index' => 'total_refunded_amount', 'total' => 'sum', - 'sortable' => false + 'sortable' => false, + 'rate' => $rate, )); $this->addColumn('total_tax_amount', array( @@ -154,17 +161,19 @@ protected function _prepareColumns() 'currency_code' => $currencyCode, 'index' => 'total_tax_amount', 'total' => 'sum', - 'sortable' => false + 'sortable' => false, + 'rate' => $rate, )); $this->addColumn('total_tax_amount_actual', array( - 'header' => Mage::helper('sales')->__('Tax'), - 'type' => 'currency', - 'currency_code' => $currencyCode, - 'index' => 'total_tax_amount_actual', - 'total' => 'sum', - 'sortable' => false, - 'visibility_filter' => array('show_actual_columns') + 'header' => Mage::helper('sales')->__('Tax'), + 'type' => 'currency', + 'currency_code' => $currencyCode, + 'index' => 'total_tax_amount_actual', + 'total' => 'sum', + 'sortable' => false, + 'visibility_filter' => array('show_actual_columns'), + 'rate' => $rate, )); $this->addColumn('total_shipping_amount', array( @@ -173,17 +182,19 @@ protected function _prepareColumns() 'currency_code' => $currencyCode, 'index' => 'total_shipping_amount', 'total' => 'sum', - 'sortable' => false + 'sortable' => false, + 'rate' => $rate, )); $this->addColumn('total_shipping_amount_actual', array( - 'header' => Mage::helper('sales')->__('Shipping'), - 'type' => 'currency', - 'currency_code' => $currencyCode, - 'index' => 'total_shipping_amount_actual', - 'total' => 'sum', - 'sortable' => false, - 'visibility_filter' => array('show_actual_columns') + 'header' => Mage::helper('sales')->__('Shipping'), + 'type' => 'currency', + 'currency_code' => $currencyCode, + 'index' => 'total_shipping_amount_actual', + 'total' => 'sum', + 'sortable' => false, + 'visibility_filter' => array('show_actual_columns'), + 'rate' => $rate, )); $this->addColumn('total_discount_amount', array( @@ -192,17 +203,19 @@ protected function _prepareColumns() 'currency_code' => $currencyCode, 'index' => 'total_discount_amount', 'total' => 'sum', - 'sortable' => false + 'sortable' => false, + 'rate' => $rate, )); $this->addColumn('total_discount_amount_actual', array( - 'header' => Mage::helper('sales')->__('Discount'), - 'type' => 'currency', - 'currency_code' => $currencyCode, - 'index' => 'total_discount_amount_actual', - 'total' => 'sum', - 'sortable' => false, - 'visibility_filter' => array('show_actual_columns') + 'header' => Mage::helper('sales')->__('Discount'), + 'type' => 'currency', + 'currency_code' => $currencyCode, + 'index' => 'total_discount_amount_actual', + 'total' => 'sum', + 'sortable' => false, + 'visibility_filter' => array('show_actual_columns'), + 'rate' => $rate, )); $this->addColumn('total_canceled_amount', array( @@ -211,7 +224,8 @@ protected function _prepareColumns() 'currency_code' => $currencyCode, 'index' => 'total_canceled_amount', 'total' => 'sum', - 'sortable' => false + 'sortable' => false, + 'rate' => $rate, )); diff --git a/app/code/core/Mage/Adminhtml/Block/Report/Sales/Shipping/Grid.php b/app/code/core/Mage/Adminhtml/Block/Report/Sales/Shipping/Grid.php index 340706fdea..d9a0703ced 100644 --- a/app/code/core/Mage/Adminhtml/Block/Report/Sales/Shipping/Grid.php +++ b/app/code/core/Mage/Adminhtml/Block/Report/Sales/Shipping/Grid.php @@ -82,22 +82,27 @@ protected function _prepareColumns() $this->setStoreIds(explode(',', $this->getFilterData()->getStoreIds())); } + $currencyCode = $this->getCurrentCurrencyCode(); + $rate = $this->getRate($currencyCode); + $this->addColumn('total_shipping', array( 'header' => Mage::helper('sales')->__('Total Sales Shipping'), 'type' => 'currency', - 'currency_code' => $this->getCurrentCurrencyCode(), + 'currency_code' => $currencyCode, 'index' => 'total_shipping', 'total' => 'sum', - 'sortable' => false + 'sortable' => false, + 'rate' => $rate, )); $this->addColumn('total_shipping_actual', array( 'header' => Mage::helper('sales')->__('Total Shipping'), 'type' => 'currency', - 'currency_code' => $this->getCurrentCurrencyCode(), + 'currency_code' => $currencyCode, 'index' => 'total_shipping_actual', 'total' => 'sum', - 'sortable' => false + 'sortable' => false, + 'rate' => $rate, )); $this->addExportType('*/*/exportShippingCsv', Mage::helper('adminhtml')->__('CSV')); diff --git a/app/code/core/Mage/Adminhtml/Block/Report/Sales/Tax/Grid.php b/app/code/core/Mage/Adminhtml/Block/Report/Sales/Tax/Grid.php index 874715ff1f..bee1385f01 100644 --- a/app/code/core/Mage/Adminhtml/Block/Report/Sales/Tax/Grid.php +++ b/app/code/core/Mage/Adminhtml/Block/Report/Sales/Tax/Grid.php @@ -90,14 +90,16 @@ protected function _prepareColumns() if ($this->getFilterData()->getStoreIds()) { $this->setStoreIds(explode(',', $this->getFilterData()->getStoreIds())); } + $currencyCode = $this->getCurrentCurrencyCode(); $this->addColumn('tax_base_amount_sum', array( 'header' => Mage::helper('sales')->__('Tax Amount'), 'type' => 'currency', - 'currency_code' => $this->getCurrentCurrencyCode(), + 'currency_code' => $currencyCode, 'index' => 'tax_base_amount_sum', 'total' => 'sum', - 'sortable' => false + 'sortable' => false, + 'rate' => $this->getRate($currencyCode), )); $this->addExportType('*/*/exportTaxCsv', Mage::helper('adminhtml')->__('CSV')); diff --git a/app/code/core/Mage/Adminhtml/Block/Report/Shopcart/Abandoned/Grid.php b/app/code/core/Mage/Adminhtml/Block/Report/Shopcart/Abandoned/Grid.php index 77ae5f118c..7725ae799a 100644 --- a/app/code/core/Mage/Adminhtml/Block/Report/Shopcart/Abandoned/Grid.php +++ b/app/code/core/Mage/Adminhtml/Block/Report/Shopcart/Abandoned/Grid.php @@ -31,7 +31,7 @@ * @package Mage_Adminhtml * @author Magento Core Team */ -class Mage_Adminhtml_Block_Report_Shopcart_Abandoned_Grid extends Mage_Adminhtml_Block_Widget_Grid +class Mage_Adminhtml_Block_Report_Shopcart_Abandoned_Grid extends Mage_Adminhtml_Block_Report_Grid_Shopcart { public function __construct() @@ -42,16 +42,6 @@ public function __construct() protected function _prepareCollection() { - if ($this->getRequest()->getParam('website')) { - $storeIds = Mage::app()->getWebsite($this->getRequest()->getParam('website'))->getStoreIds(); - } else if ($this->getRequest()->getParam('group')) { - $storeIds = Mage::app()->getGroup($this->getRequest()->getParam('group'))->getStoreIds(); - } else if ($this->getRequest()->getParam('store')) { - $storeIds = array((int)$this->getRequest()->getParam('store')); - } else { - $storeIds = ''; - } - /** @var $collection Mage_Reports_Model_Resource_Quote_Collection */ $collection = Mage::getResourceModel('reports/quote_collection'); @@ -62,9 +52,9 @@ protected function _prepareCollection() } if (!empty($data)) { - $collection->prepareForAbandonedReport($storeIds, $data); + $collection->prepareForAbandonedReport($this->_storeIds, $data); } else { - $collection->prepareForAbandonedReport($storeIds); + $collection->prepareForAbandonedReport($this->_storeIds); } $this->setCollection($collection); @@ -116,14 +106,27 @@ protected function _prepareColumns() 'type' =>'number' )); + if ($this->getRequest()->getParam('website')) { + $storeIds = Mage::app()->getWebsite($this->getRequest()->getParam('website'))->getStoreIds(); + } else if ($this->getRequest()->getParam('group')) { + $storeIds = Mage::app()->getGroup($this->getRequest()->getParam('group'))->getStoreIds(); + } else if ($this->getRequest()->getParam('store')) { + $storeIds = array((int)$this->getRequest()->getParam('store')); + } else { + $storeIds = array(); + } + $this->setStoreIds($storeIds); + $currencyCode = $this->getCurrentCurrencyCode(); + $this->addColumn('subtotal', array( - 'header' =>Mage::helper('reports')->__('Subtotal'), - 'width' =>'80px', - 'type' =>'currency', - 'currency_code' => $this->getCurrentCurrencyCode(), - 'index' =>'subtotal', - 'sortable' =>false, - 'renderer' =>'adminhtml/report_grid_column_renderer_currency' + 'header' => Mage::helper('reports')->__('Subtotal'), + 'width' => '80px', + 'type' => 'currency', + 'currency_code' => $currencyCode, + 'index' => 'subtotal', + 'sortable' => false, + 'renderer' => 'adminhtml/report_grid_column_renderer_currency', + 'rate' => $this->getRate($currencyCode), )); $this->addColumn('coupon_code', array( diff --git a/app/code/core/Mage/Adminhtml/Block/Report/Shopcart/Customer/Grid.php b/app/code/core/Mage/Adminhtml/Block/Report/Shopcart/Customer/Grid.php index 5ee05b2502..a4ffa4e6d3 100644 --- a/app/code/core/Mage/Adminhtml/Block/Report/Shopcart/Customer/Grid.php +++ b/app/code/core/Mage/Adminhtml/Block/Report/Shopcart/Customer/Grid.php @@ -31,7 +31,7 @@ * @package Mage_Adminhtml * @author Magento Core Team */ -class Mage_Adminhtml_Block_Report_Shopcart_Customer_Grid extends Mage_Adminhtml_Block_Widget_Grid +class Mage_Adminhtml_Block_Report_Shopcart_Customer_Grid extends Mage_Adminhtml_Block_Report_Grid_Shopcart { public function __construct() @@ -83,15 +83,18 @@ protected function _prepareColumns() 'index' =>'items' )); + $currencyCode = $this->getCurrentCurrencyCode(); + $this->addColumn('total', array( 'header' =>Mage::helper('reports')->__('Total'), 'width' =>'70px', 'sortable' =>false, 'type' =>'currency', 'align' =>'right', - 'currency_code' => $this->getCurrentCurrencyCode(), + 'currency_code' => $currencyCode, 'index' =>'total', - 'renderer' =>'adminhtml/report_grid_column_renderer_currency' + 'renderer' =>'adminhtml/report_grid_column_renderer_currency', + 'rate' => $this->getRate($currencyCode), )); $this->setFilterVisibility(false); @@ -103,4 +106,3 @@ protected function _prepareColumns() } } - diff --git a/app/code/core/Mage/Adminhtml/Block/Report/Shopcart/Product/Grid.php b/app/code/core/Mage/Adminhtml/Block/Report/Shopcart/Product/Grid.php index b57898b6b4..526f13e3df 100644 --- a/app/code/core/Mage/Adminhtml/Block/Report/Shopcart/Product/Grid.php +++ b/app/code/core/Mage/Adminhtml/Block/Report/Shopcart/Product/Grid.php @@ -31,7 +31,7 @@ * @package Mage_Adminhtml * @author Magento Core Team */ -class Mage_Adminhtml_Block_Report_Shopcart_Product_Grid extends Mage_Adminhtml_Block_Widget_Grid +class Mage_Adminhtml_Block_Report_Shopcart_Product_Grid extends Mage_Adminhtml_Block_Report_Grid_Shopcart { public function __construct() @@ -64,13 +64,16 @@ protected function _prepareColumns() 'index' =>'name' )); + $currencyCode = $this->getCurrentCurrencyCode(); + $this->addColumn('price', array( 'header' =>Mage::helper('reports')->__('Price'), 'width' =>'80px', 'type' =>'currency', - 'currency_code' => $this->getCurrentCurrencyCode(), + 'currency_code' => $currencyCode, 'index' =>'price', - 'renderer' =>'adminhtml/report_grid_column_renderer_currency' + 'renderer' =>'adminhtml/report_grid_column_renderer_currency', + 'rate' => $this->getRate($currencyCode), )); $this->addColumn('carts', array( diff --git a/app/code/core/Mage/Adminhtml/Block/Review/Edit/Form.php b/app/code/core/Mage/Adminhtml/Block/Review/Edit/Form.php index 40df93c846..9d316785e1 100644 --- a/app/code/core/Mage/Adminhtml/Block/Review/Edit/Form.php +++ b/app/code/core/Mage/Adminhtml/Block/Review/Edit/Form.php @@ -58,11 +58,7 @@ protected function _prepareForm() )); if ($customer->getId()) { - $customerText = Mage::helper('review')->__('%2$s %3$s (%4$s)', - $this->getUrl('*/customer/edit', array('id' => $customer->getId(), 'active_tab'=>'review')), - $this->htmlEscape($customer->getFirstname()), - $this->htmlEscape($customer->getLastname()), - $this->htmlEscape($customer->getEmail())); + $customerText = Mage::helper('review')->__('%2$s %3$s (%4$s)', $this->getUrl('*/customer/edit', array('id' => $customer->getId(), 'active_tab'=>'review')), $this->escapeHtml($customer->getFirstname()), $this->escapeHtml($customer->getLastname()), $this->escapeHtml($customer->getEmail())); } else { if (is_null($review->getCustomerId())) { $customerText = Mage::helper('review')->__('Guest'); @@ -84,7 +80,9 @@ protected function _prepareForm() $fieldset->addField('detailed_rating', 'note', array( 'label' => Mage::helper('review')->__('Detailed Rating'), 'required' => true, - 'text' => '
' . $this->getLayout()->createBlock('adminhtml/review_rating_detailed')->toHtml() . '
', + 'text' => '
' + . $this->getLayout()->createBlock('adminhtml/review_rating_detailed')->toHtml() + . '
', )); $fieldset->addField('status_id', 'select', array( diff --git a/app/code/core/Mage/Adminhtml/Block/Sales/Items/Column/Name.php b/app/code/core/Mage/Adminhtml/Block/Sales/Items/Column/Name.php index 28d240c68d..7de3d48584 100644 --- a/app/code/core/Mage/Adminhtml/Block/Sales/Items/Column/Name.php +++ b/app/code/core/Mage/Adminhtml/Block/Sales/Items/Column/Name.php @@ -34,5 +34,22 @@ */ class Mage_Adminhtml_Block_Sales_Items_Column_Name extends Mage_Adminhtml_Block_Sales_Items_Column_Default { + /** + * Add line breaks and truncate value + * + * @param string $value + * @return array + */ + public function getFormattedOption($value) + { + $_remainder = ''; + $value = Mage::helper('core/string')->truncate($value, 55, '', $_remainder); + $result = array( + 'value' => nl2br($value), + 'remainder' => nl2br($_remainder) + ); + + return $result; + } } ?> diff --git a/app/code/core/Mage/Adminhtml/Block/Sales/Order/Create.php b/app/code/core/Mage/Adminhtml/Block/Sales/Order/Create.php index 9c54b053a7..78aaa25cbb 100644 --- a/app/code/core/Mage/Adminhtml/Block/Sales/Order/Create.php +++ b/app/code/core/Mage/Adminhtml/Block/Sales/Order/Create.php @@ -56,6 +56,8 @@ public function __construct() } $this->_updateButton('back', 'id', 'back_order_top_button'); + $this->_updateButton('back', 'onclick', 'setLocation(\'' . $this->getBackUrl() . '\')'); + $this->_updateButton('reset', 'id', 'reset_order_top_button'); if (is_null($customerId)) { @@ -64,14 +66,10 @@ public function __construct() $this->_updateButton('back', 'style', 'display:none'); } - //$this->_removeButton('back'); - $this->_updateButton('back', 'onclick', 'setLocation(\'' . $this->getUrl('*/sales_order/') . '\');'); - $confirm = Mage::helper('sales')->__('Are you sure you want to cancel this order?'); $this->_updateButton('reset', 'label', Mage::helper('sales')->__('Cancel')); $this->_updateButton('reset', 'class', 'cancel'); $this->_updateButton('reset', 'onclick', 'deleteConfirm(\''.$confirm.'\', \'' . $this->getCancelUrl() . '\')'); - } /** @@ -81,9 +79,9 @@ public function __construct() */ public function getHeaderHtml() { - $out = '
'; - $out.= $this->getLayout()->createBlock('adminhtml/sales_order_create_header')->toHtml(); - $out.= '
'; + $out = '
' + . $this->getLayout()->createBlock('adminhtml/sales_order_create_header')->toHtml() + . '
'; return $out; } @@ -118,14 +116,22 @@ public function getCancelUrl() { if ($this->_getSession()->getOrder()->getId()) { $url = $this->getUrl('*/sales_order/view', array( - 'order_id'=>Mage::getSingleton('adminhtml/session_quote')->getOrder()->getId() + 'order_id' => Mage::getSingleton('adminhtml/session_quote')->getOrder()->getId() )); - } - else { + } else { $url = $this->getUrl('*/*/cancel'); } return $url; } + /** + * Get URL for back (reset) button + * + * @return string + */ + public function getBackUrl() + { + return $this->getUrl('*/' . $this->_controller . '/'); + } } diff --git a/app/code/core/Mage/Adminhtml/Block/Sales/Order/Create/Billing/Address.php b/app/code/core/Mage/Adminhtml/Block/Sales/Order/Create/Billing/Address.php index a49ff5fa45..6e265807c2 100644 --- a/app/code/core/Mage/Adminhtml/Block/Sales/Order/Create/Billing/Address.php +++ b/app/code/core/Mage/Adminhtml/Block/Sales/Order/Create/Billing/Address.php @@ -68,6 +68,7 @@ protected function _prepareForm() $this->_form->setHtmlNamePrefix('order[billing_address]'); $this->_form->setHtmlIdPrefix('order-billing_address_'); + Mage::dispatchEvent('adminhtml_sales_order_billing_address_prepare_form_after', array('form' => $this)); return $this; } diff --git a/app/code/core/Mage/Adminhtml/Block/Sales/Order/Create/Form/Address.php b/app/code/core/Mage/Adminhtml/Block/Sales/Order/Create/Form/Address.php index 9daeae6fe2..5d0b2b432f 100644 --- a/app/code/core/Mage/Adminhtml/Block/Sales/Order/Create/Form/Address.php +++ b/app/code/core/Mage/Adminhtml/Block/Sales/Order/Create/Form/Address.php @@ -154,6 +154,15 @@ protected function _prepareForm() $this->_form->setValues($this->getFormValues()); + if ($this->_form->getElement('country_id')->getValue()) { + $countryId = $this->_form->getElement('country_id')->getValue(); + $this->_form->getElement('country_id')->setValue(null); + foreach ($this->_form->getElement('country_id')->getValues() as $country) { + if ($country['value'] == $countryId) { + $this->_form->getElement('country_id')->setValue($countryId); + } + } + } if (!$this->_form->getElement('country_id')->getValue()) { $this->_form->getElement('country_id')->setValue( Mage::helper('core')->getDefaultCountry($this->getStore()) diff --git a/app/code/core/Mage/Adminhtml/Block/Sales/Order/Create/Sidebar/Cart.php b/app/code/core/Mage/Adminhtml/Block/Sales/Order/Create/Sidebar/Cart.php index 10e88d1ca7..7d5f718c60 100644 --- a/app/code/core/Mage/Adminhtml/Block/Sales/Order/Create/Sidebar/Cart.php +++ b/app/code/core/Mage/Adminhtml/Block/Sales/Order/Create/Sidebar/Cart.php @@ -31,7 +31,8 @@ * @package Mage_Adminhtml * @author Magento Core Team */ -class Mage_Adminhtml_Block_Sales_Order_Create_Sidebar_Cart extends Mage_Adminhtml_Block_Sales_Order_Create_Sidebar_Abstract +class Mage_Adminhtml_Block_Sales_Order_Create_Sidebar_Cart + extends Mage_Adminhtml_Block_Sales_Order_Create_Sidebar_Abstract { /** * Storage action on selected item @@ -93,4 +94,23 @@ public function getProductId($item) { return $item->getProduct()->getId(); } + + /** + * Prepare layout + * + * Add button that clears customer's shopping cart + * + * @return Mage_Adminhtml_Block_Sales_Order_Create_Sidebar_Cart + */ + protected function _prepareLayout() + { + $button = $this->getLayout()->createBlock('adminhtml/widget_button')->setData(array( + 'label' => Mage::helper('sales')->__('Clear Shopping Cart'), + 'onclick' => 'order.sidebarApplyChanges({\'sidebar[empty_customer_cart]\': 1})', + 'style' => 'float: right;' + )); + $this->setChild('empty_customer_cart_button', $button); + + return parent::_prepareLayout(); + } } diff --git a/app/code/core/Mage/Adminhtml/Block/Sales/Order/Creditmemo/Create.php b/app/code/core/Mage/Adminhtml/Block/Sales/Order/Creditmemo/Create.php index 7a530e58b7..c27b6e8650 100644 --- a/app/code/core/Mage/Adminhtml/Block/Sales/Order/Creditmemo/Create.php +++ b/app/code/core/Mage/Adminhtml/Block/Sales/Order/Creditmemo/Create.php @@ -67,20 +67,12 @@ public function getCreditmemo() public function getHeaderText() { if ($this->getCreditmemo()->getInvoice()) { - $header = Mage::helper('sales')->__('New Credit Memo for Invoice #%s', - $this->getCreditmemo()->getInvoice()->getIncrementId() - ); + $header = Mage::helper('sales')->__('New Credit Memo for Invoice #%s', $this->getCreditmemo()->getInvoice()->getIncrementId()); } else { - $header = Mage::helper('sales')->__('New Credit Memo for Order #%s', - $this->getCreditmemo()->getOrder()->getRealOrderId() - ); + $header = Mage::helper('sales')->__('New Credit Memo for Order #%s', $this->getCreditmemo()->getOrder()->getRealOrderId()); } - /*$header = Mage::helper('sales')->__('New Credit Memo for Order #%s | Order Date: %s | Customer Name: %s', - $this->getCreditmemo()->getOrder()->getRealOrderId(), - $this->formatDate($this->getCreditmemo()->getOrder()->getCreatedAt(), 'medium', true), - $this->getCreditmemo()->getOrder()->getCustomerName() - );*/ + return $header; } diff --git a/app/code/core/Mage/Adminhtml/Block/Sales/Order/View/Tab/History.php b/app/code/core/Mage/Adminhtml/Block/Sales/Order/View/Tab/History.php index 2d62888132..dc42deee2d 100644 --- a/app/code/core/Mage/Adminhtml/Block/Sales/Order/View/Tab/History.php +++ b/app/code/core/Mage/Adminhtml/Block/Sales/Order/View/Tab/History.php @@ -54,6 +54,7 @@ public function getOrder() /** * Compose and get order full history. * Consists of the status history comments as well as of invoices, shipments and creditmemos creations + * * @return array */ public function getFullHistory() @@ -62,7 +63,7 @@ public function getFullHistory() $history = array(); foreach ($order->getAllStatusHistory() as $orderComment){ - $history[$orderComment->getEntityId()] = $this->_prepareHistoryItem( + $history[] = $this->_prepareHistoryItem( $orderComment->getStatusLabel(), $orderComment->getIsCustomerNotified(), $orderComment->getCreatedAtDate(), @@ -71,54 +72,74 @@ public function getFullHistory() } foreach ($order->getCreditmemosCollection() as $_memo){ - $history[$_memo->getEntityId()] = - $this->_prepareHistoryItem($this->__('Credit memo #%s created', $_memo->getIncrementId()), - $_memo->getEmailSent(), $_memo->getCreatedAtDate()); + $history[] = $this->_prepareHistoryItem( + $this->__('Credit memo #%s created', $_memo->getIncrementId()), + $_memo->getEmailSent(), + $_memo->getCreatedAtDate() + ); foreach ($_memo->getCommentsCollection() as $_comment){ - $history[$_comment->getEntityId()] = - $this->_prepareHistoryItem($this->__('Credit memo #%s comment added', $_memo->getIncrementId()), - $_comment->getIsCustomerNotified(), $_comment->getCreatedAtDate(), $_comment->getComment()); + $history[] = $this->_prepareHistoryItem( + $this->__('Credit memo #%s comment added', $_memo->getIncrementId()), + $_comment->getIsCustomerNotified(), + $_comment->getCreatedAtDate(), + $_comment->getComment() + ); } } foreach ($order->getShipmentsCollection() as $_shipment){ - $history[$_shipment->getEntityId()] = - $this->_prepareHistoryItem($this->__('Shipment #%s created', $_shipment->getIncrementId()), - $_shipment->getEmailSent(), $_shipment->getCreatedAtDate()); + $history[] = $this->_prepareHistoryItem( + $this->__('Shipment #%s created', $_shipment->getIncrementId()), + $_shipment->getEmailSent(), + $_shipment->getCreatedAtDate() + ); foreach ($_shipment->getCommentsCollection() as $_comment){ - $history[$_comment->getEntityId()] = - $this->_prepareHistoryItem($this->__('Shipment #%s comment added', $_shipment->getIncrementId()), - $_comment->getIsCustomerNotified(), $_comment->getCreatedAtDate(), $_comment->getComment()); + $history[] = $this->_prepareHistoryItem( + $this->__('Shipment #%s comment added', $_shipment->getIncrementId()), + $_comment->getIsCustomerNotified(), + $_comment->getCreatedAtDate(), + $_comment->getComment() + ); } } foreach ($order->getInvoiceCollection() as $_invoice){ - $history[$_invoice->getEntityId()] = - $this->_prepareHistoryItem($this->__('Invoice #%s created', $_invoice->getIncrementId()), - $_invoice->getEmailSent(), $_invoice->getCreatedAtDate()); + $history[] = $this->_prepareHistoryItem( + $this->__('Invoice #%s created', $_invoice->getIncrementId()), + $_invoice->getEmailSent(), + $_invoice->getCreatedAtDate() + ); foreach ($_invoice->getCommentsCollection() as $_comment){ - $history[$_comment->getEntityId()] = - $this->_prepareHistoryItem($this->__('Invoice #%s comment added', $_invoice->getIncrementId()), - $_comment->getIsCustomerNotified(), $_comment->getCreatedAtDate(), $_comment->getComment()); + $history[] = $this->_prepareHistoryItem( + $this->__('Invoice #%s comment added', $_invoice->getIncrementId()), + $_comment->getIsCustomerNotified(), + $_comment->getCreatedAtDate(), + $_comment->getComment() + ); } } foreach ($order->getTracksCollection() as $_track){ - $history[$_track->getEntityId()] = - $this->_prepareHistoryItem($this->__('Tracking number %s for %s assigned', $_track->getNumber(), $_track->getTitle()), - false, $_track->getCreatedAtDate()); + $history[] = $this->_prepareHistoryItem( + $this->__('Tracking number %s for %s assigned', $_track->getNumber(), $_track->getTitle()), + false, + $_track->getCreatedAtDate() + ); } - krsort($history); + usort($history, array(__CLASS__, "_sortHistoryByTimestamp")); return $history; } /** * Status history date/datetime getter + * * @param array $item + * @param string $dateType + * @param string $format * @return string */ public function getItemCreatedAt(array $item, $dateType = 'date', $format = 'medium') @@ -134,6 +155,7 @@ public function getItemCreatedAt(array $item, $dateType = 'date', $format = 'med /** * Status history item title getter + * * @param array $item * @return string */ @@ -144,7 +166,9 @@ public function getItemTitle(array $item) /** * Check whether status history comment is with customer notification + * * @param array $item + * @param boolean $isSimpleCheck * @return bool */ public function isItemNotified(array $item, $isSimpleCheck = true) @@ -157,6 +181,7 @@ public function isItemNotified(array $item, $isSimpleCheck = true) /** * Status history item comment getter + * * @param array $item * @return string */ @@ -168,10 +193,12 @@ public function getItemComment(array $item) /** * Map history items as array + * * @param string $label * @param bool $notified * @param Zend_Date $created * @param string $comment + * @return array */ protected function _prepareHistoryItem($label, $notified, $created, $comment = '') { @@ -184,38 +211,70 @@ protected function _prepareHistoryItem($label, $notified, $created, $comment = ' } /** - * ######################## TAB settings ################################# + * Get Tab Label + * + * @return string */ public function getTabLabel() { return Mage::helper('sales')->__('Comments History'); } + /** + * Get Tab Title + * + * @return string + */ public function getTabTitle() { return Mage::helper('sales')->__('Order History'); } + /** + * Get Tab Class + * + * @return string + */ public function getTabClass() { return 'ajax only'; } + /** + * Get Class + * + * @return string + */ public function getClass() { return $this->getTabClass(); } + /** + * Get Tab Url + * + * @return string + */ public function getTabUrl() { return $this->getUrl('*/*/commentsHistory', array('_current' => true)); } + /** + * Can Show Tab + * + * @return boolean + */ public function canShowTab() { return true; } + /** + * Is Hidden + * + * @return boolean + */ public function isHidden() { return false; @@ -224,11 +283,30 @@ public function isHidden() /** * Customer Notification Applicable check method * - * @param array $history + * @param array $historyItem * @return boolean */ public function isCustomerNotificationNotApplicable($historyItem) { return $historyItem['notified'] == Mage_Sales_Model_Order_Status_History::CUSTOMER_NOTIFICATION_NOT_APPLICABLE; } + + /** + * Comparison For Sorting History By Timestamp + * + * @param mixed $a + * @param mixed $b + * @return int + */ + private static function _sortHistoryByTimestamp($a, $b) + { + $createdAtA = $a['created_at']; + $createdAtB = $b['created_at']; + + /** @var $createdAta Zend_Date */ + if ($createdAtA->getTimestamp() == $createdAtB->getTimestamp()) { + return 0; + } + return ($createdAtA->getTimestamp() < $createdAtB->getTimestamp()) ? -1 : 1; + } } diff --git a/app/code/core/Mage/Adminhtml/Block/Sales/Transactions/Detail.php b/app/code/core/Mage/Adminhtml/Block/Sales/Transactions/Detail.php index 5a3d0b7386..2a6041227d 100644 --- a/app/code/core/Mage/Adminhtml/Block/Sales/Transactions/Detail.php +++ b/app/code/core/Mage/Adminhtml/Block/Sales/Transactions/Detail.php @@ -75,10 +75,7 @@ public function __construct() */ public function getHeaderText() { - return Mage::helper('sales')->__("Transaction # %s | %s", - $this->_txn->getTxnId(), - $this->formatDate($this->_txn->getCreatedAt(), Mage_Core_Model_Locale::FORMAT_TYPE_MEDIUM, true) - ); + return Mage::helper('sales')->__("Transaction # %s | %s", $this->_txn->getTxnId(), $this->formatDate($this->_txn->getCreatedAt(), Mage_Core_Model_Locale::FORMAT_TYPE_MEDIUM, true)); } protected function _toHtml() diff --git a/app/code/core/Mage/Adminhtml/Block/Sales/Transactions/Detail/Grid.php b/app/code/core/Mage/Adminhtml/Block/Sales/Transactions/Detail/Grid.php index 14c18a45a8..98e8e72b02 100644 --- a/app/code/core/Mage/Adminhtml/Block/Sales/Transactions/Detail/Grid.php +++ b/app/code/core/Mage/Adminhtml/Block/Sales/Transactions/Detail/Grid.php @@ -79,7 +79,8 @@ protected function _prepareColumns() 'header' => Mage::helper('sales')->__('Value'), 'index' => 'value', 'sortable' => false, - 'type' => 'text' + 'type' => 'text', + 'escape' => true )); return parent::_prepareColumns(); diff --git a/app/code/core/Mage/Adminhtml/Block/System/Config/Form/Fieldset.php b/app/code/core/Mage/Adminhtml/Block/System/Config/Form/Fieldset.php index e09e6e0f36..c363fca180 100644 --- a/app/code/core/Mage/Adminhtml/Block/System/Config/Form/Fieldset.php +++ b/app/code/core/Mage/Adminhtml/Block/System/Config/Form/Fieldset.php @@ -66,20 +66,23 @@ protected function _getHeaderHtml($element) { $default = !$this->getRequest()->getParam('website') && !$this->getRequest()->getParam('store'); - $html = '
'.$element->getLegend().'
'; - $html.= ''; - $html.= '
'; - $html.= ''.$element->getLegend().''; + $html = '
' . $element->getLegend() . '
'; + $html .= ''; + $html .= '
'; + $html .= '' . $element->getLegend() . ''; if ($element->getComment()) { - $html .= '
'.$element->getComment().'
'; + $html .= '' . $element->getComment() . ''; } // field label column - $html.= ''; + $html .= '
'; if (!$default) { - $html.= ''; + $html .= ''; } - $html.= ''; + $html .= ''; return $html; } diff --git a/app/code/core/Mage/Adminhtml/Block/System/Email/Template/Grid/Filter/Type.php b/app/code/core/Mage/Adminhtml/Block/System/Email/Template/Grid/Filter/Type.php index b7b1aeb9cb..8b7109b313 100644 --- a/app/code/core/Mage/Adminhtml/Block/System/Email/Template/Grid/Filter/Type.php +++ b/app/code/core/Mage/Adminhtml/Block/System/Email/Template/Grid/Filter/Type.php @@ -32,19 +32,20 @@ * @author Magento Core Team */ -class Mage_Adminhtml_Block_System_Email_Template_Grid_Filter_Type extends Mage_Adminhtml_Block_Widget_Grid_Column_Filter_Select +class Mage_Adminhtml_Block_System_Email_Template_Grid_Filter_Type + extends Mage_Adminhtml_Block_Widget_Grid_Column_Filter_Select { protected static $_types = array( - null => null, + null => null, Mage_Newsletter_Model_Template::TYPE_HTML => 'HTML', - Mage_Newsletter_Model_Template::TYPE_TEXT => 'Text', + Mage_Newsletter_Model_Template::TYPE_TEXT => 'Text', ); protected function _getOptions() { $result = array(); - foreach (self::$_types as $code=>$label) { - $result[] = array('value'=>$code, 'label'=>Mage::helper('adminhtml')->__($label)); + foreach (self::$_types as $code => $label) { + $result[] = array('value' => $code, 'label' => Mage::helper('adminhtml')->__($label)); } return $result; @@ -57,8 +58,6 @@ public function getCondition() return null; } - return array('eq'=>$this->getValue()); + return array('eq' => $this->getValue()); } - - -}// Class Mage_Adminhtml_Block_Newsletter_Queue_Grid_Filter_Status END +} diff --git a/app/code/core/Mage/Adminhtml/Block/System/Email/Template/Grid/Renderer/Type.php b/app/code/core/Mage/Adminhtml/Block/System/Email/Template/Grid/Renderer/Type.php index ff77ac5efb..8973b34142 100644 --- a/app/code/core/Mage/Adminhtml/Block/System/Email/Template/Grid/Renderer/Type.php +++ b/app/code/core/Mage/Adminhtml/Block/System/Email/Template/Grid/Renderer/Type.php @@ -32,11 +32,12 @@ * @author Magento Core Team */ -class Mage_Adminhtml_Block_System_Email_Template_Grid_Renderer_Type extends Mage_Adminhtml_Block_Widget_Grid_Column_Renderer_Abstract +class Mage_Adminhtml_Block_System_Email_Template_Grid_Renderer_Type + extends Mage_Adminhtml_Block_Widget_Grid_Column_Renderer_Abstract { protected static $_types = array( - Mage_Newsletter_Model_Template::TYPE_HTML => 'HTML', - Mage_Newsletter_Model_Template::TYPE_TEXT => 'Text', + Mage_Newsletter_Model_Template::TYPE_HTML => 'HTML', + Mage_Newsletter_Model_Template::TYPE_TEXT => 'Text', ); public function render(Varien_Object $row) { diff --git a/app/code/core/Mage/Adminhtml/Block/System/Email/Template/Preview.php b/app/code/core/Mage/Adminhtml/Block/System/Email/Template/Preview.php index 7048b3d6f2..418688e4f3 100644 --- a/app/code/core/Mage/Adminhtml/Block/System/Email/Template/Preview.php +++ b/app/code/core/Mage/Adminhtml/Block/System/Email/Template/Preview.php @@ -29,23 +29,27 @@ * * @category Mage * @package Mage_Adminhtml - * @author Magento Core Team + * @author Magento Core Team */ class Mage_Adminhtml_Block_System_Email_Template_Preview extends Mage_Adminhtml_Block_Widget { - protected function _toHtml() { + /* @var $template Mage_Core_Model_Email_Template */ $template = Mage::getModel('core/email_template'); - if($id = (int)$this->getRequest()->getParam('id')) { + if ($id = (int)$this->getRequest()->getParam('id')) { $template->load($id); } else { $template->setTemplateType($this->getRequest()->getParam('type')); $template->setTemplateText($this->getRequest()->getParam('text')); $template->setTemplateStyles($this->getRequest()->getParam('styles')); } + + /* @var $filter Mage_Core_Model_Input_Filter_MaliciousCode */ + $filter = Mage::getSingleton('core/input_filter_maliciousCode'); + $template->setTemplateText( - $this->escapeHtml($template->getTemplateText()) + $filter->filter($template->getTemplateText()) ); Varien_Profiler::start("email_template_proccessing"); @@ -53,7 +57,7 @@ protected function _toHtml() $templateProcessed = $template->getProcessedTemplate($vars, true); - if($template->isPlain()) { + if ($template->isPlain()) { $templateProcessed = "
" . htmlspecialchars($templateProcessed) . "
"; } @@ -61,5 +65,4 @@ protected function _toHtml() return $templateProcessed; } - } diff --git a/app/code/core/Mage/Adminhtml/Block/Tag/Edit/Form.php b/app/code/core/Mage/Adminhtml/Block/Tag/Edit/Form.php index 420574694e..7d14ac5d60 100644 --- a/app/code/core/Mage/Adminhtml/Block/Tag/Edit/Form.php +++ b/app/code/core/Mage/Adminhtml/Block/Tag/Edit/Form.php @@ -42,6 +42,11 @@ public function __construct() $this->setTitle(Mage::helper('tag')->__('Block Information')); } + /** + * Prepare form + * + * @return Mage_Adminhtml_Block_Widget_Form + */ protected function _prepareForm() { $model = Mage::registry('tag_tag'); @@ -50,7 +55,8 @@ protected function _prepareForm() array('id' => 'edit_form', 'action' => $this->getData('action'), 'method' => 'post') ); - $fieldset = $form->addFieldset('base_fieldset', array('legend'=>Mage::helper('tag')->__('General Information'))); + $fieldset = $form->addFieldset('base_fieldset', + array('legend'=>Mage::helper('tag')->__('General Information'))); if ($model->getTagId()) { $fieldset->addField('tag_id', 'hidden', array( @@ -73,7 +79,7 @@ protected function _prepareForm() 'label' => Mage::helper('tag')->__('Tag Name'), 'title' => Mage::helper('tag')->__('Tag Name'), 'required' => true, - 'after_element_html' => ' [GLOBAL]', + 'after_element_html' => ' ' . Mage::helper('adminhtml')->__('[GLOBAL]'), )); $fieldset->addField('status', 'select', array( @@ -86,14 +92,14 @@ protected function _prepareForm() Mage_Tag_Model_Tag::STATUS_PENDING => Mage::helper('tag')->__('Pending'), Mage_Tag_Model_Tag::STATUS_APPROVED => Mage::helper('tag')->__('Approved'), ), - 'after_element_html' => ' [GLOBAL]', + 'after_element_html' => ' ' . Mage::helper('adminhtml')->__('[GLOBAL]'), )); $fieldset->addField('base_popularity', 'text', array( 'name' => 'base_popularity', 'label' => Mage::helper('tag')->__('Base Popularity'), 'title' => Mage::helper('tag')->__('Base Popularity'), - 'after_element_html' => ' [STORE VIEW]', + 'after_element_html' => ' ' . Mage::helper('tag')->__('[STORE VIEW]'), )); if (!$model->getId() && !Mage::getSingleton('adminhtml/session')->getTagData() ) { diff --git a/app/code/core/Mage/Adminhtml/Block/Widget/Form.php b/app/code/core/Mage/Adminhtml/Block/Widget/Form.php index 8ffb587e94..f70f3f04f8 100644 --- a/app/code/core/Mage/Adminhtml/Block/Widget/Form.php +++ b/app/code/core/Mage/Adminhtml/Block/Widget/Form.php @@ -201,6 +201,7 @@ protected function _setFieldset($attributes, $fieldset, $exclude=array()) $element->setValues($attribute->getSource()->getAllOptions(true, true)); } else if ($inputType == 'multiselect') { $element->setValues($attribute->getSource()->getAllOptions(false, true)); + $element->setCanBeEmpty(true); } else if ($inputType == 'date') { $element->setImage($this->getSkinUrl('images/grid-cal.gif')); $element->setFormat( diff --git a/app/code/core/Mage/Adminhtml/Block/Widget/Form/Renderer/Fieldset/Element.php b/app/code/core/Mage/Adminhtml/Block/Widget/Form/Renderer/Fieldset/Element.php index 18037260f2..b1bd89841a 100644 --- a/app/code/core/Mage/Adminhtml/Block/Widget/Form/Renderer/Fieldset/Element.php +++ b/app/code/core/Mage/Adminhtml/Block/Widget/Form/Renderer/Fieldset/Element.php @@ -31,7 +31,8 @@ * @package Mage_Adminhtml * @author Magento Core Team */ -class Mage_Adminhtml_Block_Widget_Form_Renderer_Fieldset_Element extends Mage_Adminhtml_Block_Template implements Varien_Data_Form_Element_Renderer_Interface +class Mage_Adminhtml_Block_Widget_Form_Renderer_Fieldset_Element extends Mage_Adminhtml_Block_Template + implements Varien_Data_Form_Element_Renderer_Interface { protected $_element; diff --git a/app/code/core/Mage/Adminhtml/Block/Widget/Grid.php b/app/code/core/Mage/Adminhtml/Block/Widget/Grid.php index 607f1720d8..7c58ced2f1 100644 --- a/app/code/core/Mage/Adminhtml/Block/Widget/Grid.php +++ b/app/code/core/Mage/Adminhtml/Block/Widget/Grid.php @@ -543,8 +543,8 @@ protected function _decodeFilter(&$value) protected function _preparePage() { - $this->getCollection()->setPageSize($this->getParam($this->getVarNameLimit(), $this->_defaultLimit)); - $this->getCollection()->setCurPage($this->getParam($this->getVarNamePage(), $this->_defaultPage)); + $this->getCollection()->setPageSize((int) $this->getParam($this->getVarNameLimit(), $this->_defaultLimit)); + $this->getCollection()->setCurPage((int) $this->getParam($this->getVarNamePage(), $this->_defaultPage)); } protected function _prepareColumns() diff --git a/app/code/core/Mage/Adminhtml/Controller/Report/Abstract.php b/app/code/core/Mage/Adminhtml/Controller/Report/Abstract.php new file mode 100644 index 0000000000..598a5f5512 --- /dev/null +++ b/app/code/core/Mage/Adminhtml/Controller/Report/Abstract.php @@ -0,0 +1,124 @@ + + */ +abstract class Mage_Adminhtml_Controller_Report_Abstract extends Mage_Adminhtml_Controller_Action +{ + /** + * Admin session model + * + * @var null|Mage_Admin_Model_Session + */ + protected $_adminSession = null; + + /** + * Retrieve admin session model + * + * @return Mage_Admin_Model_Session + */ + protected function _getSession() + { + if (is_null($this->_adminSession)) { + $this->_adminSession = Mage::getSingleton('admin/session'); + } + return $this->_adminSession; + } + + /** + * Add report breadcrumbs + * + * @return Mage_Adminhtml_Controller_Report_Abstract + */ + public function _initAction() + { + $this->loadLayout() + ->_addBreadcrumb(Mage::helper('reports')->__('Reports'), Mage::helper('reports')->__('Reports')); + return $this; + } + + /** + * Report action init operations + * + * @param array|Varien_Object $blocks + * @return Mage_Adminhtml_Controller_Report_Abstract + */ + public function _initReportAction($blocks) + { + if (!is_array($blocks)) { + $blocks = array($blocks); + } + + $requestData = Mage::helper('adminhtml')->prepareFilterString($this->getRequest()->getParam('filter')); + $requestData = $this->_filterDates($requestData, array('from', 'to')); + $requestData['store_ids'] = $this->getRequest()->getParam('store_ids'); + $params = new Varien_Object(); + + foreach ($requestData as $key => $value) { + if (!empty($value)) { + $params->setData($key, $value); + } + } + + foreach ($blocks as $block) { + if ($block) { + $block->setPeriodType($params->getData('period_type')); + $block->setFilterData($params); + } + } + + return $this; + } + + /** + * Add refresh statistics links + * + * @param string $flagCode + * @param string $refreshCode + * @return Mage_Adminhtml_Controller_Report_Abstract + */ + protected function _showLastExecutionTime($flagCode, $refreshCode) + { + $flag = Mage::getModel('reports/flag')->setReportFlagCode($flagCode)->loadSelf(); + $updatedAt = ($flag->hasData()) + ? Mage::app()->getLocale()->storeDate( + 0, new Zend_Date($flag->getLastUpdate(), Varien_Date::DATETIME_INTERNAL_FORMAT), true + ) + : 'undefined'; + + $refreshStatsLink = $this->getUrl('*/report_statistics'); + $directRefreshLink = $this->getUrl('*/report_statistics/refreshRecent', array('code' => $refreshCode)); + + Mage::getSingleton('adminhtml/session')->addNotice(Mage::helper('adminhtml')->__('Last updated: %s. To refresh last day\'s statistics, click here.', $updatedAt, $refreshStatsLink, $directRefreshLink)); + return $this; + } +} diff --git a/app/code/core/Mage/Adminhtml/Model/Sales/Order.php b/app/code/core/Mage/Adminhtml/Model/Sales/Order.php index d079bc1e61..194d99415c 100644 --- a/app/code/core/Mage/Adminhtml/Model/Sales/Order.php +++ b/app/code/core/Mage/Adminhtml/Model/Sales/Order.php @@ -71,9 +71,7 @@ public function checkRelation(Mage_Sales_Model_Order $order) foreach ($order->getAllItems() as $item) { if (!$productCollection->getItemById($item->getProductId())) { $this->_getSession()->addError( - Mage::helper('adminhtml')->__('The item %s (SKU %s) does not exist in the catalog anymore.', - $item->getName(), - $item->getSku() + Mage::helper('adminhtml')->__('The item %s (SKU %s) does not exist in the catalog anymore.', $item->getName(), $item->getSku() )); $hasBadItems = true; } diff --git a/app/code/core/Mage/Adminhtml/Model/Sales/Order/Create.php b/app/code/core/Mage/Adminhtml/Model/Sales/Order/Create.php index f8b15930a9..f34da5e269 100644 --- a/app/code/core/Mage/Adminhtml/Model/Sales/Order/Create.php +++ b/app/code/core/Mage/Adminhtml/Model/Sales/Order/Create.php @@ -633,6 +633,9 @@ public function applySidebarData($data) $this->removeItem($itemId, $from); } } + if (isset($data['empty_customer_cart']) && (int)$data['empty_customer_cart'] == 1) { + $this->getCustomerCart()->removeAllItems()->collectTotals()->save(); + } return $this; } @@ -716,6 +719,8 @@ public function addProduct($product, $config = 1) $stockItem = $product->getStockItem(); if ($stockItem && $stockItem->getIsQtyDecimal()) { $product->setIsQtyDecimal(1); + } else { + $config->setQty((int) $config->getQty()); } $product->setCartQty($config->getQty()); @@ -1305,21 +1310,28 @@ protected function _setCustomerData(Mage_Customer_Model_Customer $customer) /** * Prepare quote customer + * + * @return Mage_Adminhtml_Model_Sales_Order_Create */ public function _prepareCustomer() { + /** @var $quote Mage_Sales_Model_Quote */ $quote = $this->getQuote(); if ($quote->getCustomerIsGuest()) { return $this; } - $customer = $this->getSession()->getCustomer(); - $store = $this->getSession()->getStore(); - $customerIsInStore = $this->_customerIsInStore($store); - $billingAddress = null; - $shippingAddress = null; + /** @var $customer Mage_Customer_Model_Customer */ + $customer = $this->getSession()->getCustomer(); + /** @var $store Mage_Core_Model_Store */ + $store = $this->getSession()->getStore(); + + $customerIsInStore = $this->_customerIsInStore($store); + $customerBillingAddress = null; + $customerShippingAddress = null; if ($customer->getId()) { + // Create new customer if customer is not registered in specified store if (!$customerIsInStore) { $customer->setId(null) ->setStore($store) @@ -1328,70 +1340,82 @@ public function _prepareCustomer() ->setPassword($customer->generatePassword()); $this->_setCustomerData($customer); } - if ($this->getBillingAddress()->getSaveInAddressBook() || !$customerIsInStore) { - $billingAddress = $this->getBillingAddress()->exportCustomerAddress(); + + if ($this->getBillingAddress()->getSaveInAddressBook()) { + /** @var $customerBillingAddress Mage_Customer_Model_Address */ + $customerBillingAddress = $this->getBillingAddress()->exportCustomerAddress(); $customerAddressId = $this->getBillingAddress()->getCustomerAddressId(); if ($customerAddressId && $customer->getId()) { - $customer->getAddressItemById($customerAddressId)->addData($billingAddress->getData()); + $customer->getAddressItemById($customerAddressId)->addData($customerBillingAddress->getData()); } else { - $customer->addAddress($billingAddress); + $customer->addAddress($customerBillingAddress); } } - if (!$this->getQuote()->isVirtual() && ($this->getShippingAddress()->getSaveInAddressBook() - || !$customerIsInStore) - ) { - $shippingAddress = $this->getShippingAddress()->exportCustomerAddress(); + + if (!$this->getQuote()->isVirtual() && $this->getShippingAddress()->getSaveInAddressBook()) { + /** @var $customerShippingAddress Mage_Customer_Model_Address */ + $customerShippingAddress = $this->getShippingAddress()->exportCustomerAddress(); $customerAddressId = $this->getShippingAddress()->getCustomerAddressId(); if ($customerAddressId && $customer->getId()) { - $customer->getAddressItemById($customerAddressId)->addData($shippingAddress->getData()); + $customer->getAddressItemById($customerAddressId)->addData($customerShippingAddress->getData()); } elseif (!empty($customerAddressId) - && $billingAddress !== null + && $customerBillingAddress !== null && $this->getBillingAddress()->getCustomerAddressId() == $customerAddressId ) { - $billingAddress->setIsDefaultShipping(true); + $customerBillingAddress->setIsDefaultShipping(true); } else { - $customer->addAddress($shippingAddress); + $customer->addAddress($customerShippingAddress); } } - if (is_null($customer->getDefaultBilling()) && $billingAddress) { - $billingAddress->setIsDefaultBilling(true); + if (is_null($customer->getDefaultBilling()) && $customerBillingAddress) { + $customerBillingAddress->setIsDefaultBilling(true); } + if (is_null($customer->getDefaultShipping())) { - if ($this->getShippingAddress()->getSameAsBilling() && $billingAddress) { - $billingAddress->setIsDefaultShipping(true); - } elseif ($shippingAddress) { - $shippingAddress->setIsDefaultShipping(true); + if ($this->getShippingAddress()->getSameAsBilling() && $customerBillingAddress) { + $customerBillingAddress->setIsDefaultShipping(true); + } elseif ($customerShippingAddress) { + $customerShippingAddress->setIsDefaultShipping(true); } } } else { - $customer->addData($this->getBillingAddress()->exportCustomerAddress()->getData()) + // Prepare new customer + /** @var $customerBillingAddress Mage_Customer_Model_Address */ + $customerBillingAddress = $this->getBillingAddress()->exportCustomerAddress(); + $customer->addData($customerBillingAddress->getData()) ->setPassword($customer->generatePassword()) ->setStore($store); $customer->setEmail($this->_getNewCustomerEmail($customer)); $this->_setCustomerData($customer); - $customerBilling = $this->getBillingAddress()->exportCustomerAddress(); - $customerBilling->setIsDefaultBilling(true); - $customer->addAddress($customerBilling); + if ($this->getBillingAddress()->getSaveInAddressBook()) { + $customerBillingAddress->setIsDefaultBilling(true); + $customer->addAddress($customerBillingAddress); + } - $shipping = $this->getShippingAddress(); - if (!$this->getQuote()->isVirtual() && !$shipping->getSameAsBilling()) { - $customerShipping = $shipping->exportCustomerAddress(); - $customerShipping->setIsDefaultShipping(true); - $customer->addAddress($customerShipping); + /** @var $shippingAddress Mage_Sales_Model_Quote_Address */ + $shippingAddress = $this->getShippingAddress(); + if (!$this->getQuote()->isVirtual() + && !$shippingAddress->getSameAsBilling() + && $shippingAddress->getSaveInAddressBook() + ) { + /** @var $customerShippingAddress Mage_Customer_Model_Address */ + $customerShippingAddress = $shippingAddress->exportCustomerAddress(); + $customerShippingAddress->setIsDefaultShipping(true); + $customer->addAddress($customerShippingAddress); } else { - $customerBilling->setIsDefaultShipping(true); + $customerBillingAddress->setIsDefaultShipping(true); } } - // set quote customer data to customer + // Set quote customer data to customer $this->_setCustomerData($customer); - // set customer to quote and convert customer data to quote + // Set customer to quote and convert customer data to quote $quote->setCustomer($customer); - // add user defined attributes to quote + // Add user defined attributes to quote $form = $this->_getCustomerForm()->setEntity($customer); foreach ($form->getUserAttributes() as $attribute) { $quoteCode = sprintf('customer_%s', $attribute->getAttributeCode()); @@ -1399,7 +1423,7 @@ public function _prepareCustomer() } if ($customer->getId()) { - // we should not change account data for existing customer, so restore it + // Restore account data for existing customer $this->_getCustomerForm() ->setEntity($customer) ->resetEntityData(); diff --git a/app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Admin/Password/Link/Expirationperiod.php b/app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Admin/Password/Link/Expirationperiod.php index 28ccc7562b..c0b48d180a 100644 --- a/app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Admin/Password/Link/Expirationperiod.php +++ b/app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Admin/Password/Link/Expirationperiod.php @@ -43,7 +43,8 @@ protected function _beforeSave() { parent::_beforeSave(); $resetPasswordLinkExpirationPeriod = (int) $this->getValue(); - if ($resetPasswordLinkExpirationPeriod < 0) { + // This value must be greater than 0 + if ($resetPasswordLinkExpirationPeriod < 1) { $resetPasswordLinkExpirationPeriod = (int) $this->getOldValue(); } $this->setValue((string) $resetPasswordLinkExpirationPeriod); diff --git a/app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Customer/GroupAutoAssign.php b/app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Customer/GroupAutoAssign.php new file mode 100644 index 0000000000..b2c7c22d26 --- /dev/null +++ b/app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Customer/GroupAutoAssign.php @@ -0,0 +1,57 @@ + + */ +class Mage_Adminhtml_Model_System_Config_Backend_Customer_GroupAutoAssign extends Mage_Core_Model_Config_Data +{ + /** + * If merchant country is not in EU, VAT Validation should be disabled + * + * @return Mage_Core_Model_Abstract + */ + protected function _beforeSave() + { + $storeId = $this->getScopeId(); + $merchantCountry = Mage::getStoreConfig('general/store_information/merchant_country', $storeId); + + if (!Mage::helper('core')->isCountryInEU($merchantCountry, $storeId)) { + Mage::getConfig()->saveConfig( + Mage_Customer_Helper_Address::XML_PATH_VAT_VALIDATION_ENABLED, + 0, + $this->getScope(), + $storeId + ); + } + + return parent::_beforeSave(); + } +} diff --git a/app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Customer/Password/Link/Expirationperiod.php b/app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Customer/Password/Link/Expirationperiod.php index 90dbb07a12..c64ddee4d1 100644 --- a/app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Customer/Password/Link/Expirationperiod.php +++ b/app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Customer/Password/Link/Expirationperiod.php @@ -44,7 +44,8 @@ protected function _beforeSave() { parent::_beforeSave(); $resetPasswordLinkExpirationPeriod = (int) $this->getValue(); - if ($resetPasswordLinkExpirationPeriod < 0) { + // This value must be greater than 0 + if ($resetPasswordLinkExpirationPeriod < 1) { $resetPasswordLinkExpirationPeriod = (int) $this->getOldValue(); } $this->setValue((string) $resetPasswordLinkExpirationPeriod); diff --git a/app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Filename.php b/app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Filename.php new file mode 100644 index 0000000000..95a11f853a --- /dev/null +++ b/app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Filename.php @@ -0,0 +1,37 @@ +getValue(); + $value = basename($value); + $this->setValue($value); + return $this; + } +} diff --git a/app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Locale.php b/app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Locale.php index 4fa95f2b34..bf230e8af3 100644 --- a/app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Locale.php +++ b/app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Locale.php @@ -48,8 +48,11 @@ protected function _afterSave() $values = explode(',', $this->getValue()); $exceptions = array(); + foreach ($collection as $data) { $match = false; + $scopeName = Mage::helper('adminhtml')->__('Default scope'); + if (preg_match('/(base|default)$/', $data->getPath(), $match)) { if (!in_array($data->getValue(), $values)) { $currencyName = Mage::app()->getLocale()->currency($data->getValue())->getName(); @@ -76,11 +79,7 @@ protected function _afterSave() break; } - $exceptions[] = Mage::helper('adminhtml')->__('Currency "%s" is used as %s in %s.', - $currencyName, - $fieldName, - $scopeName - ); + $exceptions[] = Mage::helper('adminhtml')->__('Currency "%s" is used as %s in %s.', $currencyName, $fieldName, $scopeName); } } } diff --git a/app/code/core/Mage/Adminhtml/Model/System/Config/Source/Dev/Dbautoup.php b/app/code/core/Mage/Adminhtml/Model/System/Config/Source/Dev/Dbautoup.php index a1ef51b6ae..c30d1cba54 100644 --- a/app/code/core/Mage/Adminhtml/Model/System/Config/Source/Dev/Dbautoup.php +++ b/app/code/core/Mage/Adminhtml/Model/System/Config/Source/Dev/Dbautoup.php @@ -27,11 +27,10 @@ class Mage_Adminhtml_Model_System_Config_Source_Dev_Dbautoup { public function toOptionArray() { - $hlp = Mage::helper('adminhtml'); return array( - array('value'=>Mage_Core_Model_Resource::AUTO_UPDATE_ALWAYS, 'label'=>$hlp->__('Always (during development)')), - array('value'=>Mage_Core_Model_Resource::AUTO_UPDATE_ONCE, 'label'=>$hlp->__('Only Once (version upgrade)')), - array('value'=>Mage_Core_Model_Resource::AUTO_UPDATE_NEVER, 'label'=>$hlp->__('Never (production)')), + array('value'=>Mage_Core_Model_Resource::AUTO_UPDATE_ALWAYS, 'label' => Mage::helper('adminhtml')->__('Always (during development)')), + array('value'=>Mage_Core_Model_Resource::AUTO_UPDATE_ONCE, 'label' => Mage::helper('adminhtml')->__('Only Once (version upgrade)')), + array('value'=>Mage_Core_Model_Resource::AUTO_UPDATE_NEVER, 'label' => Mage::helper('adminhtml')->__('Never (production)')), ); } diff --git a/app/code/core/Mage/Adminhtml/Model/System/Store.php b/app/code/core/Mage/Adminhtml/Model/System/Store.php index e35d179e08..2e36c7ad1b 100644 --- a/app/code/core/Mage/Adminhtml/Model/System/Store.php +++ b/app/code/core/Mage/Adminhtml/Model/System/Store.php @@ -488,7 +488,7 @@ public function getStoreNamePath($storeId) if (isset($this->_storeCollection[$storeId])) { $data = $this->_storeCollection[$storeId]; $name .= $this->getWebsiteName($data->getWebsiteId()); - $name .= ($name ? '/' : '').$this->getGroupName($data->getGroupId()); + $name .= ($name ? '/' : '') . $this->getGroupName($data->getGroupId()); } } return $name; diff --git a/app/code/core/Mage/Adminhtml/controllers/Catalog/CategoryController.php b/app/code/core/Mage/Adminhtml/controllers/Catalog/CategoryController.php index 01d729a522..0d040515fb 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Catalog/CategoryController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Catalog/CategoryController.php @@ -107,7 +107,7 @@ public function editAction() $_prevStoreId = Mage::getSingleton('admin/session') ->getLastViewedStore(true); - if ($_prevStoreId != null && !$this->getRequest()->getQuery('isAjax')) { + if (!empty($_prevStoreId) && !$this->getRequest()->getQuery('isAjax')) { $params['store'] = $_prevStoreId; $redirect = true; } diff --git a/app/code/core/Mage/Adminhtml/controllers/Catalog/Product/Action/AttributeController.php b/app/code/core/Mage/Adminhtml/controllers/Catalog/Product/Action/AttributeController.php index 5767b7ab67..45bdca7127 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Catalog/Product/Action/AttributeController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Catalog/Product/Action/AttributeController.php @@ -107,7 +107,10 @@ public function saveAction() ->updateAttributes($this->_getHelper()->getProductIds(), $attributesData, $storeId); } if ($inventoryData) { + /** @var $stockItem Mage_CatalogInventory_Model_Stock_Item */ $stockItem = Mage::getModel('cataloginventory/stock_item'); + $stockItem->setProcessIndexEvents(false); + $stockItemSaved = false; foreach ($this->_getHelper()->getProductIds() as $productId) { $stockItem->setData(array()); @@ -123,8 +126,16 @@ public function saveAction() } if ($stockDataChanged) { $stockItem->save(); + $stockItemSaved = true; } } + + if ($stockItemSaved) { + Mage::getSingleton('index/indexer')->indexEvents( + Mage_CatalogInventory_Model_Stock_Item::ENTITY, + Mage_Index_Model_Event::TYPE_SAVE + ); + } } if ($websiteAddData || $websiteRemoveData) { @@ -152,8 +163,7 @@ public function saveAction() } $this->_getSession()->addSuccess( - $this->__('Total of %d record(s) were updated', - count($this->_getHelper()->getProductIds())) + $this->__('Total of %d record(s) were updated', count($this->_getHelper()->getProductIds())) ); } catch (Mage_Core_Exception $e) { diff --git a/app/code/core/Mage/Adminhtml/controllers/Catalog/Product/AttributeController.php b/app/code/core/Mage/Adminhtml/controllers/Catalog/Product/AttributeController.php index 90af67c2d1..fe4ee451e1 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Catalog/Product/AttributeController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Catalog/Product/AttributeController.php @@ -158,27 +158,9 @@ protected function _filterPostData($data) //labels foreach ($data['frontend_label'] as & $value) { if ($value) { - $value = $helperCatalog->escapeHtml($value); + $value = $helperCatalog->stripTags($value); } } - //options - if (!empty($data['option']['value'])) { - foreach ($data['option']['value'] as &$options) { - foreach ($options as &$label) { - $label = $helperCatalog->escapeHtml($label); - } - } - } - //default value - if (!empty($data['default_value'])) { - $data['default_value'] = $helperCatalog->escapeHtml($data['default_value']); - } - if (!empty($data['default_value_text'])) { - $data['default_value_text'] = $helperCatalog->escapeHtml($data['default_value_text']); - } - if (!empty($data['default_value_textarea'])) { - $data['default_value_textarea'] = $helperCatalog->escapeHtml($data['default_value_textarea']); - } } return $data; } @@ -203,7 +185,8 @@ public function saveAction() $validatorAttrCode = new Zend_Validate_Regex(array('pattern' => '/^[a-z][a-z_0-9]{1,254}$/')); if (!$validatorAttrCode->isValid($data['attribute_code'])) { $session->addError( - $helper->__('Attribute code is invalid. Please use only letters (a-z), numbers (0-9) or underscore(_) in this field, first character should be a letter.')); + Mage::helper('catalog')->__('Attribute code is invalid. Please use only letters (a-z), numbers (0-9) or underscore(_) in this field, first character should be a letter.') + ); $this->_redirect('*/*/edit', array('attribute_id' => $id, '_current' => true)); return; } diff --git a/app/code/core/Mage/Adminhtml/controllers/Catalog/ProductController.php b/app/code/core/Mage/Adminhtml/controllers/Catalog/ProductController.php index 5370900190..4917332e54 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Catalog/ProductController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Catalog/ProductController.php @@ -552,12 +552,7 @@ protected function _initProductSave() $product = $this->_initProduct(); $productData = $this->getRequest()->getPost('product'); if ($productData) { - if (!isset($productData['stock_data']['use_config_manage_stock'])) { - $productData['stock_data']['use_config_manage_stock'] = 0; - } - if (isset($productData['stock_data']['qty']) && (float)$productData['stock_data']['qty'] > self::MAX_QTY_VALUE) { - $productData['stock_data']['qty'] = self::MAX_QTY_VALUE; - } + $this->_filterStockData($productData['stock_data']); } /** @@ -669,6 +664,23 @@ protected function _initProductSave() return $product; } + /** + * Filter product stock data + * + * @param array $stockData + */ + protected function _filterStockData(&$stockData) { + if (!isset($stockData['use_config_manage_stock'])) { + $stockData['use_config_manage_stock'] = 0; + } + if (isset($stockData['qty']) && (float)$stockData['qty'] > self::MAX_QTY_VALUE) { + $stockData['qty'] = self::MAX_QTY_VALUE; + } + if (isset($stockData['min_qty']) && (int)$stockData['min_qty'] < 0) { + $stockData['min_qty'] = 0; + } + } + public function categoriesJsonAction() { $product = $this->_initProduct(); @@ -691,9 +703,8 @@ public function saveAction() $data = $this->getRequest()->getPost(); if ($data) { - if (!isset($data['product']['stock_data']['use_config_manage_stock'])) { - $data['product']['stock_data']['use_config_manage_stock'] = 0; - } + $this->_filterStockData($data['product']['stock_data']); + $product = $this->_initProductSave(); try { diff --git a/app/code/core/Mage/Adminhtml/controllers/Catalog/SearchController.php b/app/code/core/Mage/Adminhtml/controllers/Catalog/SearchController.php index fe1cf4ffc2..62a413db3b 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Catalog/SearchController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Catalog/SearchController.php @@ -178,9 +178,7 @@ public function massDeleteAction() $model->delete(); } Mage::getSingleton('adminhtml/session')->addSuccess( - Mage::helper('adminhtml')->__( - 'Total of %d record(s) were deleted', count($searchIds) - ) + Mage::helper('adminhtml')->__('Total of %d record(s) were deleted', count($searchIds)) ); } catch (Exception $e) { Mage::getSingleton('adminhtml/session')->addError($e->getMessage()); diff --git a/app/code/core/Mage/Adminhtml/controllers/Checkout/AgreementController.php b/app/code/core/Mage/Adminhtml/controllers/Checkout/AgreementController.php index f55eb7ffef..a92e91c0be 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Checkout/AgreementController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Checkout/AgreementController.php @@ -54,11 +54,13 @@ public function editAction() $id = $this->getRequest()->getParam('id'); $agreementModel = Mage::getModel('checkout/agreement'); - $hlp = Mage::helper('checkout'); + if ($id) { $agreementModel->load($id); if (!$agreementModel->getId()) { - Mage::getSingleton('adminhtml/session')->addError($hlp->__('This condition no longer exists.')); + Mage::getSingleton('adminhtml/session')->addError( + Mage::helper('checkout')->__('This condition no longer exists.') + ); $this->_redirect('*/*/'); return; } @@ -74,7 +76,7 @@ public function editAction() Mage::register('checkout_agreement', $agreementModel); $this->_initAction() - ->_addBreadcrumb($id ? $hlp->__('Edit Condition') : $hlp->__('New Condition'), $id ? $hlp->__('Edit Condition') : $hlp->__('New Condition')) + ->_addBreadcrumb($id ? Mage::helper('checkout')->__('Edit Condition') : Mage::helper('checkout')->__('New Condition'), $id ? Mage::helper('checkout')->__('Edit Condition') : Mage::helper('checkout')->__('New Condition')) ->_addContent($this->getLayout()->createBlock('adminhtml/checkout_agreement_edit')->setData('action', $this->getUrl('*/*/save'))) ->renderLayout(); } diff --git a/app/code/core/Mage/Adminhtml/controllers/Customer/System/Config/ValidatevatController.php b/app/code/core/Mage/Adminhtml/controllers/Customer/System/Config/ValidatevatController.php new file mode 100644 index 0000000000..75be29f450 --- /dev/null +++ b/app/code/core/Mage/Adminhtml/controllers/Customer/System/Config/ValidatevatController.php @@ -0,0 +1,85 @@ + + */ +class Mage_Adminhtml_Customer_System_Config_ValidatevatController extends Mage_Adminhtml_Controller_Action +{ + /** + * Perform customer VAT ID validation + * + * @return Varien_Object + */ + protected function _validate() + { + return Mage::helper('customer')->checkVatNumber( + $this->getRequest()->getParam('country'), + $this->getRequest()->getParam('vat') + ); + } + + /** + * Check whether vat is valid + * + * @return void + */ + public function validateAction() + { + $result = $this->_validate(); + $this->getResponse()->setBody((int)$result->getIsValid()); + } + + /** + * Retrieve validation result as JSON + * + * @return void + */ + public function validateAdvancedAction() + { + /** @var $coreHelper Mage_Core_Helper_Data */ + $coreHelper = Mage::helper('core'); + + $result = $this->_validate(); + $valid = $result->getIsValid(); + $success = $result->getRequestSuccess(); + + $groupId = Mage::helper('customer')->getCustomerGroupIdBasedOnVatNumber( + $this->getRequest()->getParam('country'), $result + ); + + $body = $coreHelper->jsonEncode(array( + 'valid' => $valid, + 'group' => $groupId, + 'success' => $success + )); + $this->getResponse()->setBody($body); + } +} diff --git a/app/code/core/Mage/Adminhtml/controllers/CustomerController.php b/app/code/core/Mage/Adminhtml/controllers/CustomerController.php index 7e43cb9ab6..c5296bbb9c 100644 --- a/app/code/core/Mage/Adminhtml/controllers/CustomerController.php +++ b/app/code/core/Mage/Adminhtml/controllers/CustomerController.php @@ -86,7 +86,7 @@ public function indexAction() public function gridAction() { $this->loadLayout(); - $this->getResponse()->setBody($this->getLayout()->createBlock('adminhtml/customer_grid')->toHtml()); + $this->renderLayout(); } /** @@ -186,21 +186,27 @@ public function saveAction() { $data = $this->getRequest()->getPost(); if ($data) { - $redirectBack = $this->getRequest()->getParam('back', false); + $redirectBack = $this->getRequest()->getParam('back', false); $this->_initCustomer('customer_id'); - /* @var $customer Mage_Customer_Model_Customer */ + /** @var $customer Mage_Customer_Model_Customer */ $customer = Mage::registry('current_customer'); - /* @var $customerForm Mage_Customer_Model_Form */ + /** @var $customerForm Mage_Customer_Model_Form */ $customerForm = Mage::getModel('customer/form'); $customerForm->setEntity($customer) ->setFormCode('adminhtml_customer') ->ignoreInvisible(false) ; - $formData = $customerForm->extractData($this->getRequest(), 'account'); - $errors = $customerForm->validateData($formData); + $formData = $customerForm->extractData($this->getRequest(), 'account'); + + // Handle 'disable auto_group_change' attribute + if (isset($formData['disable_auto_group_change'])) { + $formData['disable_auto_group_change'] = empty($formData['disable_auto_group_change']) ? '0' : '1'; + } + + $errors = $customerForm->validateData($formData); if ($errors !== true) { foreach ($errors as $error) { $this->_getSession()->addError($error); @@ -212,14 +218,14 @@ public function saveAction() $customerForm->compactData($formData); - // unset template data + // Unset template data if (isset($data['address']['_template_'])) { unset($data['address']['_template_']); } $modifiedAddresses = array(); if (!empty($data['address'])) { - /* @var $addressForm Mage_Customer_Model_Form */ + /** @var $addressForm Mage_Customer_Model_Form */ $addressForm = Mage::getModel('customer/form'); $addressForm->setFormCode('adminhtml_customer_address')->ignoreInvisible(false); @@ -232,7 +238,10 @@ public function saveAction() $requestScope = sprintf('address/%s', $index); $formData = $addressForm->setEntity($address) ->extractData($this->getRequest(), $requestScope); - $errors = $addressForm->validateData($formData); + + $address->setIsDefaultBilling($data['account']['default_billing'] == $index); + + $errors = $addressForm->validateData($formData); if ($errors !== true) { foreach ($errors as $error) { $this->_getSession()->addError($error); @@ -257,7 +266,7 @@ public function saveAction() } } - // default billing and shipping + // Default billing and shipping if (isset($data['account']['default_billing'])) { $customer->setData('default_billing', $data['account']['default_billing']); } @@ -268,17 +277,15 @@ public function saveAction() $customer->setData('confirmation', $data['account']['confirmation']); } - // not modified customer addresses mark for delete + // Mark not modified customer addresses for delete foreach ($customer->getAddressesCollection() as $customerAddress) { if ($customerAddress->getId() && !in_array($customerAddress->getId(), $modifiedAddresses)) { $customerAddress->setData('_deleted', true); } } - if (isset($data['subscription'])) { - $customer->setIsSubscribed(true); - } else { - $customer->setIsSubscribed(false); + if (Mage::getSingleton('admin/session')->isAllowed('customer/newsletter')) { + $customer->setIsSubscribed(isset($data['subscription'])); } if (isset($data['account']['sendemail_store_id'])) { @@ -288,7 +295,7 @@ public function saveAction() $isNewCustomer = $customer->isObjectNew(); try { $sendPassToEmail = false; - // force new customer active + // Force new customer confirmation if ($isNewCustomer) { $customer->setPassword($data['account']['password']); $customer->setForceConfirmed(true); @@ -305,14 +312,13 @@ public function saveAction() $customer->save(); - // send welcome email + // Send welcome email if ($customer->getWebsiteId() && (isset($data['account']['sendemail']) || $sendPassToEmail)) { $storeId = $customer->getSendemailStoreId(); if ($isNewCustomer) { $customer->sendNewAccountEmail('registered', '', $storeId); - } - // confirm not confirmed customer - else if ((!$customer->getConfirmation())) { + } elseif ((!$customer->getConfirmation())) { + // Confirm not confirmed customer $customer->sendNewAccountEmail('confirmed', '', $storeId); } } @@ -336,8 +342,8 @@ public function saveAction() if ($redirectBack) { $this->_redirect('*/*/edit', array( - 'id' => $customer->getId(), - '_current'=>true + 'id' => $customer->getId(), + '_current' => true )); return; } @@ -401,7 +407,8 @@ protected function _sendUploadResponse($fileName, $content, $contentType='applic */ public function ordersAction() { $this->_initCustomer(); - $this->getResponse()->setBody($this->getLayout()->createBlock('adminhtml/customer_edit_tab_orders')->toHtml()); + $this->loadLayout(); + $this->renderLayout(); } /** @@ -410,8 +417,8 @@ public function ordersAction() { */ public function lastOrdersAction() { $this->_initCustomer(); - $this->getResponse()->setBody( - $this->getLayout()->createBlock('adminhtml/customer_edit_tab_view_orders')->toHtml()); + $this->loadLayout(); + $this->renderLayout(); } /** @@ -425,8 +432,8 @@ public function newsletterAction() ->loadByCustomer(Mage::registry('current_customer')); Mage::register('subscriber', $subscriber); - $this->getResponse()->setBody( - $this->getLayout()->createBlock('adminhtml/customer_edit_tab_newsletter_grid')->toHtml()); + $this->loadLayout(); + $this->renderLayout(); } public function wishlistAction() @@ -459,8 +466,8 @@ public function wishlistAction() public function viewWishlistAction() { $this->_initCustomer(); - $this->getResponse()->setBody( - $this->getLayout()->createBlock('adminhtml/customer_edit_tab_view_wishlist')->toHtml()); + $this->loadLayout(); + $this->renderLayout(); } /** @@ -486,10 +493,9 @@ public function cartAction() } } - $this->getResponse()->setBody( - $this->getLayout()->createBlock('adminhtml/customer_edit_tab_cart', 'admin.customer.view.cart', - array('website_id'=>$websiteId))->toHtml() - ); + $this->loadLayout(); + $this->getLayout()->getBlock('admin.customer.view.edit.cart')->setWebsiteId($websiteId); + $this->renderLayout(); } /** @@ -499,57 +505,62 @@ public function cartAction() public function viewCartAction() { $this->_initCustomer(); - - $this->getResponse()->setBody( - $this->getLayout()->createBlock('adminhtml/customer_edit_tab_view_cart', 'admin.customer.view.cart') - ->setWebsiteId($this->getRequest()->getParam('website_id')) - ->toHtml() - ); + $layout = $this->loadLayout() + ->getLayout() + ->getBlock('admin.customer.view.cart') + ->setWebsiteId(); + $this->renderLayout(); } /** * Get shopping carts from all websites for specified client * - * @return string */ public function cartsAction() { $this->_initCustomer(); - $this->getResponse()->setBody( - $this->getLayout()->createBlock('adminhtml/customer_edit_tab_carts', 'admin.customer.carts')->toHtml() - ); + $this->loadLayout(); + $this->renderLayout(); } + /** + * Get customer's product reviews list + * + */ public function productReviewsAction() { $this->_initCustomer(); - $this->getResponse()->setBody( - $this->getLayout()->createBlock('adminhtml/customer_edit_tab_reviews', 'admin.customer.reviews') - ->setCustomerId(Mage::registry('current_customer')->getId()) - ->setUseAjax(true) - ->toHtml() - ); + $this->loadLayout() + ->getLayout() + ->getBlock('admin.customer.reviews') + ->setCustomerId(Mage::registry('current_customer')->getId()) + ->setUseAjax(true); + $this->renderLayout(); } + /** + * Get customer's tags list + * + */ public function productTagsAction() { $this->_initCustomer(); - $this->getResponse()->setBody( - $this->getLayout()->createBlock('adminhtml/customer_edit_tab_tag', 'admin.customer.tags') - ->setCustomerId(Mage::registry('current_customer')->getId()) - ->setUseAjax(true) - ->toHtml() - ); + $this->loadLayout() + ->getLayout() + ->getBlock('admin.customer.tags') + ->setCustomerId(Mage::registry('current_customer')->getId()) + ->setUseAjax(true); + $this->renderLayout(); } public function tagGridAction() { $this->_initCustomer(); - $this->getResponse()->setBody( - $this->getLayout()->createBlock('adminhtml/customer_edit_tab_tag', 'admin.customer.tags') - ->setCustomerId(Mage::registry('current_customer')) - ->toHtml() + $this->loadLayout(); + $this->getLayout()->getBlock('admin.customer.tags')->setCustomerId( + Mage::registry('current_customer') ); + $this->renderLayout(); } public function validateAction() @@ -650,9 +661,7 @@ public function massSubscribeAction() $customer->save(); } Mage::getSingleton('adminhtml/session')->addSuccess( - Mage::helper('adminhtml')->__( - 'Total of %d record(s) were updated.', count($customersIds) - ) + Mage::helper('adminhtml')->__('Total of %d record(s) were updated.', count($customersIds)) ); } catch (Exception $e) { Mage::getSingleton('adminhtml/session')->addError($e->getMessage()); @@ -674,9 +683,7 @@ public function massUnsubscribeAction() $customer->save(); } Mage::getSingleton('adminhtml/session')->addSuccess( - Mage::helper('adminhtml')->__( - 'Total of %d record(s) were updated.', count($customersIds) - ) + Mage::helper('adminhtml')->__('Total of %d record(s) were updated.', count($customersIds)) ); } catch (Exception $e) { Mage::getSingleton('adminhtml/session')->addError($e->getMessage()); @@ -700,9 +707,7 @@ public function massDeleteAction() ->delete(); } Mage::getSingleton('adminhtml/session')->addSuccess( - Mage::helper('adminhtml')->__( - 'Total of %d record(s) were deleted.', count($customersIds) - ) + Mage::helper('adminhtml')->__('Total of %d record(s) were deleted.', count($customersIds)) ); } catch (Exception $e) { Mage::getSingleton('adminhtml/session')->addError($e->getMessage()); @@ -725,9 +730,7 @@ public function massAssignGroupAction() $customer->save(); } Mage::getSingleton('adminhtml/session')->addSuccess( - Mage::helper('adminhtml')->__( - 'Total of %d record(s) were updated.', count($customersIds) - ) + Mage::helper('adminhtml')->__('Total of %d record(s) were updated.', count($customersIds)) ); } catch (Exception $e) { Mage::getSingleton('adminhtml/session')->addError($e->getMessage()); diff --git a/app/code/core/Mage/Adminhtml/controllers/DashboardController.php b/app/code/core/Mage/Adminhtml/controllers/DashboardController.php index 5edfd5d892..709d5095a2 100644 --- a/app/code/core/Mage/Adminhtml/controllers/DashboardController.php +++ b/app/code/core/Mage/Adminhtml/controllers/DashboardController.php @@ -43,19 +43,34 @@ public function indexAction() $this->renderLayout(); } + /** + * Gets most viewed products list + * + */ public function productsViewedAction() { - $this->getResponse()->setBody($this->getLayout()->createBlock('adminhtml/dashboard_tab_products_viewed')->toHtml()); + $this->loadLayout(); + $this->renderLayout(); } + /** + * Gets latest customers list + * + */ public function customersNewestAction() { - $this->getResponse()->setBody($this->getLayout()->createBlock('adminhtml/dashboard_tab_customers_newest')->toHtml()); + $this->loadLayout(); + $this->renderLayout(); } + /** + * Gets the list of most active customers + * + */ public function customersMostAction() { - $this->getResponse()->setBody($this->getLayout()->createBlock('adminhtml/dashboard_tab_customers_most')->toHtml()); + $this->loadLayout(); + $this->renderLayout(); } public function ajaxBlockAction() diff --git a/app/code/core/Mage/Adminhtml/controllers/IndexController.php b/app/code/core/Mage/Adminhtml/controllers/IndexController.php index 20960f7537..7bf1be892f 100644 --- a/app/code/core/Mage/Adminhtml/controllers/IndexController.php +++ b/app/code/core/Mage/Adminhtml/controllers/IndexController.php @@ -76,15 +76,10 @@ public function loginAction() return; } $loginData = $this->getRequest()->getParam('login'); - $data = array(); + $username = (is_array($loginData) && array_key_exists('username', $loginData)) ? $loginData['username'] : null; - if(is_array($loginData) && array_key_exists('username', $loginData)) { - $data['username'] = $loginData['username']; - } else { - $data['username'] = null; - } - - $this->_outTemplate('login', $data); + $this->loadLayout(); + $this->renderLayout(); } /** @@ -229,36 +224,41 @@ protected function _getDeniedIframe() */ public function forgotpasswordAction() { - $email = $this->getRequest()->getParam('email'); + $email = (string) $this->getRequest()->getParam('email'); $params = $this->getRequest()->getParams(); + if (!empty($email) && !empty($params)) { - $collection = Mage::getResourceModel('admin/user_collection'); - /** @var $collection Mage_Admin_Model_Mysql4_User_Collection */ - $collection->addFieldToFilter('email', $email); - $collection->load(false); - - if ($collection->getSize() > 0) { - foreach ($collection as $item) { - $user = Mage::getModel('admin/user')->load($item->getId()); - if ($user->getId()) { - $newResetPasswordLinkToken = Mage::helper('admin')->generateResetPasswordLinkToken(); - $user->changeResetPasswordLinkToken($newResetPasswordLinkToken); - $user->save(); - $user->sendPasswordResetConfirmationEmail(); + // Validate received data to be an email address + if (Zend_Validate::is($email, 'EmailAddress')) { + $collection = Mage::getResourceModel('admin/user_collection'); + /** @var $collection Mage_Admin_Model_Resource_User_Collection */ + $collection->addFieldToFilter('email', $email); + $collection->load(false); + + if ($collection->getSize() > 0) { + foreach ($collection as $item) { + $user = Mage::getModel('admin/user')->load($item->getId()); + if ($user->getId()) { + $newResetPasswordLinkToken = Mage::helper('admin')->generateResetPasswordLinkToken(); + $user->changeResetPasswordLinkToken($newResetPasswordLinkToken); + $user->save(); + $user->sendPasswordResetConfirmationEmail(); + } + break; } - break; } + $this->_getSession() + ->addSuccess(Mage::helper('adminhtml')->__('If there is an account associated with %s you will receive an email with a link to reset your password.', Mage::helper('adminhtml')->escapeHtml($email))); + $this->_redirect('*/*/login'); + return; + } else { + $this->_getSession()->addError($this->__('Invalid email address.')); } - $this->_getSession() - ->addSuccess(Mage::helper('adminhtml')->__('If there is an account associated with %s you will receive an email with a link to reset your password.', Mage::helper('adminhtml')->htmlEscape($email))); } elseif (!empty($params)) { $this->_getSession()->addError(Mage::helper('adminhtml')->__('The email address is empty.')); } - - $data = array( - 'email' => $email - ); - $this->_outTemplate('forgotpassword', $data); + $this->loadLayout(); + $this->renderLayout(); } /** @@ -279,7 +279,7 @@ public function resetPasswordAction() $this->_outTemplate('resetforgottenpassword', $data); } catch (Exception $exception) { $this->_getSession()->addError(Mage::helper('adminhtml')->__('Your password reset link has expired.')); - $this->_redirect('*/*/'); + $this->_redirect('*/*/forgotpassword', array('_nosecret' => true)); } } @@ -334,13 +334,11 @@ public function resetPasswordPostAction() $user->setRpToken(null); $user->setRpTokenCreatedAt(null); $user->setPasswordConfirmation(null); - // Force password change - $user->setForceNewPassword(true); $user->save(); $this->_getSession()->addSuccess(Mage::helper('adminhtml')->__('Your password has been updated.')); $this->_redirect('*/*/login'); } catch (Exception $exception) { - $this->_getSession()->addException($exception, $this->__('Cannot save a new password.')); + $this->_getSession()->addError($exception->getMessage()); $data = array( 'userId' => $userId, 'resetPasswordLinkToken' => $resetPasswordLinkToken diff --git a/app/code/core/Mage/Adminhtml/controllers/Newsletter/SubscriberController.php b/app/code/core/Mage/Adminhtml/controllers/Newsletter/SubscriberController.php index cf95f21cd9..580512668f 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Newsletter/SubscriberController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Newsletter/SubscriberController.php @@ -117,9 +117,7 @@ public function massUnsubscribeAction() $subscriber->unsubscribe(); } Mage::getSingleton('adminhtml/session')->addSuccess( - Mage::helper('adminhtml')->__( - 'Total of %d record(s) were updated', count($subscribersIds) - ) + Mage::helper('adminhtml')->__('Total of %d record(s) were updated', count($subscribersIds)) ); } catch (Exception $e) { Mage::getSingleton('adminhtml/session')->addError($e->getMessage()); @@ -142,9 +140,7 @@ public function massDeleteAction() $subscriber->delete(); } Mage::getSingleton('adminhtml/session')->addSuccess( - Mage::helper('adminhtml')->__( - 'Total of %d record(s) were deleted', count($subscribersIds) - ) + Mage::helper('adminhtml')->__('Total of %d record(s) were deleted', count($subscribersIds)) ); } catch (Exception $e) { Mage::getSingleton('adminhtml/session')->addError($e->getMessage()); diff --git a/app/code/core/Mage/Adminhtml/controllers/Permissions/RoleController.php b/app/code/core/Mage/Adminhtml/controllers/Permissions/RoleController.php index b4cdbe5533..7e8efef410 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Permissions/RoleController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Permissions/RoleController.php @@ -124,7 +124,9 @@ public function editRoleAction() ->setRoleInfo($role) ->setTemplate('permissions/roleinfo.phtml') ); - $this->_addJs($this->getLayout()->createBlock('adminhtml/template')->setTemplate('permissions/role_users_grid_js.phtml')); + $this->_addJs( + $this->getLayout()->createBlock('adminhtml/template')->setTemplate('permissions/role_users_grid_js.phtml') + ); $this->renderLayout(); } @@ -183,10 +185,15 @@ public function saveRoleAction() } try { - $role->setName($this->getRequest()->getParam('rolename', false)) + $roleName = Mage::helper('adminhtml')->stripTags($this->getRequest()->getParam('rolename', false)); + + $role->setName($roleName) ->setPid($this->getRequest()->getParam('parent_id', false)) ->setRoleType('G'); - Mage::dispatchEvent('admin_permissions_role_prepare_save', array('object' => $role, 'request' => $this->getRequest())); + Mage::dispatchEvent( + 'admin_permissions_role_prepare_save', + array('object' => $role, 'request' => $this->getRequest()) + ); $role->save(); Mage::getModel("admin/rules") @@ -221,7 +228,9 @@ public function saveRoleAction() */ public function editrolegridAction() { - $this->getResponse()->setBody($this->getLayout()->createBlock('adminhtml/permissions_role_grid_user')->toHtml()); + $this->getResponse()->setBody( + $this->getLayout()->createBlock('adminhtml/permissions_role_grid_user')->toHtml() + ); } /** diff --git a/app/code/core/Mage/Adminhtml/controllers/Promo/CatalogController.php b/app/code/core/Mage/Adminhtml/controllers/Promo/CatalogController.php index 9723ccb3df..1141958a1e 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Promo/CatalogController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Promo/CatalogController.php @@ -283,17 +283,15 @@ public function newActionHtmlAction() */ public function applyRulesAction() { + $errorMessage = Mage::helper('catalogrule')->__('Unable to apply rules.'); try { Mage::getModel('catalogrule/rule')->applyAll(); Mage::app()->removeCache('catalog_rules_dirty'); - Mage::getSingleton('adminhtml/session')->addSuccess( - Mage::helper('catalogrule')->__('The rules have been applied.') - ); + $this->_getSession()->addSuccess(Mage::helper('catalogrule')->__('The rules have been applied.')); + } catch (Mage_Core_Exception $e) { + $this->_getSession()->addError($errorMessage . ' ' . $e->getMessage()); } catch (Exception $e) { - Mage::getSingleton('adminhtml/session')->addError( - Mage::helper('catalogrule')->__('Unable to apply rules.') - ); - throw $e; + $this->_getSession()->addError($errorMessage); } $this->_redirect('*/*'); } diff --git a/app/code/core/Mage/Adminhtml/controllers/Promo/QuoteController.php b/app/code/core/Mage/Adminhtml/controllers/Promo/QuoteController.php index d2d16501ca..f222b22566 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Promo/QuoteController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Promo/QuoteController.php @@ -32,9 +32,14 @@ protected function _initRule() $this->_title($this->__('Promotions'))->_title($this->__('Shopping Cart Price Rules')); Mage::register('current_promo_quote_rule', Mage::getModel('salesrule/rule')); - if ($id = (int) $this->getRequest()->getParam('id')) { - Mage::registry('current_promo_quote_rule') - ->load($id); + $id = (int)$this->getRequest()->getParam('id'); + + if (!$id && $this->getRequest()->getParam('rule_id')) { + $id = (int)$this->getRequest()->getParam('rule_id'); + } + + if ($id) { + Mage::registry('current_promo_quote_rule')->load($id); } } @@ -110,6 +115,7 @@ public function saveAction() { if ($this->getRequest()->getPost()) { try { + /** @var $model Mage_SalesRule_Model_Rule */ $model = Mage::getModel('salesrule/rule'); Mage::dispatchEvent( 'adminhtml_controller_salesrule_prepare_save', @@ -159,6 +165,9 @@ public function saveAction() unset($data['rule']); $model->loadPost($data); + $useAutoGeneration = (int)!empty($data['use_auto_generation']); + $model->setUseAutoGeneration($useAutoGeneration); + $session->setPageData($model->getData()); $model->save(); @@ -269,6 +278,128 @@ public function gridAction() $this->_initRule()->loadLayout()->renderLayout(); } + /** + * Coupon codes grid + */ + public function couponsGridAction() + { + $this->_initRule(); + $this->loadLayout()->renderLayout(); + } + + /** + * Export coupon codes as excel xml file + * + * @return void + */ + public function exportCouponsXmlAction() + { + $this->_initRule(); + $rule = Mage::registry('current_promo_quote_rule'); + if ($rule->getId()) { + $fileName = 'coupon_codes.xml'; + $content = $this->getLayout() + ->createBlock('adminhtml/promo_quote_edit_tab_coupons_grid') + ->getExcelFile($fileName); + $this->_prepareDownloadResponse($fileName, $content); + } else { + $this->_redirect('*/*/detail', array('_current' => true)); + return; + } + } + + /** + * Export coupon codes as CSV file + * + * @return void + */ + public function exportCouponsCsvAction() + { + $this->_initRule(); + $rule = Mage::registry('current_promo_quote_rule'); + if ($rule->getId()) { + $fileName = 'coupon_codes.csv'; + $content = $this->getLayout() + ->createBlock('adminhtml/promo_quote_edit_tab_coupons_grid') + ->getCsvFile(); + $this->_prepareDownloadResponse($fileName, $content); + } else { + $this->_redirect('*/*/detail', array('_current' => true)); + return; + } + } + + /** + * Coupons mass delete action + */ + public function couponsMassDeleteAction() + { + $this->_initRule(); + $rule = Mage::registry('current_promo_quote_rule'); + + if (!$rule->getId()) { + $this->_forward('noRoute'); + } + + $codesIds = $this->getRequest()->getParam('ids'); + + if (is_array($codesIds)) { + + $couponsCollection = Mage::getResourceModel('salesrule/coupon_collection') + ->addFieldToFilter('coupon_id', array('in' => $codesIds)); + + foreach ($couponsCollection as $coupon) { + $coupon->delete(); + } + } + } + + /** + * Generate Coupons action + */ + public function generateAction() + { + if (!$this->getRequest()->isAjax()) { + $this->_forward('noRoute'); + return; + } + $result = array(); + $this->_initRule(); + + /** @var $rule Mage_SalesRule_Model_Rule */ + $rule = Mage::registry('current_promo_quote_rule'); + + if (!$rule->getId()) { + $result['error'] = Mage::helper('salesrule')->__('Rule is not defined'); + } else { + try { + $data = $this->getRequest()->getParams(); + if (!empty($data['to_date'])) { + $data = array_merge($data, $this->_filterDates($data, array('to_date'))); + } + + /** @var $generator Mage_SalesRule_Model_Coupon_Massgenerator */ + $generator = $rule->getCouponMassGenerator(); + if (!$generator->validateData($data)) { + $result['error'] = Mage::helper('salesrule')->__('Not valid data provided'); + } else { + $generator->setData($data); + $generator->generatePool(); + $generated = $generator->getGeneratedCount(); + $this->_getSession()->addSuccess(Mage::helper('salesrule')->__('%s Coupon(s) generated successfully', $generated)); + $this->_initLayoutMessages('adminhtml/session'); + $result['messages'] = $this->getLayout()->getMessagesBlock()->getGroupedHtml(); + } + } catch (Mage_Core_Exception $e) { + $result['error'] = $e->getMessage(); + } catch (Exception $e) { + $result['error'] = Mage::helper('salesrule')->__('An error occurred while generating coupons. Please review the log and try again.'); + Mage::logException($e); + } + } + $this->getResponse()->setBody(Mage::helper('core')->jsonEncode($result)); + } + /** * Chooser source action */ @@ -281,6 +412,10 @@ public function chooserAction() $this->getResponse()->setBody($chooserBlock->toHtml()); } + /** + * Returns result of current user permission check on resource and privilege + * @return boolean + */ protected function _isAllowed() { return Mage::getSingleton('admin/session')->isAllowed('promo/quote'); diff --git a/app/code/core/Mage/Adminhtml/controllers/Report/ProductController.php b/app/code/core/Mage/Adminhtml/controllers/Report/ProductController.php index 4d0586eda8..80d56c5518 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Report/ProductController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Report/ProductController.php @@ -32,21 +32,17 @@ * @package Mage_Adminhtml * @author Magento Core Team */ -class Mage_Adminhtml_Report_ProductController extends Mage_Adminhtml_Controller_Action +class Mage_Adminhtml_Report_ProductController extends Mage_Adminhtml_Controller_Report_Abstract { /** - * init + * Add report/products breadcrumbs * * @return Mage_Adminhtml_Report_ProductController */ public function _initAction() { - $act = $this->getRequest()->getActionName(); - if(!$act) - $act = 'default'; - $this->loadLayout() - ->_addBreadcrumb(Mage::helper('reports')->__('Reports'), Mage::helper('reports')->__('Reports')) - ->_addBreadcrumb(Mage::helper('reports')->__('Products'), Mage::helper('reports')->__('Products')); + parent::_initAction(); + $this->_addBreadcrumb(Mage::helper('reports')->__('Products'), Mage::helper('reports')->__('Products')); return $this; } @@ -131,15 +127,23 @@ public function exportSoldExcelAction() */ public function viewedAction() { - $this->_title($this->__('Reports')) - ->_title($this->__('Products')) - ->_title($this->__('Most Viewed')); + $this->_title($this->__('Reports'))->_title($this->__('Products'))->_title($this->__('Most Viewed')); + + $this->_showLastExecutionTime(Mage_Reports_Model_Flag::REPORT_PRODUCT_VIEWED_FLAG_CODE, 'viewed'); $this->_initAction() - ->_setActiveMenu('report/product/viewed') - ->_addBreadcrumb(Mage::helper('reports')->__('Most Viewed'), Mage::helper('reports')->__('Most Viewed')) - ->_addContent($this->getLayout()->createBlock('adminhtml/report_product_viewed')) - ->renderLayout(); + ->_setActiveMenu('report/products/viewed') + ->_addBreadcrumb(Mage::helper('adminhtml')->__('Products Most Viewed Report'), Mage::helper('adminhtml')->__('Products Most Viewed Report')); + + $gridBlock = $this->getLayout()->getBlock('report_product_viewed.grid'); + $filterFormBlock = $this->getLayout()->getBlock('grid.filter.form'); + + $this->_initReportAction(array( + $gridBlock, + $filterFormBlock + )); + + $this->renderLayout(); } /** @@ -149,10 +153,9 @@ public function viewedAction() public function exportViewedCsvAction() { $fileName = 'products_mostviewed.csv'; - $content = $this->getLayout()->createBlock('adminhtml/report_product_viewed_grid') - ->getCsv(); - - $this->_prepareDownloadResponse($fileName, $content); + $grid = $this->getLayout()->createBlock('adminhtml/report_product_viewed_grid'); + $this->_initReportAction($grid); + $this->_prepareDownloadResponse($fileName, $grid->getCsvFile()); } /** @@ -162,10 +165,9 @@ public function exportViewedCsvAction() public function exportViewedExcelAction() { $fileName = 'products_mostviewed.xml'; - $content = $this->getLayout()->createBlock('adminhtml/report_product_viewed_grid') - ->getExcel($fileName); - - $this->_prepareDownloadResponse($fileName, $content); + $grid = $this->getLayout()->createBlock('adminhtml/report_product_viewed_grid'); + $this->_initReportAction($grid); + $this->_prepareDownloadResponse($fileName, $grid->getExcelFile($fileName)); } /** diff --git a/app/code/core/Mage/Adminhtml/controllers/Report/SalesController.php b/app/code/core/Mage/Adminhtml/controllers/Report/SalesController.php index 8a5b56332a..ab97eb8df7 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Report/SalesController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Report/SalesController.php @@ -29,49 +29,19 @@ * * @category Mage * @package Mage_Adminhtml - * @author Magento Core Team + * @author Magento Core Team */ -class Mage_Adminhtml_Report_SalesController extends Mage_Adminhtml_Controller_Action +class Mage_Adminhtml_Report_SalesController extends Mage_Adminhtml_Controller_Report_Abstract { /** - * Admin session model + * Add report/sales breadcrumbs * - * @var null|Mage_Admin_Model_Session + * @return Mage_Adminhtml_Report_SalesController */ - protected $_adminSession = null; - public function _initAction() { - $this->loadLayout() - ->_addBreadcrumb(Mage::helper('reports')->__('Reports'), Mage::helper('reports')->__('Reports')) - ->_addBreadcrumb(Mage::helper('reports')->__('Sales'), Mage::helper('reports')->__('Sales')); - return $this; - } - - public function _initReportAction($blocks) - { - if (!is_array($blocks)) { - $blocks = array($blocks); - } - - $requestData = Mage::helper('adminhtml')->prepareFilterString($this->getRequest()->getParam('filter')); - $requestData = $this->_filterDates($requestData, array('from', 'to')); - $requestData['store_ids'] = $this->getRequest()->getParam('store_ids'); - $params = new Varien_Object(); - - foreach ($requestData as $key => $value) { - if (!empty($value)) { - $params->setData($key, $value); - } - } - - foreach ($blocks as $block) { - if ($block) { - $block->setPeriodType($params->getData('period_type')); - $block->setFilterData($params); - } - } - + parent::_initAction(); + $this->_addBreadcrumb(Mage::helper('reports')->__('Sales'), Mage::helper('reports')->__('Sales')); return $this; } @@ -103,7 +73,7 @@ public function bestsellersAction() $this->_showLastExecutionTime(Mage_Reports_Model_Flag::REPORT_BESTSELLERS_FLAG_CODE, 'bestsellers'); $this->_initAction() - ->_setActiveMenu('report/sales/bestsellers') + ->_setActiveMenu('report/products/bestsellers') ->_addBreadcrumb(Mage::helper('adminhtml')->__('Products Bestsellers Report'), Mage::helper('adminhtml')->__('Products Bestsellers Report')); $gridBlock = $this->getLayout()->getBlock('report_sales_bestsellers.grid'); @@ -150,22 +120,6 @@ protected function _getCollectionNames() return array(); } - protected function _showLastExecutionTime($flagCode, $refreshCode) - { - $flag = Mage::getModel('reports/flag')->setReportFlagCode($flagCode)->loadSelf(); - $updatedAt = ($flag->hasData()) - ? Mage::app()->getLocale()->storeDate( - 0, new Zend_Date($flag->getLastUpdate(), Varien_Date::DATETIME_INTERNAL_FORMAT), true - ) - : 'undefined'; - - $refreshStatsLink = $this->getUrl('*/*/refreshstatistics'); - $directRefreshLink = $this->getUrl('*/*/refreshRecent', array('code' => $refreshCode)); - - Mage::getSingleton('adminhtml/session')->addNotice(Mage::helper('adminhtml')->__('Last updated: %s. To refresh last day\'s statistics, click here.', $updatedAt, $refreshStatsLink, $directRefreshLink)); - return $this; - } - /** * Refresh statistics for last 25 hours * @@ -465,17 +419,4 @@ protected function _isAllowed() break; } } - - /** - * Retrieve admin session model - * - * @return Mage_Admin_Model_Session - */ - protected function _getSession() - { - if (is_null($this->_adminSession)) { - $this->_adminSession = Mage::getSingleton('admin/session'); - } - return $this->_adminSession; - } } diff --git a/app/code/core/Mage/Adminhtml/controllers/Report/StatisticsController.php b/app/code/core/Mage/Adminhtml/controllers/Report/StatisticsController.php index 08764ff459..0ae974edc6 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Report/StatisticsController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Report/StatisticsController.php @@ -100,13 +100,14 @@ protected function _getCollectionNames() } $aliases = array( - 'sales' => 'sales/report_order', - 'tax' => 'tax/report_tax', - 'shipping' => 'sales/report_shipping', - 'invoiced' => 'sales/report_invoiced', - 'refunded' => 'sales/report_refunded', - 'coupons' => 'salesrule/report_rule', + 'sales' => 'sales/report_order', + 'tax' => 'tax/report_tax', + 'shipping' => 'sales/report_shipping', + 'invoiced' => 'sales/report_invoiced', + 'refunded' => 'sales/report_refunded', + 'coupons' => 'salesrule/report_rule', 'bestsellers' => 'sales/report_bestsellers', + 'viewed' => 'reports/report_product_viewed', ); $out = array(); foreach ($codes as $code) { diff --git a/app/code/core/Mage/Adminhtml/controllers/Sales/Order/CreateController.php b/app/code/core/Mage/Adminhtml/controllers/Sales/Order/CreateController.php index 78fe6831fa..5420a578b2 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Sales/Order/CreateController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Sales/Order/CreateController.php @@ -279,7 +279,8 @@ protected function _processActionData($action = null) $data = $this->getRequest()->getPost('order'); if (!empty($data['coupon']['code'])) { if ($this->_getQuote()->getCouponCode() !== $data['coupon']['code']) { - $this->_getSession()->addError($this->__('"%s" coupon code is not valid.', $data['coupon']['code'])); + $this->_getSession()->addError( + $this->__('"%s" coupon code is not valid.', $this->_getHelper()->escapeHtml($data['coupon']['code']))); } else { $this->_getSession()->addSuccess($this->__('The coupon code has been accepted.')); } diff --git a/app/code/core/Mage/Adminhtml/controllers/Sales/Order/ShipmentController.php b/app/code/core/Mage/Adminhtml/controllers/Sales/Order/ShipmentController.php index 77916fa02b..dc9a37f663 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Sales/Order/ShipmentController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Sales/Order/ShipmentController.php @@ -212,18 +212,20 @@ public function saveAction() $shipment->getOrder()->setCustomerNoteNotify(!empty($data['send_email'])); $responseAjax = new Varien_Object(); $isNeedCreateLabel = isset($data['create_shipping_label']) && $data['create_shipping_label']; - if ($isNeedCreateLabel) { - if ($this->_createShippingLabel($shipment)) { - $this->_getSession() - ->addSuccess($this->__('The shipment has been created. The shipping label has been created.')); - $responseAjax->setOk(true); - } - } else { - $this->_getSession() - ->addSuccess($this->__('The shipment has been created.')); + + if ($isNeedCreateLabel && $this->_createShippingLabel($shipment)) { + $responseAjax->setOk(true); } + $this->_saveShipment($shipment); + $shipment->sendEmail(!empty($data['send_email']), $comment); + + $shipmentCreatedMessage = $this->__('The shipment has been created.'); + $labelCreatedMessage = $this->__('The shipping label has been created.'); + + $this->_getSession()->addSuccess($isNeedCreateLabel ? $shipmentCreatedMessage . ' ' . $labelCreatedMessage + : $shipmentCreatedMessage); Mage::getSingleton('adminhtml/session')->getCommentText(true); } catch (Mage_Core_Exception $e) { if ($isNeedCreateLabel) { @@ -237,7 +239,8 @@ public function saveAction() Mage::logException($e); if ($isNeedCreateLabel) { $responseAjax->setError(true); - $responseAjax->setMessage(Mage::helper('sales')->__('An error occurred while creating shipping label.')); + $responseAjax->setMessage( + Mage::helper('sales')->__('An error occurred while creating shipping label.')); } else { $this->_getSession()->addError($this->__('Cannot save shipment.')); $this->_redirect('*/*/new', array('order_id' => $this->getRequest()->getParam('order_id'))); diff --git a/app/code/core/Mage/Adminhtml/controllers/Sales/OrderController.php b/app/code/core/Mage/Adminhtml/controllers/Sales/OrderController.php index e4455ebe9a..e9f934f96d 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Sales/OrderController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Sales/OrderController.php @@ -336,9 +336,13 @@ public function creditmemosAction() public function commentsHistoryAction() { $this->_initOrder(); - $this->getResponse()->setBody( - $this->getLayout()->createBlock('adminhtml/sales_order_view_tab_history')->toHtml() - ); + $html = $this->getLayout()->createBlock('adminhtml/sales_order_view_tab_history')->toHtml(); + /* @var $translate Mage_Core_Model_Translate_Inline */ + $translate = Mage::getModel('core/translate_inline'); + if ($translate->isAllowed()) { + $translate->processResponseBody($html); + } + $this->getResponse()->setBody($html); } /** @@ -718,6 +722,7 @@ public function addressAction() $addressId = $this->getRequest()->getParam('address_id'); $address = Mage::getModel('sales/order_address') ->getCollection() + ->addFilter('entity_id', $addressId) ->getItemById($addressId); if ($address) { Mage::register('order_address', $address); diff --git a/app/code/core/Mage/Adminhtml/controllers/Sales/TransactionsController.php b/app/code/core/Mage/Adminhtml/controllers/Sales/TransactionsController.php index 769c153480..c565cdaa1b 100644 --- a/app/code/core/Mage/Adminhtml/controllers/Sales/TransactionsController.php +++ b/app/code/core/Mage/Adminhtml/controllers/Sales/TransactionsController.php @@ -110,13 +110,15 @@ public function fetchAction() ->setOrder($txn->getOrder()) ->importTransactionInfo($txn); $txn->save(); - Mage::getSingleton('adminhtml/session')->addSuccess(Mage::helper('adminhtml') - ->__('The transaction details have been updated.')); + Mage::getSingleton('adminhtml/session')->addSuccess( + Mage::helper('adminhtml')->__('The transaction details have been updated.') + ); } catch (Mage_Core_Exception $e) { Mage::getSingleton('adminhtml/session')->addError($e->getMessage()); } catch (Exception $e) { - Mage::getSingleton('adminhtml/session')->addError(Mage::helper('adminhtml') - ->__('Unable to update transaction details.')); + Mage::getSingleton('adminhtml/session')->addError( + Mage::helper('adminhtml')->__('Unable to update transaction details.') + ); Mage::logException($e); } $this->_redirect('*/sales_transactions/view', array('_current' => true)); diff --git a/app/code/core/Mage/Adminhtml/controllers/System/BackupController.php b/app/code/core/Mage/Adminhtml/controllers/System/BackupController.php index 77678814c0..d7b46e2d4c 100644 --- a/app/code/core/Mage/Adminhtml/controllers/System/BackupController.php +++ b/app/code/core/Mage/Adminhtml/controllers/System/BackupController.php @@ -66,43 +66,93 @@ public function gridAction() /** * Create backup action + * + * @return Mage_Adminhtml_Controller_Action */ public function createAction() { + if (!$this->getRequest()->isAjax()) { + return $this->getUrl('*/*/index'); + } + + $response = new Varien_Object(); + $helper = Mage::helper('backup'); + try { - $backupDb = Mage::getModel('backup/db'); - $backup = Mage::getModel('backup/backup') + $type = $this->getRequest()->getParam('type'); + + $backupManager = Mage_Backup::getBackupInstance($type) + ->setBackupExtension($helper->getExtensionByType($type)) ->setTime(time()) - ->setType('db') - ->setPath(Mage::getBaseDir("var") . DS . "backups"); + ->setBackupsDir($helper->getBackupsDir()); + + Mage::register('backup_manager', $backupManager); + + if ($this->getRequest()->getParam('maintenance_mode')) { + $turnedOn = $helper->turnOnMaintenanceMode(); - Mage::register('backup_model', $backup); + if (!$turnedOn) { + $response->setError( + Mage::helper('backup')->__("Warning! System couldn't put store on the maintenance mode.") . ' ' + . Mage::helper('backup')->__("Please deselect the sufficient check-box, if you want to continue backup's creation") + ); + $backupManager->setErrorMessage(Mage::helper('backup')->__("System couldn't put store on the maintenance mode")); + return $this->getResponse()->setBody($response->toJson()); + } + } - $backupDb->createBackup($backup); - $this->_getSession()->addSuccess(Mage::helper('adminhtml')->__('The backup has been created.')); + if ($type != Mage_Backup_Helper_Data::TYPE_DB) { + $backupManager->setRootDir(Mage::getBaseDir()) + ->addIgnorePaths($helper->getBackupIgnorePaths()); + } + + $successMessage = $helper->getCreateSuccessMessageByType($type); + + $backupManager->create(); + + $this->_getSession()->addSuccess($successMessage); + + if ($this->getRequest()->getParam('maintenance_mode')) { + $helper->turnOffMaintenanceMode(); + } + + $response->setRedirectUrl($this->getUrl('*/*/index')); + } catch (Mage_Backup_Exception_NotEnoughFreeSpace $e) { + $errorMessage = Mage::helper('backup')->__('Not enough free space to create backup.'); + } catch (Mage_Backup_Exception_NotEnoughPermissions $e) { + Mage::log($e->getMessage()); + $errorMessage = Mage::helper('backup')->__('Not enough permissions to create backup.'); + } catch (Exception $e) { + Mage::log($e->getMessage()); + $errorMessage = Mage::helper('backup')->__('An error occurred while creating the backup.'); } - catch (Exception $e) { - $this->_getSession()->addException($e, Mage::helper('adminhtml')->__('An error occurred while creating the backup.')); + + if (!empty($errorMessage)) { + $response->setError($errorMessage); + $backupManager->setErrorMessage($errorMessage); } - $this->_redirect('*/*'); + + $this->getResponse()->setBody($response->toJson()); } /** * Download backup action + * + * @return Mage_Adminhtml_Controller_Action */ public function downloadAction() { $backup = Mage::getModel('backup/backup') ->setTime((int)$this->getRequest()->getParam('time')) ->setType($this->getRequest()->getParam('type')) - ->setPath(Mage::getBaseDir("var") . DS . "backups"); + ->setPath(Mage::helper('backup')->getBackupsDir()); /* @var $backup Mage_Backup_Model_Backup */ if (!$backup->exists()) { - $this->_redirect('*/*'); + return $this->_redirect('*/*'); } - $fileName = 'backup-' . date('YmdHis', $backup->getTime()) . '.sql.gz'; + $fileName = Mage::helper('backup')->generateBackupDownloadName($backup); $this->_prepareDownloadResponse($fileName, null, 'application/octet-stream', $backup->getSize()); @@ -113,32 +163,178 @@ public function downloadAction() } /** - * Delete backup action + * Rollback Action + * + * @return Mage_Adminhtml_Controller_Action */ - public function deleteAction() + public function rollbackAction() { + if (!Mage::helper('backup')->isRollbackAllowed()){ + return $this->_forward('denied'); + } + + if (!$this->getRequest()->isAjax()) { + return $this->getUrl('*/*/index'); + } + + $helper = Mage::helper('backup'); + $response = new Varien_Object(); + try { - $backup = Mage::getModel('backup/backup') - ->setTime((int)$this->getRequest()->getParam('time')) - ->setType($this->getRequest()->getParam('type')) - ->setPath(Mage::getBaseDir("var") . DS . "backups") - ->deleteFile(); + $type = $this->getRequest()->getParam('type'); + + $backupManager = Mage_Backup::getBackupInstance($type) + ->setBackupExtension($helper->getExtensionByType($type)) + ->setTime($this->getRequest()->getParam('time')) + ->setBackupsDir($helper->getBackupsDir()) + ->setResourceModel(Mage::getResourceModel('backup/db')); + + Mage::register('backup_manager', $backupManager); + + $passwordValid = Mage::getModel('backup/backup')->validateUserPassword( + $this->getRequest()->getParam('password') + ); + + if (!$passwordValid) { + $response->setError(Mage::helper('backup')->__('Invalid Password.')); + $backupManager->setErrorMessage(Mage::helper('backup')->__('Invalid Password.')); + return $this->getResponse()->setBody($response->toJson()); + } + + if ($this->getRequest()->getParam('maintenance_mode')) { + $turnedOn = $helper->turnOnMaintenanceMode(); + + if (!$turnedOn) { + $response->setError( + Mage::helper('backup')->__("Warning! System couldn't put store on the maintenance mode.") . ' ' + . Mage::helper('backup')->__("Please deselect the sufficient check-box, if you want to continue rollback processing") + ); + $backupManager->setErrorMessage(Mage::helper('backup')->__("System couldn't put store on the maintenance mode")); + return $this->getResponse()->setBody($response->toJson()); + } + } + + if ($type != Mage_Backup_Helper_Data::TYPE_DB) { - Mage::register('backup_model', $backup); + $backupManager->setRootDir(Mage::getBaseDir()) + ->addIgnorePaths($helper->getRollbackIgnorePaths()); - $this->_getSession()->addSuccess(Mage::helper('adminhtml')->__('Backup record was deleted.')); + if ($this->getRequest()->getParam('use_ftp', false)) { + $backupManager->setUseFtp( + $this->getRequest()->getParam('ftp_host', ''), + $this->getRequest()->getParam('ftp_user', ''), + $this->getRequest()->getParam('ftp_pass', ''), + $this->getRequest()->getParam('ftp_path', '') + ); + } + } + + $backupManager->rollback(); + + $helper->invalidateCache()->invalidateIndexer(); + + $adminSession = $this->_getSession(); + $adminSession->unsetAll(); + $adminSession->getCookie()->delete($adminSession->getSessionName()); + + if ($this->getRequest()->getParam('maintenance_mode')) { + $helper->turnOffMaintenanceMode(); + } + + $response->setRedirectUrl($this->getUrl('*')); + + } catch (Mage_Backup_Exception_CantLoadSnapshot $e) { + $errorMsg = Mage::helper('backup')->__('Backup file not found'); + } catch (Mage_Backup_Exception_FtpConnectionFailed $e) { + $errorMsg = Mage::helper('backup')->__('Failed to connect to FTP'); + } catch (Mage_Backup_Exception_FtpValidationFailed $e) { + $errorMsg = Mage::helper('backup')->__('Failed to validate FTP'); + } catch (Mage_Backup_Exception_NotEnoughPermissions $e) { + Mage::log($e->getMessage()); + $errorMsg = Mage::helper('backup')->__('Not enough permissions to perform rollback'); + } catch (Exception $e) { + Mage::log($e->getMessage()); + $errorMsg = Mage::helper('backup')->__('Failed to rollback'); + } + + if (!empty($errorMsg)) { + $response->setError($errorMsg); + $backupManager->setErrorMessage($errorMsg); } - catch (Exception $e) { - // Nothing + + $this->getResponse()->setBody($response->toJson()); + } + + /** + * Delete backups mass action + * + * @return Mage_Adminhtml_Controller_Action + */ + public function massDeleteAction() + { + $backupIds = $this->getRequest()->getParam('ids', array()); + + if (!is_array($backupIds) || !count($backupIds)) { + return $this->_redirect('*/*/index'); } - $this->_redirect('*/*/'); + /** @var $backupModel Mage_Backup_Model_Backup */ + $backupModel = Mage::getModel('backup/backup'); + $resultData = new Varien_Object(); + $resultData->setIsSuccess(false); + $resultData->setDeleteResult(array()); + Mage::register('backup_manager', $resultData); + + $deleteFailMessage = Mage::helper('backup')->__('Failed to delete one or several backups.'); + + try { + $allBackupsDeleted = true; + + foreach ($backupIds as $id) { + list($time, $type) = explode('_', $id); + + $backupModel->setTime((int)$time) + ->setType($type) + ->setPath(Mage::helper('backup')->getBackupsDir()) + ->deleteFile(); + if ($backupModel->exists()) { + $allBackupsDeleted = false; + $result = Mage::helper('adminhtml')->__('failed'); + } else { + $result = Mage::helper('adminhtml')->__('successful'); + } + + $resultData->setDeleteResult( + array_merge($resultData->getDeleteResult(), array($backupModel->getFileName() . ' ' . $result)) + ); + } + + $resultData->setIsSuccess(true); + if ($allBackupsDeleted) { + $this->_getSession()->addSuccess( + Mage::helper('backup')->__('The selected backup(s) has been deleted.') + ); + } + else { + throw new Exception($deleteFailMessage); + } + } catch (Exception $e) { + $resultData->setIsSuccess(false); + $this->_getSession()->addError($deleteFailMessage); + } + + return $this->_redirect('*/*/index'); } + /** + * Check Permissions for all actions + * + * @return bool + */ protected function _isAllowed() { - return Mage::getSingleton('admin/session')->isAllowed('system/tools/backup'); + return Mage::getSingleton('admin/session')->isAllowed('system/tools/backup' ); } /** diff --git a/app/code/core/Mage/Adminhtml/controllers/System/VariableController.php b/app/code/core/Mage/Adminhtml/controllers/System/VariableController.php index 4e8d743b4c..85b1bfdf7b 100644 --- a/app/code/core/Mage/Adminhtml/controllers/System/VariableController.php +++ b/app/code/core/Mage/Adminhtml/controllers/System/VariableController.php @@ -56,7 +56,7 @@ protected function _initVariable() $this->_title($this->__('System'))->_title($this->__('Custom Variables')); $variableId = $this->getRequest()->getParam('variable_id', null); - $storeId = $this->getRequest()->getParam('store', 0); + $storeId = (int)$this->getRequest()->getParam('store', 0); /* @var $emailVariable Mage_Core_Model_Variable */ $variable = Mage::getModel('core/variable'); if ($variableId) { @@ -101,7 +101,9 @@ public function editAction() $this->_initLayout() ->_addContent($this->getLayout()->createBlock('adminhtml/system_variable_edit')) - ->_addJs($this->getLayout()->createBlock('core/template', '', array('template' => 'system/variable/js.phtml'))) + ->_addJs($this->getLayout()->createBlock('core/template', '', array( + 'template' => 'system/variable/js.phtml' + ))) ->renderLayout(); } diff --git a/app/code/core/Mage/Adminhtml/controllers/TagController.php b/app/code/core/Mage/Adminhtml/controllers/TagController.php index a6be40dcfb..5fff4c6eed 100644 --- a/app/code/core/Mage/Adminhtml/controllers/TagController.php +++ b/app/code/core/Mage/Adminhtml/controllers/TagController.php @@ -82,7 +82,6 @@ public function indexAction() $this->_initAction() ->_addBreadcrumb(Mage::helper('adminhtml')->__('All Tags'), Mage::helper('adminhtml')->__('All Tags')) ->_setActiveMenu('catalog/tag/all') - ->_addContent($this->getLayout()->createBlock('adminhtml/tag_tag')) ->renderLayout(); } @@ -93,7 +92,7 @@ public function indexAction() public function ajaxGridAction() { $this->loadLayout(); - $this->getResponse()->setBody($this->getLayout()->createBlock('adminhtml/tag_tag_grid')->toHtml()); + $this->renderLayout(); } /** @@ -103,7 +102,7 @@ public function ajaxGridAction() public function ajaxPendingGridAction() { $this->loadLayout(); - $this->getResponse()->setBody($this->getLayout()->createBlock('adminhtml/tag_grid_pending')->toHtml()); + $this->renderLayout(); } /** @@ -170,7 +169,9 @@ public function saveAction() $model->addData($data); if (isset($postData['tag_assigned_products'])) { - $productIds = Mage::helper('adminhtml/js')->decodeGridSerializedInput($postData['tag_assigned_products']); + $productIds = Mage::helper('adminhtml/js')->decodeGridSerializedInput( + $postData['tag_assigned_products'] + ); $tagRelationModel = Mage::getModel('tag/tag_relation'); $tagRelationModel->addRelations($model, $productIds); } @@ -234,7 +235,6 @@ public function pendingAction() $this->_initAction() ->_addBreadcrumb(Mage::helper('adminhtml')->__('Pending Tags'), Mage::helper('adminhtml')->__('Pending Tags')) ->_setActiveMenu('catalog/tag/pending') - ->_addContent($this->getLayout()->createBlock('adminhtml/tag_pending')) ->renderLayout(); } @@ -269,7 +269,8 @@ public function assignedGridOnlyAction() public function productAction() { $this->_initTag(); - $this->getResponse()->setBody($this->getLayout()->createBlock('adminhtml/tag_product_grid')->toHtml()); + $this->loadLayout(); + $this->renderLayout(); } /** @@ -279,7 +280,8 @@ public function productAction() public function customerAction() { $this->_initTag(); - $this->getResponse()->setBody($this->getLayout()->createBlock('adminhtml/tag_customer_grid')->toHtml()); + $this->loadLayout(); + $this->renderLayout(); } /** diff --git a/app/code/core/Mage/Authorizenet/etc/system.xml b/app/code/core/Mage/Authorizenet/etc/system.xml index b160eead43..951b1e4a06 100755 --- a/app/code/core/Mage/Authorizenet/etc/system.xml +++ b/app/code/core/Mage/Authorizenet/etc/system.xml @@ -210,6 +210,7 @@ 1 1 0 + validate-number diff --git a/app/code/core/Mage/Backup/Helper/Data.php b/app/code/core/Mage/Backup/Helper/Data.php index c9540e00e1..c937758309 100644 --- a/app/code/core/Mage/Backup/Helper/Data.php +++ b/app/code/core/Mage/Backup/Helper/Data.php @@ -29,5 +29,245 @@ */ class Mage_Backup_Helper_Data extends Mage_Core_Helper_Abstract { + /** + * Backup type constant for database backup + * + * @const string + */ + const TYPE_DB = 'db'; + /** + * Backup type constant for filesystem backup + * + * @const string + */ + const TYPE_FILESYSTEM = 'filesystem'; + + /** + * Backup type constant for full system backup(database + filesystem) + * + * @const string + */ + const TYPE_SYSTEM_SNAPSHOT = 'snapshot'; + + /** + * Backup type constant for media and database backup + * + * @const string + */ + const TYPE_MEDIA = 'media'; + + /** + * Get all possible backup type values with descriptive title + * + * @return array + */ + public function getBackupTypes() + { + return array( + self::TYPE_DB => self::__('Database'), + self::TYPE_MEDIA => self::__('Database and Media'), + self::TYPE_SYSTEM_SNAPSHOT => self::__('System') + ); + } + + /** + * Get all possible backup type values + * + * @return array + */ + public function getBackupTypesList() + { + return array( + self::TYPE_DB, + self::TYPE_SYSTEM_SNAPSHOT, + self::TYPE_MEDIA + ); + } + + /** + * Get default backup type value + * + * @return string + */ + public function getDefaultBackupType() + { + return self::TYPE_DB; + } + + /** + * Get directory path where backups stored + * + * @return string + */ + public function getBackupsDir() + { + return Mage::getBaseDir('var') . DS . 'backups'; + } + + /** + * Get backup file extension by backup type + * + * @param string $type + * @return string + */ + public function getExtensionByType($type) + { + $extensions = $this->getExtensions(); + return isset($extensions[$type]) ? $extensions[$type] : ''; + } + + /** + * Get all types to extensions map + * + * @return array + */ + public function getExtensions() + { + return array( + self::TYPE_SYSTEM_SNAPSHOT => 'tgz', + self::TYPE_MEDIA => 'tgz', + self::TYPE_DB => 'gz' + ); + } + + /** + * Generate backup download name + * + * @param Mage_Backup_Model_Backup $backup + * @return string + */ + public function generateBackupDownloadName(Mage_Backup_Model_Backup $backup) + { + $additionalExtension = $backup->getType() == self::TYPE_DB ? '.sql' : ''; + return $backup->getType() . '-' . date('YmdHis', $backup->getTime()) . $additionalExtension . '.' + . $this->getExtensionByType($backup->getType()); + } + + /** + * Check Permission for Rollback + * + * @return boolean + */ + public function isRollbackAllowed(){ + return Mage::getSingleton('admin/session')->isAllowed('system/tools/backup/rollback' ); + } + + /** + * Get paths that should be ignored when creating system snapshots + * + * @return array + */ + public function getBackupIgnorePaths() + { + return array( + '.svn', + 'maintenance.flag', + Mage::getBaseDir('var') . DS . 'session', + Mage::getBaseDir('var') . DS . 'cache', + Mage::getBaseDir('var') . DS . 'full_page_cache', + Mage::getBaseDir('var') . DS . 'locks', + Mage::getBaseDir('var') . DS . 'log', + Mage::getBaseDir('var') . DS . 'report' + ); + } + + /** + * Get paths that should be ignored when rolling back system snapshots + * + * @return array + */ + public function getRollbackIgnorePaths() + { + return array( + '.svn', + 'maintenance.flag', + Mage::getBaseDir('var') . DS . 'session', + Mage::getBaseDir('var') . DS . 'locks', + Mage::getBaseDir('var') . DS . 'log', + Mage::getBaseDir('var') . DS . 'report', + Mage::getBaseDir('app') . DS . 'Mage.php', + Mage::getBaseDir() . DS . 'errors', + Mage::getBaseDir() . DS . 'index.php' + ); + } + + /** + * Put store into maintenance mode + * + * @return bool + */ + public function turnOnMaintenanceMode() + { + $maintenanceFlagFile = $this->getMaintenanceFlagFilePath(); + $result = file_put_contents($maintenanceFlagFile, 'maintenance'); + + return $result !== false; + } + + /** + * Turn off store maintenance mode + */ + public function turnOffMaintenanceMode() + { + $maintenanceFlagFile = $this->getMaintenanceFlagFilePath(); + @unlink($maintenanceFlagFile); + } + + /** + * Get backup create success message by backup type + * + * @param string $type + * @return string + */ + public function getCreateSuccessMessageByType($type) + { + $messagesMap = array( + self::TYPE_SYSTEM_SNAPSHOT => $this->__('The system backup has been created.'), + self::TYPE_MEDIA => $this->__('The database and media backup has been created.'), + self::TYPE_DB => $this->__('The database backup has been created.') + ); + + if (!isset($messagesMap[$type])) { + return; + } + + return $messagesMap[$type]; + } + + /** + * Get path to maintenance flag file + * + * @return string + */ + protected function getMaintenanceFlagFilePath() + { + return Mage::getBaseDir() . DS . 'maintenance.flag'; + } + + /** + * Invalidate Cache + * @return Mage_Backup_Helper_Data + */ + public function invalidateCache() + { + if ($cacheTypesNode = Mage::getConfig()->getNode(Mage_Core_Model_Cache::XML_PATH_TYPES)) { + $cacheTypesList = array_keys($cacheTypesNode->asArray()); + Mage::app()->getCacheInstance()->invalidateType($cacheTypesList); + } + return $this; + } + + /** + * Invalidate Indexer + * + * @return Mage_Backup_Helper_Data + */ + public function invalidateIndexer() + { + foreach (Mage::getResourceModel('index/process_collection') as $process){ + $process->changeStatus(Mage_Index_Model_Process::STATUS_REQUIRE_REINDEX); + } + return $this; + } } diff --git a/app/code/core/Mage/Backup/Model/Backup.php b/app/code/core/Mage/Backup/Model/Backup.php index a2c6664ab0..84cf07818c 100644 --- a/app/code/core/Mage/Backup/Model/Backup.php +++ b/app/code/core/Mage/Backup/Model/Backup.php @@ -29,23 +29,17 @@ * * @category Mage * @package Mage_Backup - * @author Magento Core Team + * @author Magento Core Team */ class Mage_Backup_Model_Backup extends Varien_Object { - /* backup types */ - const BACKUP_DB = 'db'; - const BACKUP_VIEW = 'view'; - const BACKUP_MEDIA = 'media'; - /* internal constants */ - const BACKUP_EXTENSION = 'gz'; const COMPRESS_RATE = 9; /** * Type of backup file * - * @var string db|media|view + * @var string */ private $_type = 'db'; @@ -65,12 +59,26 @@ class Mage_Backup_Model_Backup extends Varien_Object */ public function load($fileName, $filePath) { - list ($time, $type) = explode("_", substr($fileName, 0, strrpos($fileName, "."))); + $extensions = Mage::helper('backup')->getExtensions(); + + $fileNameWithoutExtension = $fileName; + + foreach ($extensions as $extension) { + $fileNameWithoutExtension = preg_replace('/' . preg_quote($extension, '/') . '$/', '', + $fileNameWithoutExtension + ); + } + + list ($time, $type) = explode("_", substr($fileNameWithoutExtension, 0, + strrpos($fileNameWithoutExtension, ".") + )); + $this->addData(array( 'id' => $filePath . DS . $fileName, 'time' => (int)$time, 'path' => $filePath, - 'date_object' => new Zend_Date((int)$time) + 'extension' => Mage::helper('backup')->getExtensionByType($type), + 'date_object' => new Zend_Date((int)$time, Mage::app()->getLocale()->getLocaleCode()) )); $this->setType($type); return $this; @@ -94,18 +102,19 @@ public function exists() public function getFileName() { return $this->getTime() . "_" . $this->getType() - . "." . self::BACKUP_EXTENSION; + . "." . Mage::helper('backup')->getExtensionByType($this->getType()); } /** * Sets type of file * - * @param string $value db|media|view + * @param string $value */ public function setType($value='db') { - if(!in_array($value, array('db','media','view'))) { - $value = 'db'; + $possibleTypes = Mage::helper('backup')->getBackupTypesList(); + if(!in_array($value, $possibleTypes)) { + $value = Mage::helper('backup')->getDefaultBackupType(); } $this->_type = $value; @@ -117,7 +126,7 @@ public function setType($value='db') /** * Returns type of backup file * - * @return string db|media|view + * @return string */ public function getType() { @@ -249,11 +258,12 @@ public function open($write = false) $mode = $write ? 'wb' . self::COMPRESS_RATE : 'rb'; - try { - $this->_handler = gzopen($filePath, $mode); - } - catch (Exception $e) { - Mage::exception('Mage_Backup', Mage::helper('backup')->__('Backup file "%s" cannot be read from or written to.', $this->getFileName())); + $this->_handler = @gzopen($filePath, $mode); + + if (!$this->_handler) { + throw new Mage_Backup_Exception_NotEnoughPermissions( + Mage::helper('backup')->__('Backup file "%s" cannot be read from or written to.', $this->getFileName()) + ); } return $this; @@ -351,4 +361,16 @@ public function getSize() return 0; } + + /** + * Validate user password + * + * @param string $password + * @return bool + */ + public function validateUserPassword($password) + { + $userPasswordHash = Mage::getModel('admin/session')->getUser()->getPassword(); + return Mage::helper('core')->validateHash($password, $userPasswordHash); + } } diff --git a/app/code/core/Mage/Backup/Model/Config/Backend/Cron.php b/app/code/core/Mage/Backup/Model/Config/Backend/Cron.php new file mode 100644 index 0000000000..34dbf9d066 --- /dev/null +++ b/app/code/core/Mage/Backup/Model/Config/Backend/Cron.php @@ -0,0 +1,89 @@ + + */ +class Mage_Backup_Model_Config_Backend_Cron extends Mage_Core_Model_Config_Data +{ + const CRON_STRING_PATH = 'crontab/jobs/system_backup/schedule/cron_expr'; + const CRON_MODEL_PATH = 'crontab/jobs/system_backup/run/model'; + + const XML_PATH_BACKUP_ENABLED = 'groups/backup/fields/enabled/value'; + const XML_PATH_BACKUP_TIME = 'groups/backup/fields/time/value'; + const XML_PATH_BACKUP_FREQUENCY = 'groups/backup/fields/frequency/value'; + + /** + * Cron settings after save + * + * @return Mage_Adminhtml_Model_System_Config_Backend_Log_Cron + */ + protected function _afterSave() + { + $enabled = $this->getData(self::XML_PATH_BACKUP_ENABLED); + $time = $this->getData(self::XML_PATH_BACKUP_TIME); + $frequency = $this->getData(self::XML_PATH_BACKUP_FREQUENCY); + + $frequencyWeekly = Mage_Adminhtml_Model_System_Config_Source_Cron_Frequency::CRON_WEEKLY; + $frequencyMonthly = Mage_Adminhtml_Model_System_Config_Source_Cron_Frequency::CRON_MONTHLY; + + if ($enabled) { + $cronExprArray = array( + intval($time[1]), # Minute + intval($time[0]), # Hour + ($frequency == $frequencyMonthly) ? '1' : '*', # Day of the Month + '*', # Month of the Year + ($frequency == $frequencyWeekly) ? '1' : '*', # Day of the Week + ); + $cronExprString = join(' ', $cronExprArray); + } + else { + $cronExprString = ''; + } + + try { + Mage::getModel('core/config_data') + ->load(self::CRON_STRING_PATH, 'path') + ->setValue($cronExprString) + ->setPath(self::CRON_STRING_PATH) + ->save(); + + Mage::getModel('core/config_data') + ->load(self::CRON_MODEL_PATH, 'path') + ->setValue((string) Mage::getConfig()->getNode(self::CRON_MODEL_PATH)) + ->setPath(self::CRON_MODEL_PATH) + ->save(); + } + catch (Exception $e) { + Mage::throwException(Mage::helper('backup')->__('Unable to save the cron expression.')); + } + } +} diff --git a/app/code/core/Mage/Backup/Model/Config/Source/Type.php b/app/code/core/Mage/Backup/Model/Config/Source/Type.php new file mode 100644 index 0000000000..75234eeb14 --- /dev/null +++ b/app/code/core/Mage/Backup/Model/Config/Source/Type.php @@ -0,0 +1,52 @@ + + */ +class Mage_Backup_Model_Config_Source_Type +{ + /** + * return possible options + * + * @return array + */ + public function toOptionArray() + { + $backupTypes = array(); + foreach(Mage::helper('backup')->getBackupTypes() as $type => $label) { + $backupTypes[] = array( + 'label' => $label, + 'value' => $type, + ); + } + return $backupTypes; + } +} diff --git a/app/code/core/Mage/Backup/Model/Fs/Collection.php b/app/code/core/Mage/Backup/Model/Fs/Collection.php index 897a0ade97..c95398fbde 100644 --- a/app/code/core/Mage/Backup/Model/Fs/Collection.php +++ b/app/code/core/Mage/Backup/Model/Fs/Collection.php @@ -58,10 +58,17 @@ public function __construct() } // set collection specific params + $extensions = Mage::helper('backup')->getExtensions(); + + foreach ($extensions as $key => $value) { + $extensions[] = '(' . preg_quote($value, '/') . ')'; + } + $extensions = implode('|', $extensions); + $this ->setOrder('time', self::SORT_ORDER_DESC) ->addTargetDir($this->_baseDir) - ->setFilesFilter('/^[a-z0-9\-\_]+\.' . preg_quote(Mage_Backup_Model_Backup::BACKUP_EXTENSION, '/') . '$/') + ->setFilesFilter('/^[a-z0-9\-\_]+\.' . $extensions . '$/') ->setCollectRecursively(false) ; } @@ -80,6 +87,7 @@ protected function _generateRow($filename) $row[$key] = $value; } $row['size'] = filesize($filename); + $row['id'] = $row['time'] . '_' . $row['type']; return $row; } } diff --git a/app/code/core/Mage/Backup/Model/Observer.php b/app/code/core/Mage/Backup/Model/Observer.php new file mode 100644 index 0000000000..ff773d637e --- /dev/null +++ b/app/code/core/Mage/Backup/Model/Observer.php @@ -0,0 +1,95 @@ + + */ +class Mage_Backup_Model_Observer +{ + const XML_PATH_BACKUP_ENABLED = 'system/backup/enabled'; + const XML_PATH_BACKUP_TYPE = 'system/backup/type'; + const XML_PATH_BACKUP_MAINTENANCE_MODE = 'system/backup/maintenance'; + + /** + * Error messages + * + * @var array + */ + protected $_errors = array(); + + /** + * Create Backup + * + * @return Mage_Log_Model_Cron + */ + public function scheduledBackup() + { + if (!Mage::getStoreConfigFlag(self::XML_PATH_BACKUP_ENABLED)) { + return $this; + } + + if (Mage::getStoreConfigFlag(self::XML_PATH_BACKUP_MAINTENANCE_MODE)) { + Mage::helper('backup')->turnOnMaintenanceMode(); + } + + $type = Mage::getStoreConfig(self::XML_PATH_BACKUP_TYPE); + + $this->_errors = array(); + try { + $backupManager = Mage_Backup::getBackupInstance($type) + ->setBackupExtension(Mage::helper('backup')->getExtensionByType($type)) + ->setTime(time()) + ->setBackupsDir(Mage::helper('backup')->getBackupsDir()); + + Mage::register('backup_manager', $backupManager); + + if ($type != Mage_Backup_Helper_Data::TYPE_DB) { + $backupManager->setRootDir(Mage::getBaseDir()) + ->addIgnorePaths(Mage::helper('backup')->getBackupIgnorePaths()); + } + + $backupManager->create(); + Mage::log(Mage::helper('backup')->getCreateSuccessMessageByType($type)); + } + catch (Exception $e) { + $this->_errors[] = $e->getMessage(); + $this->_errors[] = $e->getTrace(); + Mage::log($e->getMessage(), Zend_Log::ERR); + Mage::logException($e); + } + + if (Mage::getStoreConfigFlag(self::XML_PATH_BACKUP_MAINTENANCE_MODE)) { + Mage::helper('backup')->turnOffMaintenanceMode(); + } + + return $this; + } +} diff --git a/app/code/core/Mage/Backup/Model/Resource/Db.php b/app/code/core/Mage/Backup/Model/Resource/Db.php index 885107c7e0..bf2a9ce325 100755 --- a/app/code/core/Mage/Backup/Model/Resource/Db.php +++ b/app/code/core/Mage/Backup/Model/Resource/Db.php @@ -35,11 +35,11 @@ class Mage_Backup_Model_Resource_Db { /** - * Read connection + * Database connection adapter * * @var Varien_Db_Adapter_Pdo_Mysql */ - protected $_read; + protected $_write; /** * tables Foreign key data array @@ -55,7 +55,7 @@ class Mage_Backup_Model_Resource_Db */ public function __construct() { - $this->_read = Mage::getSingleton('core/resource')->getConnection('backup_read'); + $this->_write = Mage::getSingleton('core/resource')->getConnection('backup_write'); } /** @@ -85,7 +85,7 @@ public function clear() */ public function getTables() { - return $this->_read->listTables(); + return $this->_write->listTables(); } /** @@ -123,7 +123,10 @@ public function getTableForeignKeysSql($tableName = null) if (!$tableName) { $tables = $this->getTables(); foreach($tables as $table) { - $fkScript = $fkScript . Mage::getResourceHelper('backup')->getTableForeignKeysSql($table); + $tableFkScript = Mage::getResourceHelper('backup')->getTableForeignKeysSql($table); + if (!empty($tableFkScript)) { + $fkScript .= "\n" . $tableFkScript; + } } } else { $fkScript = $this->getTableForeignKeysSql($tableName); @@ -139,7 +142,7 @@ public function getTableForeignKeysSql($tableName = null) */ public function getTableStatus($tableName) { - $row = $this->_read->showTableStatus($tableName); + $row = $this->_write->showTableStatus($tableName); if ($row) { $statusObject = new Varien_Object(); @@ -148,8 +151,8 @@ public function getTableStatus($tableName) $statusObject->setData(strtolower($field), $value); } - $cntRow = $this->_read->fetchRow( - $this->_read->select()->from($tableName, 'COUNT(1) as rows')); + $cntRow = $this->_write->fetchRow( + $this->_write->select()->from($tableName, 'COUNT(1) as rows')); $statusObject->setRows($cntRow['rows']); return $statusObject; @@ -161,7 +164,7 @@ public function getTableStatus($tableName) /** * Quote Table Row * - * @deprecated + * @deprecated * * @param string $tableName * @param array $row @@ -169,7 +172,7 @@ public function getTableStatus($tableName) */ protected function _quoteRow($tableName, array $row) { - return $row; + return $row; } /** @@ -182,8 +185,7 @@ protected function _quoteRow($tableName, array $row) */ public function getTableDataSql($tableName, $count = null, $offset = null) { - return Mage::getResourceHelper('backup')->getInsertSql($tableName); - + return Mage::getResourceHelper('backup')->getPartInsertSql($tableName, $count, $offset); } /** @@ -206,7 +208,7 @@ public function getTableCreateScript($tableName, $addDropIfExists = false) */ public function getTableHeader($tableName) { - $quotedTableName = $this->_read->quoteIdentifier($tableName); + $quotedTableName = $this->_write->quoteIdentifier($tableName); return "\n--\n" . "-- Table structure for table {$quotedTableName}\n" . "--\n\n"; @@ -241,7 +243,7 @@ public function getHeader() */ public function getFooter() { - return Mage::getResourceHelper('backup')->getFooter(); + return Mage::getResourceHelper('backup')->getFooter(); } /** @@ -274,7 +276,7 @@ public function getTableDataAfterSql($tableName) public function beginTransaction() { Mage::getResourceHelper('backup')->turnOnSerializableMode(); - $this->_read->beginTransaction(); + $this->_write->beginTransaction(); return $this; } @@ -285,7 +287,7 @@ public function beginTransaction() */ public function commitTransaction() { - $this->_read->commit(); + $this->_write->commit(); Mage::getResourceHelper('backup')->turnOnReadCommittedMode(); return $this; } @@ -297,7 +299,18 @@ public function commitTransaction() */ public function rollBackTransaction() { - $this->_read->rollBack(); + $this->_write->rollBack(); + return $this; + } + + /** + * Run sql code + * + * @param $command + * @return Mage_Backup_Model_Resource_Db + */ + public function runCommand($command){ + $this->_write->query($command); return $this; } } diff --git a/app/code/core/Mage/Backup/Model/Resource/Helper/Mysql4.php b/app/code/core/Mage/Backup/Model/Resource/Helper/Mysql4.php index bcb54f9e87..1693b188b3 100644 --- a/app/code/core/Mage/Backup/Model/Resource/Helper/Mysql4.php +++ b/app/code/core/Mage/Backup/Model/Resource/Helper/Mysql4.php @@ -54,19 +54,40 @@ public function getTableDropSql($tableName) */ public function getTableForeignKeysSql($tableName = null) { + $sql = false; + if ($tableName === null) { $sql = ''; foreach ($this->_foreignKeys as $table => $foreignKeys) { - $sql .= sprintf("ALTER TABLE %s\n %s;\n", - $this->_getReadAdapter()->quoteIdentifier($table), - join(",\n ", $foreignKeys) - ); + $sql .= $this->_buildForeignKeysAlterTableSql($table, $foreignKeys); } - return $sql; + } else if (isset($this->_foreignKeys[$tableName])) { + $foreignKeys = $this->_foreignKeys[$tableName]; + $sql = $this->_buildForeignKeysAlterTableSql($tableName, $foreignKeys); + } + + return $sql; + } + + /** + * Build sql that will add foreign keys to it + * + * @param string $tableName + * @param array $foreignKeys + * @return string + */ + protected function _buildForeignKeysAlterTableSql($tableName, $foreignKeys) + { + if (!is_array($foreignKeys) || empty($foreignKeys)) { + return ''; } - return false; + return sprintf("ALTER TABLE %s\n %s;\n", + $this->_getReadAdapter()->quoteIdentifier($tableName), + join(",\n ", $foreignKeys) + ); } + /** * Get create script for table * @@ -217,14 +238,17 @@ public function getTableDataAfterSql($tableName) * Return table part data SQL insert * * @param string $tableName + * @param int $count + * @param int $offset * @return string */ - public function getInsertSql($tableName) + public function getPartInsertSql($tableName, $count = null, $offset = null) { $sql = null; $adapter = $this->_getWriteAdapter(); $select = $adapter->select() - ->from($tableName); + ->from($tableName) + ->limit($count, $offset); $query = $adapter->query($select); while ($row = $query->fetch()) { @@ -243,7 +267,16 @@ public function getInsertSql($tableName) return $sql; } - + /** + * Return table data SQL insert + * + * @param string $tableName + * @return string + */ + public function getInsertSql($tableName) + { + return $this->getPartInsertSql($tableName); + } /** * Quote Table Row * diff --git a/app/code/core/Mage/Backup/etc/adminhtml.xml b/app/code/core/Mage/Backup/etc/adminhtml.xml index ac212733c4..51481326a1 100644 --- a/app/code/core/Mage/Backup/etc/adminhtml.xml +++ b/app/code/core/Mage/Backup/etc/adminhtml.xml @@ -50,6 +50,11 @@ Backups + + + Rollback + + diff --git a/app/code/core/Mage/Backup/etc/config.xml b/app/code/core/Mage/Backup/etc/config.xml index 1fe7c44131..295c8a35cd 100644 --- a/app/code/core/Mage/Backup/etc/config.xml +++ b/app/code/core/Mage/Backup/etc/config.xml @@ -61,4 +61,13 @@ + + + + + backup/observer::scheduledBackup + + + + diff --git a/app/code/core/Mage/Backup/etc/system.xml b/app/code/core/Mage/Backup/etc/system.xml new file mode 100644 index 0000000000..29cf2beedf --- /dev/null +++ b/app/code/core/Mage/Backup/etc/system.xml @@ -0,0 +1,95 @@ + + + + + + + + + text + 500 + 1 + 0 + 0 + + + + select + adminhtml/system_config_source_yesno + 10 + 1 + 0 + 0 + + + + select + 1 + backup/config_source_type + 20 + 1 + 0 + 0 + + + + + select + 1 + adminhtml/system_config_source_cron_frequency + backup/config_backend_cron + 40 + 1 + 0 + 0 + + + + Put store on the maintenance mode while backup's creation + select + 1 + adminhtml/system_config_source_yesno + 50 + 1 + 0 + 0 + + + + + + + diff --git a/app/code/core/Mage/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes.php b/app/code/core/Mage/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes.php index 710a939bc7..a01f8992bd 100644 --- a/app/code/core/Mage/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes.php +++ b/app/code/core/Mage/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes.php @@ -72,6 +72,7 @@ protected function _prepareForm() $tax->setAfterElementHtml( '' ); @@ -112,6 +116,15 @@ function changeTaxClassId() { ); } + $groupPrice = $this->getForm()->getElement('group_price'); + if ($groupPrice) { + $groupPrice->setRenderer( + $this->getLayout()->createBlock('adminhtml/catalog_product_edit_tab_price_group') + ->setPriceColumnHeader(Mage::helper('bundle')->__('Percent Discount')) + ->setPriceValidation('validate-greater-than-zero validate-percents') + ); + } + $mapEnabled = $this->getForm()->getElement('msrp_enabled'); if ($mapEnabled && $this->getCanEditPrice() !== false) { $mapEnabled->setAfterElementHtml( diff --git a/app/code/core/Mage/Bundle/Block/Catalog/Product/View/Type/Bundle.php b/app/code/core/Mage/Bundle/Block/Catalog/Product/View/Type/Bundle.php index d8e5daef87..0043282b9c 100644 --- a/app/code/core/Mage/Bundle/Block/Catalog/Product/View/Type/Bundle.php +++ b/app/code/core/Mage/Bundle/Block/Catalog/Product/View/Type/Bundle.php @@ -73,6 +73,11 @@ public function hasOptions() return true; } + /** + * Returns JSON encoded config to be used in JS scripts + * + * @return string + */ public function getJsonConfig() { Mage::app()->getLocale()->getJsPriceFormat(); @@ -80,15 +85,18 @@ public function getJsonConfig() $options = array(); $selected = array(); $currentProduct = $this->getProduct(); + /* @var $coreHelper Mage_Core_Helper_Data */ $coreHelper = Mage::helper('core'); + /* @var $bundlePriceModel Mage_Bundle_Model_Product_Price */ $bundlePriceModel = Mage::getModel('bundle/product_price'); - if ($preconfiguredFlag = $currentProduct->hasPreconfiguredValues()) { - $preconfiguredValues = $currentProduct->getPreconfiguredValues(); + if ($preConfiguredFlag = $currentProduct->hasPreconfiguredValues()) { + $preConfiguredValues = $currentProduct->getPreconfiguredValues(); $defaultValues = array(); } foreach ($optionsArray as $_option) { + /* @var $_option Mage_Bundle_Model_Option */ if (!$_option->getSelections()) { continue; } @@ -103,8 +111,9 @@ public function getJsonConfig() $selectionCount = count($_option->getSelections()); foreach ($_option->getSelections() as $_selection) { + /* @var $_selection Mage_Catalog_Model_Product */ $selectionId = $_selection->getSelectionId(); - $_qty = !($_selection->getSelectionQty()*1) ? '1' : $_selection->getSelectionQty()*1; + $_qty = !($_selection->getSelectionQty() * 1) ? '1' : $_selection->getSelectionQty() * 1; // recalculate currency $tierPrices = $_selection->getTierPrice(); foreach ($tierPrices as &$tierPriceInfo) { @@ -112,17 +121,8 @@ public function getJsonConfig() } unset($tierPriceInfo); // break the reference with the last element - $taxPercent = 0; - $taxClassId = $_selection->getTaxClassId(); - if ($taxClassId) { - $request = Mage::getSingleton('tax/calculation')->getRateRequest(); - $taxPercent = Mage::getSingleton('tax/calculation')->getRate( - $request->setProductClassId($taxClassId) - ); - } - $itemPrice = $bundlePriceModel->getSelectionFinalTotalPrice($currentProduct, $_selection, - $currentProduct->getQty(), $_selection->getQty()); + $currentProduct->getQty(), $_selection->getQty(), false); $canApplyMAP = false; @@ -138,16 +138,16 @@ public function getJsonConfig() } $selection = array ( - 'qty' => $_qty, - 'customQty' => $_selection->getSelectionCanChangeQty(), - 'price' => $coreHelper->currency($_selection->getFinalPrice(), false, false), - 'priceInclTax' => $coreHelper->currency($_priceInclTax, false, false), - 'priceExclTax' => $coreHelper->currency($_priceExclTax, false, false), - 'priceValue' => $coreHelper->currency($_selection->getSelectionPriceValue(), false, false), - 'priceType' => $_selection->getSelectionPriceType(), - 'tierPrice' => $tierPrices, - 'name' => $_selection->getName(), - 'plusDisposition' => 0, + 'qty' => $_qty, + 'customQty' => $_selection->getSelectionCanChangeQty(), + 'price' => $coreHelper->currency($_selection->getFinalPrice(), false, false), + 'priceInclTax' => $coreHelper->currency($_priceInclTax, false, false), + 'priceExclTax' => $coreHelper->currency($_priceExclTax, false, false), + 'priceValue' => $coreHelper->currency($_selection->getSelectionPriceValue(), false, false), + 'priceType' => $_selection->getSelectionPriceType(), + 'tierPrice' => $tierPrices, + 'name' => $_selection->getName(), + 'plusDisposition' => 0, 'minusDisposition' => 0, 'canApplyMAP' => $canApplyMAP ); @@ -156,7 +156,7 @@ public function getJsonConfig() $args = array('response_object' => $responseObject, 'selection' => $_selection); Mage::dispatchEvent('bundle_product_view_config', $args); if (is_array($responseObject->getAdditionalOptions())) { - foreach ($responseObject->getAdditionalOptions() as $o=>$v) { + foreach ($responseObject->getAdditionalOptions() as $o => $v) { $selection[$o] = $v; } } @@ -171,8 +171,8 @@ public function getJsonConfig() $options[$optionId] = $option; // Add attribute default value (if set) - if ($preconfiguredFlag) { - $configValue = $preconfiguredValues->getData('bundle_option/' . $optionId); + if ($preConfiguredFlag) { + $configValue = $preConfiguredValues->getData('bundle_option/' . $optionId); if ($configValue) { $defaultValues[$optionId] = $configValue; } @@ -192,7 +192,7 @@ public function getJsonConfig() 'isMAPAppliedDirectly' => Mage::helper('catalog')->canApplyMsrp($this->getProduct(), null, false) ); - if ($preconfiguredFlag && !empty($defaultValues)) { + if ($preConfiguredFlag && !empty($defaultValues)) { $config['defaultValues'] = $defaultValues; } diff --git a/app/code/core/Mage/Bundle/Model/Product/Price.php b/app/code/core/Mage/Bundle/Model/Product/Price.php index 748e771322..f296ce8437 100644 --- a/app/code/core/Mage/Bundle/Model/Product/Price.php +++ b/app/code/core/Mage/Bundle/Model/Product/Price.php @@ -27,9 +27,9 @@ /** * Bundle Price Model * - * @category Mage - * @package Mage_Bundle - * @author Magento Core Team + * @category Mage + * @package Mage_Bundle + * @author Magento Core Team */ class Mage_Bundle_Model_Product_Price extends Mage_Catalog_Model_Product_Type_Price { @@ -37,7 +37,7 @@ class Mage_Bundle_Model_Product_Price extends Mage_Catalog_Model_Product_Type_Pr const PRICE_TYPE_DYNAMIC = 0; /** - * Flag wich indicates - is min/max prices have been calculated by index + * Flag which indicates - is min/max prices have been calculated by index * * @var bool */ @@ -56,6 +56,7 @@ public function getIsPricesCalculatedByIndex() /** * Return product base price * + * @param Mage_Catalog_Model_Product $product * @return string */ public function getPrice($product) @@ -70,11 +71,11 @@ public function getPrice($product) /** * Get product final price * - * @param double $qty + * @param double $qty * @param Mage_Catalog_Model_Product $product * @return double */ - public function getFinalPrice($qty=null, $product) + public function getFinalPrice($qty = null, $product) { if (is_null($qty) && !is_null($product->getCalculatedFinalPrice())) { return $product->getCalculatedFinalPrice(); @@ -86,12 +87,13 @@ public function getFinalPrice($qty=null, $product) * Just product with fixed price calculation has price */ if ($finalPrice) { - $tierPrice = $this->_applyTierPrice($product, $qty, $finalPrice); - $specialPrice = $this->_applySpecialPrice($product, $finalPrice); - $finalPrice = min(array($tierPrice, $specialPrice)); + $groupPrice = $this->_applyGroupPrice($product, $finalPrice); + $tierPrice = $this->_applyTierPrice($product, $qty, $finalPrice); + $specialPrice = $this->_applySpecialPrice($product, $finalPrice); + $finalPrice = min(array($groupPrice, $tierPrice, $specialPrice)); $product->setFinalPrice($finalPrice); - Mage::dispatchEvent('catalog_product_get_final_price', array('product'=>$product)); + Mage::dispatchEvent('catalog_product_get_final_price', array('product' => $product)); $finalPrice = $product->getData('final_price'); } $basePrice = $finalPrice; @@ -122,6 +124,15 @@ public function getFinalPrice($qty=null, $product) return max(0, $product->getData('final_price')); } + /** + * Returns final price of a child product + * + * @param Mage_Catalog_Model_Product $product + * @param float $productQty + * @param Mage_Catalog_Model_Product $childProduct + * @param float $childProductQty + * @return decimal + */ public function getChildFinalPrice($product, $productQty, $childProduct, $childProductQty) { return $this->getSelectionFinalPrice($product, $childProduct, $productQty, $childProductQty, false); @@ -134,7 +145,7 @@ public function getChildFinalPrice($product, $productQty, $childProduct, $childP * @see Mage_Bundle_Model_Product_Price::getTotalPrices() * * @param Mage_Catalog_Model_Product $product - * @param string $which + * @param string $which * @return decimal|array */ public function getPrices($product, $which = null) @@ -149,8 +160,8 @@ public function getPrices($product, $which = null) * @see Mage_Bundle_Model_Product_Price::getTotalPrices() * * @param Mage_Catalog_Model_Product $product - * @param string $which - * @param bool|null $includeTax + * @param string $which + * @param bool|null $includeTax * @return decimal|array */ public function getPricesDependingOnTax($product, $which = null, $includeTax = null) @@ -159,21 +170,23 @@ public function getPricesDependingOnTax($product, $which = null, $includeTax = n } /** - * Retrieve Price with take into account tier price + * Retrieve Price considering tier price * * @param Mage_Catalog_Model_Product $product - * @param string|null $which - * @param bool|null $includeTax - * @param bool $takeTierPrice + * @param string|null $which + * @param bool|null $includeTax + * @param bool $takeTierPrice * @return decimal|array */ public function getTotalPrices($product, $which = null, $includeTax = null, $takeTierPrice = true) { + // check if required price is stored in product data + $forceRecalculation = $includeTax xor Mage::helper('tax')->priceIncludesTax(Mage::app()->getStore()); // check calculated price index - if ($product->getData('min_price') && $product->getData('max_price')) { - $minimalPrice = Mage::helper('tax')->getPrice($product, $product->getData('min_price'), $includeTax); - $maximalPrice = Mage::helper('tax')->getPrice($product, $product->getData('max_price'), $includeTax); - $this->_isPricesCalculatedByIndex = true; + if ($product->getData('min_price') && $product->getData('max_price') && !$forceRecalculation) { + $minimalPrice = Mage::helper('tax')->getPrice($product, $product->getData('min_price'), $includeTax); + $maximalPrice = Mage::helper('tax')->getPrice($product, $product->getData('max_price'), $includeTax); + $this->_isPricesCalculatedByIndex = true; } else { /** * Check if product price is fixed @@ -206,9 +219,6 @@ public function getTotalPrices($product, $which = null, $includeTax = null, $tak } $qty = $selection->getSelectionQty(); - if ($selection->getSelectionCanChangeQty() && !$option->isMultiSelection()) { - $qty = min(1, $qty); - } $item = $product->getPriceType() == self::PRICE_TYPE_FIXED ? $product : $selection; @@ -296,7 +306,7 @@ public function getTotalPrices($product, $which = null, $includeTax = null, $tak if ($which == 'max') { return $maximalPrice; - } else if ($which == 'min') { + } elseif ($which == 'min') { return $minimalPrice; } @@ -353,8 +363,9 @@ public function getOptions($product) * * @param Mage_Catalog_Model_Product $bundleProduct * @param Mage_Catalog_Model_Product $selectionProduct - * @param decimal $selectionQty - * @return decimal + * @param float|null $selectionQty + * @param null|bool $multiplyQty Whether to multiply selection's price by its quantity + * @return float */ public function getSelectionPrice($bundleProduct, $selectionProduct, $selectionQty = null, $multiplyQty = true) { @@ -370,10 +381,14 @@ public function getSelectionPrice($bundleProduct, $selectionProduct, $selectionQ } } else { if ($selectionProduct->getSelectionPriceType()) { // percent - return $bundleProduct->getPrice() * ($selectionProduct->getSelectionPriceValue() / 100) * $selectionQty; + $price = $bundleProduct->getPrice() * ($selectionProduct->getSelectionPriceValue() / 100); } else { // fixed - return $selectionProduct->getSelectionPriceValue() * $selectionQty; + $price = $selectionProduct->getSelectionPriceValue(); + } + if ($multiplyQty) { + $price *= $selectionQty; } + return $price; } } @@ -382,15 +397,15 @@ public function getSelectionPrice($bundleProduct, $selectionProduct, $selectionQ * * @param Mage_Catalog_Model_Product $bundleProduct * @param Mage_Catalog_Model_Product $selectionProduct - * @param decimal + * @param decimal $qty * @return decimal */ public function getSelectionPreFinalPrice($bundleProduct, $selectionProduct, $qty = null) { - return $this->_applySpecialPrice( - $bundleProduct, - $this->getSelectionPrice($bundleProduct, $selectionProduct, $qty) - ); + $selectionPrice = $this->getSelectionPrice($bundleProduct, $selectionProduct, $qty); + $specialPrice = $this->_applySpecialPrice($bundleProduct, $selectionPrice); + $groupPrice = $this->_applyGroupPrice($bundleProduct, $selectionPrice); + return min($specialPrice, $groupPrice); } /** @@ -401,9 +416,9 @@ public function getSelectionPreFinalPrice($bundleProduct, $selectionProduct, $qt * * @param Mage_Catalog_Model_Product $bundleProduct * @param Mage_Catalog_Model_Product $selectionProduct - * @param decimal $bundleQty - * @param decimal $selectionQty - * @param bool $multiplyQty + * @param decimal $bundleQty + * @param decimal $selectionQty + * @param bool $multiplyQty * @return decimal */ public function getSelectionFinalPrice($bundleProduct, $selectionProduct, $bundleQty, $selectionQty = null, @@ -419,10 +434,10 @@ public function getSelectionFinalPrice($bundleProduct, $selectionProduct, $bundl * * @param Mage_Catalog_Model_Product $bundleProduct * @param Mage_Catalog_Model_Product $selectionProduct - * @param decimal $bundleQty - * @param decimal $selectionQty - * @param bool $multiplyQty - * @param bool $takeTierPrice + * @param decimal $bundleQty + * @param decimal $selectionQty + * @param bool $multiplyQty + * @param bool $takeTierPrice * @return decimal */ public function getSelectionFinalTotalPrice($bundleProduct, $selectionProduct, $bundleQty, $selectionQty, @@ -432,22 +447,80 @@ public function getSelectionFinalTotalPrice($bundleProduct, $selectionProduct, $ // apply bundle special price $specialPrice = $this->_applySpecialPrice($bundleProduct, $selectionPrice); + // apply bundle group price + $groupPrice = $this->_applyGroupPrice($bundleProduct, $selectionPrice); if ($takeTierPrice) { // apply bundle tier price $tierPrice = $this->_applyTierPrice($bundleProduct, $bundleQty, $selectionPrice); - return min(array($tierPrice, $specialPrice)); + return min(array($groupPrice, $tierPrice, $specialPrice)); } else { - return $specialPrice; + return min(array($groupPrice, $specialPrice)); } } + /** + * Apply group price for bundle product + * + * @param Mage_Catalog_Model_Product $product + * @param float $finalPrice + * @return float + */ + protected function _applyGroupPrice($product, $finalPrice) + { + $result = $finalPrice; + $groupPrice = $product->getGroupPrice(); + + if (is_numeric($groupPrice)) { + $groupPrice = $finalPrice - ($finalPrice * ($groupPrice / 100)); + $result = min($finalPrice, $groupPrice); + } + + return $result; + } + + /** + * Get product group price + * + * @param Mage_Catalog_Model_Product $product + * @return float|null + */ + public function getGroupPrice($product) + { + $groupPrices = $product->getData('group_price'); + + if (is_null($groupPrices)) { + $attribute = $product->getResource()->getAttribute('group_price'); + if ($attribute) { + $attribute->getBackend()->afterLoad($product); + $groupPrices = $product->getData('group_price'); + } + } + + if (is_null($groupPrices) || !is_array($groupPrices)) { + return null; + } + + $customerGroup = $this->_getCustomerGroupId($product); + + $matchedPrice = 0; + + foreach ($groupPrices as $groupPrice) { + if ($groupPrice['cust_group'] == $customerGroup && $groupPrice['website_price'] > $matchedPrice) { + $matchedPrice = $groupPrice['website_price']; + break; + } + } + + return $matchedPrice; + } + /** * Apply tier price for bundle * * @param Mage_Catalog_Model_Product $product - * @param decimal $qty - * @param decimal $finalPrice + * @param decimal $qty + * @param decimal $finalPrice * @return decimal */ protected function _applyTierPrice($product, $qty, $finalPrice) @@ -469,7 +542,7 @@ protected function _applyTierPrice($product, $qty, $finalPrice) /** * Get product tier price by qty * - * @param decimal $qty + * @param decimal $qty * @param Mage_Catalog_Model_Product $product * @return decimal */ @@ -554,14 +627,14 @@ public function getTierPrice($qty=null, $product) /** * Calculate product price based on special price data and price rules * - * @param float $basePrice - * @param float $specialPrice - * @param string $specialPriceFrom - * @param string $specialPriceTo + * @param float $basePrice + * @param float $specialPrice + * @param string $specialPriceFrom + * @param string $specialPriceTo * @param float|null|false $rulePrice - * @param mixed $wId - * @param mixed $gId - * @param null|int $productId + * @param mixed $wId + * @param mixed $gId + * @param null|int $productId * @return float */ public static function calculatePrice($basePrice, $specialPrice, $specialPriceFrom, $specialPriceTo, @@ -698,11 +771,11 @@ public static function calculatePrice($basePrice, $specialPrice, $specialPriceFr /** * Calculate and apply special price * - * @param float $finalPrice - * @param float $specialPrice + * @param float $finalPrice + * @param float $specialPrice * @param string $specialPriceFrom * @param string $specialPriceTo - * @param mixed $store + * @param mixed $store * @return float */ public static function calculateSpecialPrice($finalPrice, $specialPrice, $specialPriceFrom, $specialPriceTo, @@ -719,11 +792,11 @@ public static function calculateSpecialPrice($finalPrice, $specialPrice, $specia } /** - * Check is tier price value fixed or percent of original price + * Check is group price value fixed or percent of original price * * @return bool */ - public function isTierPriceFixed() + public function isGroupPriceFixed() { return false; } diff --git a/app/code/core/Mage/Bundle/Model/Resource/Indexer/Price.php b/app/code/core/Mage/Bundle/Model/Resource/Indexer/Price.php index 96a97e2512..b04b26ef70 100755 --- a/app/code/core/Mage/Bundle/Model/Resource/Indexer/Price.php +++ b/app/code/core/Mage/Bundle/Model/Resource/Indexer/Price.php @@ -42,7 +42,15 @@ class Mage_Bundle_Model_Resource_Indexer_Price extends Mage_Catalog_Model_Resour public function reindexAll() { $this->useIdxTable(true); - $this->_prepareBundlePrice(); + + $this->beginTransaction(); + try { + $this->_prepareBundlePrice(); + $this->commit(); + } catch (Exception $e) { + $this->rollBack(); + throw $e; + } return $this; } @@ -162,7 +170,13 @@ protected function _prepareBundlePriceByType($priceType, $entityIds = null) ->joinLeft( array('tp' => $this->_getTierPriceIndexTable()), 'tp.entity_id = e.entity_id AND tp.website_id = cw.website_id' - . ' AND tp.customer_group_id = cg.customer_group_id', + . ' AND tp.customer_group_id = cg.customer_group_id', + array() + ) + ->joinLeft( + array('gp' => $this->_getGroupPriceIndexTable()), + 'gp.entity_id = e.entity_id AND gp.website_id = cw.website_id' + . ' AND gp.customer_group_id = cg.customer_group_id', array() ) ->where('e.type_id=?', $this->getTypeId()); @@ -179,7 +193,9 @@ protected function _prepareBundlePriceByType($priceType, $entityIds = null) if ($priceType == Mage_Bundle_Model_Product_Price::PRICE_TYPE_DYNAMIC) { $select->columns(array('tax_class_id' => new Zend_Db_Expr('0'))); } else { - $select->columns(array('tax_class_id' => $write->getCheckSql($taxClassId . ' IS NOT NULL', $taxClassId, 0))); + $select->columns( + array('tax_class_id' => $write->getCheckSql($taxClassId . ' IS NOT NULL', $taxClassId, 0)) + ); } $priceTypeCond = $write->quoteInto('=?', $priceType); @@ -191,7 +207,6 @@ protected function _prepareBundlePriceByType($priceType, $entityIds = null) $specialTo = $this->_addAttributeToSelect($select, 'special_to_date', 'e.entity_id', 'cs.store_id'); $curentDate = new Zend_Db_Expr('cwd.website_date'); - $specialExpr = $write->getCheckSql( $write->getCheckSql( $specialFrom . ' IS NULL', @@ -215,6 +230,13 @@ protected function _prepareBundlePriceByType($priceType, $entityIds = null) $specialPrice, '0' ); + + $groupPriceExpr = $write->getCheckSql( + 'gp.price IS NOT NULL AND gp.price > 0 AND gp.price < 100', + 'gp.price', + '0' + ); + $tierExpr = new Zend_Db_Expr("tp.min_price"); if ($priceType == Mage_Bundle_Model_Product_Price::PRICE_TYPE_FIXED) { @@ -225,24 +247,38 @@ protected function _prepareBundlePriceByType($priceType, $entityIds = null) ); $tierPrice = $write->getCheckSql( $tierExpr . ' IS NOT NULL', - 'ROUND(' . $price .' - ' . '(' . $price . ' * (' . $tierExpr . ' / 100)), 4)', + 'ROUND(' . $price . ' - ' . '(' . $price . ' * (' . $tierExpr . ' / 100)), 4)', 'NULL' ); + $groupPrice = $write->getCheckSql( + $groupPriceExpr . ' > 0', + 'ROUND(' . $price . ' - ' . '(' . $price . ' * (' . $groupPriceExpr . ' / 100)), 4)', + 'NULL' + ); + $finalPrice = $write->getCheckSql( + "{$groupPrice} IS NOT NULL AND {$groupPrice} < {$finalPrice}", + $groupPrice, + $finalPrice + ); } else { $finalPrice = new Zend_Db_Expr("0"); $tierPrice = $write->getCheckSql($tierExpr . ' IS NOT NULL', '0', 'NULL'); + $groupPrice = $write->getCheckSql($groupPriceExpr . ' > 0', $groupPriceExpr, 'NULL'); } $select->columns(array( - 'price_type' => new Zend_Db_Expr($priceType), - 'special_price' => $specialExpr, - 'tier_percent' => $tierExpr, - 'orig_price' => $write->getCheckSql($price . ' IS NULL', '0', $price), - 'price' => $finalPrice, - 'min_price' => $finalPrice, - 'max_price' => $finalPrice, - 'tier_price' => $tierPrice, - 'base_tier' => $tierPrice, + 'price_type' => new Zend_Db_Expr($priceType), + 'special_price' => $specialExpr, + 'tier_percent' => $tierExpr, + 'orig_price' => $write->getCheckSql($price . ' IS NULL', '0', $price), + 'price' => $finalPrice, + 'min_price' => $finalPrice, + 'max_price' => $finalPrice, + 'tier_price' => $tierPrice, + 'base_tier' => $tierPrice, + 'group_price' => $groupPrice, + 'base_group_price' => $groupPrice, + 'group_price_percent' => new Zend_Db_Expr('gp.price'), )); if (!is_null($entityIds)) { @@ -292,6 +328,8 @@ protected function _calculateBundleOptionPrice() 'max_price' => $write->getCheckSql('i.group_type = 1', 'SUM(i.price)', 'MAX(i.price)'), 'tier_price' => $write->getCheckSql('i.is_required = 1', 'MIN(i.tier_price)', '0'), 'alt_tier_price' => $write->getCheckSql('i.is_required = 0', 'MIN(i.tier_price)', '0'), + 'group_price' => $write->getCheckSql('i.is_required = 1', 'MIN(i.group_price)', '0'), + 'alt_group_price' => $write->getCheckSql('i.is_required = 0', 'MIN(i.group_price)', '0'), )); $query = $select->insertFromSelect($this->_getBundleOptionTable()); @@ -314,6 +352,15 @@ protected function _calculateBundleOptionPrice() ) . ' + MIN(i.tier_price)', 'NULL' ); + $groupPrice = $write->getCheckSql( + 'MIN(i.group_price_percent) IS NOT NULL', + $write->getCheckSql( + 'SUM(io.group_price) = 0', + 'SUM(io.alt_group_price)', + 'SUM(io.group_price)' + ) . ' + MIN(i.group_price)', + 'NULL' + ); $select = $write->select() ->from( @@ -329,12 +376,14 @@ protected function _calculateBundleOptionPrice() ->group(array('io.entity_id', 'io.customer_group_id', 'io.website_id', 'i.tax_class_id', 'i.orig_price', 'i.price')) ->columns(array('i.tax_class_id', - 'orig_price' => 'i.orig_price', - 'price' => 'i.price', - 'min_price' => $minPrice, - 'max_price' => $maxPrice, - 'tier_price' => $tierPrice, - 'base_tier' => 'MIN(i.base_tier)' + 'orig_price' => 'i.orig_price', + 'price' => 'i.price', + 'min_price' => $minPrice, + 'max_price' => $maxPrice, + 'tier_price' => $tierPrice, + 'base_tier' => 'MIN(i.base_tier)', + 'group_price' => $groupPrice, + 'base_group_price' => 'MIN(i.base_group_price)', )); $query = $select->insertFromSelect($this->_getDefaultFinalPriceTable()); @@ -377,7 +426,6 @@ protected function _calculateBundleSelectionPrice($priceType) ) . '* bs.selection_qty' ); - $tierExpr = $write->getCheckSql( 'i.base_tier IS NOT NULL', $write->getCheckSql( @@ -392,6 +440,24 @@ protected function _calculateBundleSelectionPrice($priceType) ) . ' * bs.selection_qty', 'NULL' ); + + $groupExpr = $write->getCheckSql( + 'i.base_group_price IS NOT NULL', + $write->getCheckSql( + $selectionPriceType .' = 1', + $priceExpr, + $write->getCheckSql( + 'i.group_price_percent > 0', + 'ROUND(' . $selectionPriceValue + . ' - (' . $selectionPriceValue . ' * (i.group_price_percent / 100)),4)', + $selectionPriceValue + ) + ) . ' * bs.selection_qty', + 'NULL' + ); + $priceExpr = new Zend_Db_Expr( + $write->getCheckSql("{$groupExpr} < {$priceExpr}", $groupExpr, $priceExpr) + ); } else { $priceExpr = new Zend_Db_Expr( $write->getCheckSql( @@ -405,7 +471,21 @@ protected function _calculateBundleSelectionPrice($priceType) 'ROUND(idx.min_price * (i.base_tier / 100), 4)* bs.selection_qty', 'NULL' ); - + $groupExpr = $write->getCheckSql( + 'i.base_group_price IS NOT NULL', + 'ROUND(idx.min_price * (i.base_group_price / 100), 4)* bs.selection_qty', + 'NULL' + ); + $groupPriceExpr = new Zend_Db_Expr( + $write->getCheckSql( + 'i.base_group_price IS NOT NULL AND i.base_group_price > 0 AND i.base_group_price < 100', + 'ROUND(idx.min_price - idx.min_price * (i.base_group_price / 100), 4)', + 'idx.min_price' + ) . ' * bs.selection_qty' + ); + $priceExpr = new Zend_Db_Expr( + $write->getCheckSql("{$groupPriceExpr} < {$priceExpr}", $groupPriceExpr, $priceExpr) + ); } $select = $write->select() @@ -449,6 +529,7 @@ protected function _calculateBundleSelectionPrice($priceType) 'is_required' => 'bo.required', 'price' => $priceExpr, 'tier_price' => $tierExpr, + 'group_price' => $groupExpr, )); $query = $select->insertFromSelect($this->_getBundleSelectionTable()); @@ -466,6 +547,7 @@ protected function _calculateBundleSelectionPrice($priceType) protected function _prepareBundlePrice($entityIds = null) { $this->_prepareTierPriceIndex($entityIds); + $this->_prepareGroupPriceIndex($entityIds); $this->_prepareBundlePriceTable(); $this->_prepareBundlePriceByType(Mage_Bundle_Model_Product_Price::PRICE_TYPE_FIXED, $entityIds); $this->_prepareBundlePriceByType(Mage_Bundle_Model_Product_Price::PRICE_TYPE_DYNAMIC, $entityIds); @@ -554,4 +636,63 @@ protected function _prepareTierPriceIndex($entityIds = null) return $this; } + + /** + * Prepare percentage group price for bundle products + * + * @see Mage_Catalog_Model_Resource_Product_Indexer_Price::_prepareGroupPriceIndex + * + * @param int|array $entityIds + * @return Mage_Bundle_Model_Resource_Indexer_Price + */ + protected function _prepareGroupPriceIndex($entityIds = null) + { + $adapter = $this->_getWriteAdapter(); + + // remove index by bundle products + $select = $adapter->select() + ->from(array('i' => $this->_getGroupPriceIndexTable()), null) + ->join( + array('e' => $this->getTable('catalog/product')), + 'i.entity_id=e.entity_id', + array() + ) + ->where('e.type_id=?', $this->getTypeId()); + $query = $select->deleteFromSelect('i'); + $adapter->query($query); + + $select = $adapter->select() + ->from( + array('gp' => $this->getValueTable('catalog/product', 'group_price')), + array('entity_id') + ) + ->join( + array('e' => $this->getTable('catalog/product')), + 'gp.entity_id=e.entity_id', + array() + ) + ->join( + array('cg' => $this->getTable('customer/customer_group')), + 'gp.all_groups = 1 OR (gp.all_groups = 0 AND gp.customer_group_id = cg.customer_group_id)', + array('customer_group_id') + ) + ->join( + array('cw' => $this->getTable('core/website')), + 'gp.website_id = 0 OR gp.website_id = cw.website_id', + array('website_id') + ) + ->where('cw.website_id != 0') + ->where('e.type_id=?', $this->getTypeId()) + ->columns(new Zend_Db_Expr('MIN(gp.value)')) + ->group(array('gp.entity_id', 'cg.customer_group_id', 'cw.website_id')); + + if (!empty($entityIds)) { + $select->where('gp.entity_id IN(?)', $entityIds); + } + + $query = $select->insertFromSelect($this->_getGroupPriceIndexTable()); + $adapter->query($query); + + return $this; + } } diff --git a/app/code/core/Mage/Bundle/Model/Resource/Indexer/Stock.php b/app/code/core/Mage/Bundle/Model/Resource/Indexer/Stock.php index 094d1d0a4e..b2d3e03169 100755 --- a/app/code/core/Mage/Bundle/Model/Resource/Indexer/Stock.php +++ b/app/code/core/Mage/Bundle/Model/Resource/Indexer/Stock.php @@ -34,18 +34,6 @@ */ class Mage_Bundle_Model_Resource_Indexer_Stock extends Mage_CatalogInventory_Model_Resource_Indexer_Stock_Default { - /** - * Reindex temporary (price result data) for all products - * - * @return Mage_Bundle_Model_Resource_Indexer_Stock - */ - public function reindexAll() - { - $this->useIdxTable(true); - $this->_prepareIndexTable(); - return $this; - } - /** * Reindex temporary (price result data) for defined product(s) * diff --git a/app/code/core/Mage/Bundle/etc/config.xml b/app/code/core/Mage/Bundle/etc/config.xml index e2c943b0c9..9c9d35d843 100644 --- a/app/code/core/Mage/Bundle/etc/config.xml +++ b/app/code/core/Mage/Bundle/etc/config.xml @@ -28,7 +28,7 @@ - 1.6.0.0 + 1.6.0.0.1 diff --git a/app/code/core/Mage/Bundle/sql/bundle_setup/mysql4-upgrade-1.6.0.0-1.6.0.0.1.php b/app/code/core/Mage/Bundle/sql/bundle_setup/mysql4-upgrade-1.6.0.0-1.6.0.0.1.php new file mode 100644 index 0000000000..b56fc79f5c --- /dev/null +++ b/app/code/core/Mage/Bundle/sql/bundle_setup/mysql4-upgrade-1.6.0.0-1.6.0.0.1.php @@ -0,0 +1,44 @@ +getConnection(); +$memoryTables = array( + 'bundle/option_indexer_tmp', + 'bundle/selection_indexer_tmp', + 'bundle/price_indexer_tmp', +); + +foreach ($memoryTables as $table) { + $connection->changeTableEngine($installer->getTable($table), Varien_Db_Adapter_Pdo_Mysql::ENGINE_MEMORY); +} diff --git a/app/code/core/Mage/Bundle/sql/bundle_setup/upgrade-1.6.0.0-1.6.0.0.1.php b/app/code/core/Mage/Bundle/sql/bundle_setup/upgrade-1.6.0.0-1.6.0.0.1.php new file mode 100644 index 0000000000..c715cca8d4 --- /dev/null +++ b/app/code/core/Mage/Bundle/sql/bundle_setup/upgrade-1.6.0.0-1.6.0.0.1.php @@ -0,0 +1,84 @@ +getConnection(); + +$priceIndexerTables = array( + 'bundle/price_indexer_idx', + 'bundle/price_indexer_tmp', +); + +$optionsPriceIndexerTables = array( + 'bundle/option_indexer_idx', + 'bundle/option_indexer_tmp', +); + +$selectionPriceIndexerTables = array( + 'bundle/selection_indexer_idx', + 'bundle/selection_indexer_tmp', +); + +foreach ($priceIndexerTables as $table) { + $connection->addColumn($installer->getTable($table), 'group_price', array( + 'type' => Varien_Db_Ddl_Table::TYPE_DECIMAL, + 'length' => '12,4', + 'comment' => 'Group price', + )); + $connection->addColumn($installer->getTable($table), 'base_group_price', array( + 'type' => Varien_Db_Ddl_Table::TYPE_DECIMAL, + 'length' => '12,4', + 'comment' => 'Base Group Price', + )); + $connection->addColumn($installer->getTable($table), 'group_price_percent', array( + 'type' => Varien_Db_Ddl_Table::TYPE_DECIMAL, + 'length' => '12,4', + 'comment' => 'Group Price Percent', + )); +} + +foreach (array_merge($optionsPriceIndexerTables, $selectionPriceIndexerTables) as $table) { + $connection->addColumn($installer->getTable($table), 'group_price', array( + 'type' => Varien_Db_Ddl_Table::TYPE_DECIMAL, + 'length' => '12,4', + 'comment' => 'Group price', + )); +} + +foreach ($optionsPriceIndexerTables as $table) { + $connection->addColumn($installer->getTable($table), 'alt_group_price', array( + 'type' => Varien_Db_Ddl_Table::TYPE_DECIMAL, + 'length' => '12,4', + 'comment' => 'Alt Group Price', + )); +} + +$applyTo = explode(',', $installer->getAttribute(Mage_Catalog_Model_Product::ENTITY, 'group_price', 'apply_to')); +if (!in_array('bundle', $applyTo)) { + $applyTo[] = 'bundle'; + $installer->updateAttribute(Mage_Catalog_Model_Product::ENTITY, 'group_price', 'apply_to', implode(',', $applyTo)); +} diff --git a/app/code/core/Mage/Captcha/Block/Captcha.php b/app/code/core/Mage/Captcha/Block/Captcha.php new file mode 100755 index 0000000000..b934ed1dca --- /dev/null +++ b/app/code/core/Mage/Captcha/Block/Captcha.php @@ -0,0 +1,48 @@ + + */ +class Mage_Captcha_Block_Captcha extends Mage_Core_Block_Template +{ + /** + * Renders captcha HTML (if required) + * + * @return string + */ + protected function _toHtml() + { + $blockPath = Mage::helper('captcha')->getCaptcha($this->getFormId())->getBlockName(); + $block = $this->getLayout()->createBlock($blockPath); + $block->setData($this->getData()); + return $block->toHtml(); + } +} diff --git a/app/code/core/Mage/Captcha/Block/Captcha/Zend.php b/app/code/core/Mage/Captcha/Block/Captcha/Zend.php new file mode 100755 index 0000000000..02e1c78cf5 --- /dev/null +++ b/app/code/core/Mage/Captcha/Block/Captcha/Zend.php @@ -0,0 +1,87 @@ + + */ +class Mage_Captcha_Block_Captcha_Zend extends Mage_Core_Block_Template +{ + protected $_template = 'captcha/zend.phtml'; + + /** + * @var string + */ + protected $_captcha; + + /** + * Returns template path + * + * @return string + */ + public function getTemplate() + { + return $this->getIsAjax() ? '' : $this->_template; + } + + /** + * Returns URL to controller action which returns new captcha image + * + * @return string + */ + public function getRefreshUrl() + { + $url = Mage::app()->getStore()->isAdmin() ? "adminhtml/refresh/refresh" : "captcha/refresh"; + return Mage::getUrl($url, array('_secure' => Mage::app()->getRequest()->isSecure())); + } + + /** + * Renders captcha HTML (if required) + * + * @return string + */ + protected function _toHtml() + { + if ($this->getCaptchaModel()->isRequired()) { + $this->getCaptchaModel()->generate(); + return parent::_toHtml(); + } + return ''; + } + + /** + * Returns captcha model + * + * @return Mage_Captcha_Model_Abstract + */ + public function getCaptchaModel() + { + return Mage::helper('captcha')->getCaptcha($this->getFormId()); + } +} diff --git a/app/code/core/Mage/Captcha/Helper/Data.php b/app/code/core/Mage/Captcha/Helper/Data.php new file mode 100755 index 0000000000..ff9f08bb3b --- /dev/null +++ b/app/code/core/Mage/Captcha/Helper/Data.php @@ -0,0 +1,138 @@ + + */ +class Mage_Captcha_Helper_Data extends Mage_Core_Helper_Abstract +{ + /** + * Used for "name" attribute of captcha's input field + */ + const INPUT_NAME_FIELD_VALUE = 'captcha'; + + /** + * Always show captcha + */ + const MODE_ALWAYS = 'always'; + + /** + * Show captcha only after certain number of unsuccessful attempts + */ + const MODE_AFTER_FAIL = 'after_fail'; + + /** + * Captcha fonts path + */ + const XML_PATH_CAPTCHA_FONTS = 'default/captcha/fonts'; + + /** + * List uses Models of Captcha + * @var array + */ + protected $_captcha = array(); + + /** + * Get Captcha + * + * @param string $formId + * @return Mage_Captcha_Model_Interface + */ + public function getCaptcha($formId) + { + if (!array_key_exists($formId, $this->_captcha)) { + $type = $this->getConfigNode('type'); + $this->_captcha[$formId] = Mage::getModel('captcha/' . $type, array('formId' => $formId)); + } + return $this->_captcha[$formId]; + } + + /** + * Returns value of the node with respect to current area (frontend or backend) + * + * @param string $id The last part of XML_PATH_$area_CAPTCHA_ constant (case insensitive) + * @param Mage_Core_Model_Store $store + * @return Mage_Core_Model_Config_Element + */ + public function getConfigNode($id, $store = null) + { + $areaCode = Mage::app()->getStore($store)->isAdmin() ? 'admin' : 'customer'; + return Mage::getStoreConfig( $areaCode . '/captcha/' . $id, $store); + } + + /** + * Get list of available fonts + * Return format: + * [['arial'] => ['label' => 'Arial', 'path' => '/www/magento/fonts/arial.ttf']] + * + * @return array + */ + public function getFonts() + { + $node = Mage::getConfig()->getNode(Mage_Captcha_Helper_Data::XML_PATH_CAPTCHA_FONTS); + $fonts = array(); + if ($node) { + foreach ($node->children() as $fontName => $fontNode) { + $fonts[$fontName] = array( + 'label' => (string)$fontNode->label, + 'path' => Mage::getBaseDir('base') . DS . $fontNode->path + ); + } + } + return $fonts; + } + + /** + * Get captcha image directory + * + * @param mixed $website + * @return string + */ + public function getImgDir($website = null) + { + $websiteCode = Mage::app()->getWebsite($website)->getCode(); + $captchaDir = Mage::getBaseDir('media') . DS . 'captcha' . DS . $websiteCode . DS; + $io = new Varien_Io_File(); + $io->checkAndCreateFolder($captchaDir, 0755); + return $captchaDir; + } + + /** + * Get captcha image base URL + * + * @param mixed $website + * @return string + */ + public function getImgUrl($website = null) + { + $websiteCode = Mage::app()->getWebsite($website)->getCode(); + return Mage::getBaseUrl('media') . 'captcha' . '/' . $websiteCode . '/'; + } +} diff --git a/app/code/core/Mage/Captcha/Model/Captcha.php b/app/code/core/Mage/Captcha/Model/Captcha.php new file mode 100755 index 0000000000..2cfb2031b7 --- /dev/null +++ b/app/code/core/Mage/Captcha/Model/Captcha.php @@ -0,0 +1,149 @@ + + */ +class Mage_Captcha_Model_Captcha implements Mage_Captcha_Model_Interface +{ + protected $_model; + + /** + * Zend captcha constructor + * + * @param array $params + */ + public function __construct($params) + { + $type = Mage::helper('captcha')->getConfigNode('type'); + $this->_model = Mage::getModel('captcha/' . $type, $params); + } + + /** + * Whether to respect case while checking the answer + * + * @return bool + */ + public function isCaseSensitive() + { + return $this->_model->isCaseSensitive(); + } + + /** + * Generates captcha + * + */ + public function generate() + { + $this->_model->generate(); + } + + /** + * Checks whether word entered by user corresponds to the one generated by generate() + * + * @param string $word + * @return void + */ + public function isCorrect($word) + { + return $this->_model->isCorrect($word); + } + + /** + * Get captcha image base URL + * + * @return string + */ + public function getImgUrl() + { + return $this->_model->getImgUrl(); + } + + /** + * Return full URL to captcha image + * + * @return string + */ + public function getImgSrc() + { + return $this->_model->getImgSrc(); + } + + + /** + * Returns session instance + * + * @return Captcha_Zend_Model_Session + */ + public function getSession() + { + return $this->_model->getSession(); + } + + /** + * Returns Captcha Width + * + * @return string + */ + public function getWidth() + { + return $this->_model->getWidth(); + } + + /** + * Returns Captcha Height + * + * @return string + */ + public function getHeight() + { + return $this->_model->getHeight(); + } + + /** + * Returns Captcha Height + * + * @return string + */ + public function getImgAlt() + { + return $this->_model->getImgAlt(); + } + + /** + * Returns Template Path + * + * @return string + */ + public function getTemplatePath() + { + return $this->_model->getTemplatePath(); + } +} diff --git a/app/code/core/Mage/Captcha/Model/Config/Font.php b/app/code/core/Mage/Captcha/Model/Config/Font.php new file mode 100755 index 0000000000..cbba443fc9 --- /dev/null +++ b/app/code/core/Mage/Captcha/Model/Config/Font.php @@ -0,0 +1,49 @@ + + */ +class Mage_Captcha_Model_Config_Font +{ + /** + * Get options for font selection field + * + * @return array + */ + public function toOptionArray() + { + $optionArray = array(); + foreach (Mage::helper('captcha')->getFonts() as $fontName => $fontData) { + $optionArray[] = array('label' => $fontData['label'], 'value' => $fontName); + } + return $optionArray; + } +} diff --git a/app/code/core/Mage/Captcha/Model/Config/Form/Abstract.php b/app/code/core/Mage/Captcha/Model/Config/Form/Abstract.php new file mode 100755 index 0000000000..0a12ed2908 --- /dev/null +++ b/app/code/core/Mage/Captcha/Model/Config/Form/Abstract.php @@ -0,0 +1,61 @@ + + */ +abstract class Mage_Captcha_Model_Config_Form_Abstract extends Mage_Core_Model_Config_Data +{ + /** + * @var string + */ + protected $_configPath; + + /** + * Returns options for form multiselect + * + * @return array + */ + public function toOptionArray() + { + $optionArray = array(); + /* @var $backendNode Mage_Core_Model_Config_Element */ + $backendNode = Mage::getConfig()->getNode($this->_configPath); + if ($backendNode) { + foreach ($backendNode->children() as $formNode) { + /* @var $formNode Mage_Core_Model_Config_Element */ + if (!empty($formNode->label)) { + $optionArray[] = array('label' => (string)$formNode->label, 'value' => $formNode->getName()); + } + } + } + return $optionArray; + } +} diff --git a/app/code/core/Mage/Captcha/Model/Config/Form/Backend.php b/app/code/core/Mage/Captcha/Model/Config/Form/Backend.php new file mode 100755 index 0000000000..e469ee9ef3 --- /dev/null +++ b/app/code/core/Mage/Captcha/Model/Config/Form/Backend.php @@ -0,0 +1,40 @@ + + */ +class Mage_Captcha_Model_Config_Form_Backend extends Mage_Captcha_Model_Config_Form_Abstract +{ + /** + * @var string + */ + protected $_configPath = 'default/captcha/backend/areas'; +} diff --git a/app/code/core/Mage/Captcha/Model/Config/Form/Frontend.php b/app/code/core/Mage/Captcha/Model/Config/Form/Frontend.php new file mode 100755 index 0000000000..131402e323 --- /dev/null +++ b/app/code/core/Mage/Captcha/Model/Config/Form/Frontend.php @@ -0,0 +1,40 @@ + + */ +class Mage_Captcha_Model_Config_Form_Frontend extends Mage_Captcha_Model_Config_Form_Abstract +{ + /** + * @var string + */ + protected $_configPath = 'default/captcha/frontend/areas'; +} diff --git a/app/code/core/Mage/Captcha/Model/Config/Mode.php b/app/code/core/Mage/Captcha/Model/Config/Mode.php new file mode 100755 index 0000000000..4bfe1a409f --- /dev/null +++ b/app/code/core/Mage/Captcha/Model/Config/Mode.php @@ -0,0 +1,54 @@ + + */ +class Mage_Captcha_Model_Config_Mode +{ + /** + * Get options for captcha mode selection field + * + * @return array + */ + public function toOptionArray() + { + return array( + array( + 'label' => Mage::helper('captcha')->__('Always'), + 'value' => Mage_Captcha_Helper_Data::MODE_ALWAYS + ), + array( + 'label' => Mage::helper('captcha')->__('After number of attempts to login'), + 'value' => Mage_Captcha_Helper_Data::MODE_AFTER_FAIL + ), + ); + } +} diff --git a/app/code/core/Mage/Captcha/Model/Interface.php b/app/code/core/Mage/Captcha/Model/Interface.php new file mode 100755 index 0000000000..90b2290bc2 --- /dev/null +++ b/app/code/core/Mage/Captcha/Model/Interface.php @@ -0,0 +1,60 @@ + + */ +interface Mage_Captcha_Model_Interface +{ + /** + * Generates captcha + * + * @abstract + * @return void + */ + public function generate(); + + /** + * Checks whether word entered by user corresponds to the one generated by generate() + * + * @abstract + * @param string $word + * @return void + */ + public function isCorrect($word); + + + /** + * Get Block Name + * + * @return string + */ + public function getBlockName(); +} diff --git a/app/code/core/Mage/Captcha/Model/Observer.php b/app/code/core/Mage/Captcha/Model/Observer.php new file mode 100755 index 0000000000..ebaf827b18 --- /dev/null +++ b/app/code/core/Mage/Captcha/Model/Observer.php @@ -0,0 +1,282 @@ + + */ +class Mage_Captcha_Model_Observer +{ + /** + * Check Captcha On Forgot Password Page + * + * @param Varien_Event_Observer $observer + * @return Mage_Captcha_Model_Observer + */ + public function checkForgotpassword($observer) + { + $formId = 'user_forgotpassword'; + $captchaModel = Mage::helper('captcha')->getCaptcha($formId); + if ($captchaModel->isRequired()) { + $controller = $observer->getControllerAction(); + if (!$captchaModel->isCorrect($this->_getCaptchaString($controller->getRequest(), $formId))) { + Mage::getSingleton('customer/session')->addError(Mage::helper('captcha')->__('Incorrect CAPTCHA.')); + $controller->setFlag('', Mage_Core_Controller_Varien_Action::FLAG_NO_DISPATCH, true); + $controller->getResponse()->setRedirect(Mage::getUrl('*/*/forgotpassword')); + } + } + return $this; + } + + /** + * Check Captcha On User Login Page + * + * @param Varien_Event_Observer $observer + * @return Mage_Captcha_Model_Observer + */ + public function checkUserLogin($observer) + { + $formId = 'user_login'; + $captchaModel = Mage::helper('captcha')->getCaptcha($formId); + $controller = $observer->getControllerAction(); + $loginParams = $controller->getRequest()->getPost('login'); + $login = array_key_exists('username', $loginParams) ? $loginParams['username'] : null; + if ($captchaModel->isRequired($login)) { + $word = $this->_getCaptchaString($controller->getRequest(), $formId); + if (!$captchaModel->isCorrect($word)) { + Mage::getSingleton('customer/session')->addError(Mage::helper('captcha')->__('Incorrect CAPTCHA.')); + $controller->setFlag('', Mage_Core_Controller_Varien_Action::FLAG_NO_DISPATCH, true); + Mage::getSingleton('customer/session')->setUsername($login); + $beforeUrl = Mage::getSingleton('customer/session')->getBeforeAuthUrl(); + $url = $beforeUrl ? $beforeUrl : Mage::helper('customer')->getLoginUrl(); + $controller->getResponse()->setRedirect($url); + } + } + $captchaModel->logAttempt($login); + return $this; + } + + /** + * Check Captcha On Register User Page + * + * @param Varien_Event_Observer $observer + * @return Mage_Captcha_Model_Observer + */ + public function checkUserCreate($observer) + { + $formId = 'user_create'; + $captchaModel = Mage::helper('captcha')->getCaptcha($formId); + if ($captchaModel->isRequired()) { + $controller = $observer->getControllerAction(); + if (!$captchaModel->isCorrect($this->_getCaptchaString($controller->getRequest(), $formId))) { + Mage::getSingleton('customer/session')->addError(Mage::helper('captcha')->__('Incorrect CAPTCHA.')); + $controller->setFlag('', Mage_Core_Controller_Varien_Action::FLAG_NO_DISPATCH, true); + Mage::getSingleton('customer/session')->setCustomerFormData($controller->getRequest()->getPost()); + $controller->getResponse()->setRedirect(Mage::getUrl('*/*/create')); + } + } + return $this; + } + + /** + * Check Captcha On Checkout as Guest Page + * + * @param Varien_Event_Observer $observer + * @return Mage_Captcha_Model_Observer + */ + public function checkGuestCheckout($observer) + { + $formId = 'guest_checkout'; + $captchaModel = Mage::helper('captcha')->getCaptcha($formId); + $checkoutMethod = Mage::getSingleton('checkout/type_onepage')->getQuote()->getCheckoutMethod(); + if ($checkoutMethod == Mage_Checkout_Model_Type_Onepage::METHOD_GUEST) { + if ($captchaModel->isRequired()) { + $controller = $observer->getControllerAction(); + if (!$captchaModel->isCorrect($this->_getCaptchaString($controller->getRequest(), $formId))) { + $controller->setFlag('', Mage_Core_Controller_Varien_Action::FLAG_NO_DISPATCH, true); + $result = array('error' => 1, 'message' => Mage::helper('captcha')->__('Incorrect CAPTCHA.')); + $controller->getResponse()->setBody(Mage::helper('core')->jsonEncode($result)); + } + } + } + return $this; + } + + /** + * Check Captcha On Checkout Register Page + * + * @param Varien_Event_Observer $observer + * @return Mage_Captcha_Model_Observer + */ + public function checkRegisterCheckout($observer) + { + $formId = 'register_during_checkout'; + $captchaModel = Mage::helper('captcha')->getCaptcha($formId); + $checkoutMethod = Mage::getSingleton('checkout/type_onepage')->getQuote()->getCheckoutMethod(); + if ($checkoutMethod == Mage_Checkout_Model_Type_Onepage::METHOD_REGISTER) { + if ($captchaModel->isRequired()) { + $controller = $observer->getControllerAction(); + if (!$captchaModel->isCorrect($this->_getCaptchaString($controller->getRequest(), $formId))) { + $controller->setFlag('', Mage_Core_Controller_Varien_Action::FLAG_NO_DISPATCH, true); + $result = array('error' => 1, 'message' => Mage::helper('captcha')->__('Incorrect CAPTCHA.')); + $controller->getResponse()->setBody(Mage::helper('core')->jsonEncode($result)); + } + } + } + return $this; + } + + /** + * Check Captcha On User Login Backend Page + * + * @param Varien_Event_Observer $observer + * @return Mage_Captcha_Model_Observer + */ + public function checkUserLoginBackend($observer) + { + $formId = 'backend_login'; + $captchaModel = Mage::helper('captcha')->getCaptcha($formId); + $loginParams = Mage::app()->getRequest()->getPost('login'); + $login = array_key_exists('username', $loginParams) ? $loginParams['username'] : null; + if ($captchaModel->isRequired($login)) { + if (!$captchaModel->isCorrect($this->_getCaptchaString(Mage::app()->getRequest(), $formId))) { + $captchaModel->logAttempt($login); + Mage::throwException(Mage::helper('captcha')->__('Incorrect CAPTCHA.')); + } + } + $captchaModel->logAttempt($login); + return $this; + } + + /** + * Check Captcha On User Login Backend Page + * + * @param Varien_Event_Observer $observer + * @return Mage_Captcha_Model_Observer + */ + public function checkUserForgotPasswordBackend($observer) + { + $formId = 'backend_forgotpassword'; + $captchaModel = Mage::helper('captcha')->getCaptcha($formId); + $controller = $observer->getControllerAction(); + $email = (string) $observer->getControllerAction()->getRequest()->getParam('email'); + $params = $observer->getControllerAction()->getRequest()->getParams(); + + if (!empty($email) && !empty($params)){ + if ($captchaModel->isRequired()){ + if (!$captchaModel->isCorrect($this->_getCaptchaString($controller->getRequest(), $formId))) { + $this->_getSession()->setEmail((string) $controller->getRequest()->getPost('email')); + $controller->setFlag('', Mage_Core_Controller_Varien_Action::FLAG_NO_DISPATCH, true); + $this->_getSession()->addError(Mage::helper('captcha')->__('Incorrect CAPTCHA.')); + $controller->getResponse()->setRedirect(Mage::getUrl('*/*/forgotpassword')); + } + } + } + return $this; + } + + /** + * Reset Attempts For Frontend + * + * @param Varien_Event_Observer $observer + * @return Mage_Captcha_Model_Observer + */ + public function resetAttemptForFrontend($observer) + { + return $this->_resetAttempt($observer->getModel()->getEmail()); + } + + /** + * Reset Attempts For Backend + * + * @param Varien_Event_Observer $observer + * @return Mage_Captcha_Model_Observer + */ + public function resetAttemptForBackend($observer) + { + return $this->_resetAttempt($observer->getUser()->getUsername()); + } + + /** + * Delete Unnecessary logged attempts + * + * @return Mage_Captcha_Model_Observer + */ + public function deleteOldAttempts() + { + Mage::getResourceModel('captcha/log')->deleteOldAttempts(); + return $this; + } + + /** + * Delete Expired Captcha Images + * + * @return Mage_Captcha_Model_Observer + */ + public function deleteExpiredImages() + { + foreach (Mage::app()->getWebsites(true) as $website){ + $expire = time() - Mage::helper('captcha')->getConfigNode('timeout', $website->getDefaultStore())*60; + $imageDirectory = Mage::helper('captcha')->getImgDir($website); + foreach (new DirectoryIterator($imageDirectory) as $file) { + if ($file->isFile() && pathinfo($file->getFilename(), PATHINFO_EXTENSION) == 'png') { + if ($file->getMTime() < $expire) { + unlink($file->getPathname()); + } + } + } + } + return $this; + } + + /** + * Reset Attempts + * + * @param string $login + * @return Mage_Captcha_Model_Observer + */ + protected function _resetAttempt($login) + { + Mage::getResourceModel('captcha/log')->deleteUserAttempts($login); + return $this; + } + + /** + * Get Captcha String + * + * @param Varien_Object $request + * @param string $formId + * @return string + */ + protected function _getCaptchaString($request, $formId) + { + $captchaParams = $request->getPost(Mage_Captcha_Helper_Data::INPUT_NAME_FIELD_VALUE); + return $captchaParams[$formId]; + } +} diff --git a/app/code/core/Mage/Captcha/Model/Resource/Log.php b/app/code/core/Mage/Captcha/Model/Resource/Log.php new file mode 100755 index 0000000000..beef4f1884 --- /dev/null +++ b/app/code/core/Mage/Captcha/Model/Resource/Log.php @@ -0,0 +1,157 @@ + + */ +class Mage_Captcha_Model_Resource_Log extends Mage_Core_Model_Resource_Db_Abstract +{ + /** + * Type Remote Address + */ + const TYPE_REMOTE_ADDRESS = 1; + /** + * Type User Login Name + */ + const TYPE_LOGIN = 2; + + /** + * Define main table + * + */ + protected function _construct() + { + $this->_init('captcha/log', array('type','value')); + } + + /** + * Save or Update count Attempts + * + * @param string|null $login + * @return Mage_Captcha_Model_Resource_Log + */ + public function logAttempt($login) + { + if ($login != null){ + $this->_getWriteAdapter()->insertOnDuplicate( + $this->getMainTable(), + array( + 'type' => self::TYPE_LOGIN, 'value' => $login, 'count' => 1, + 'updated_at' => Mage::getSingleton('core/date')->gmtDate() + ), + array('count' => new Zend_Db_Expr('count+1'), 'updated_at') + ); + } + $ip = Mage::helper('core/http')->getRemoteAddr(); + if ($ip != null) { + $this->_getWriteAdapter()->insertOnDuplicate( + $this->getMainTable(), + array( + 'type' => self::TYPE_REMOTE_ADDRESS, 'value' => $ip, 'count' => 1, + 'updated_at' => Mage::getSingleton('core/date')->gmtDate() + ), + array('count' => new Zend_Db_Expr('count+1'), 'updated_at') + ); + } + return $this; + } + + /** + * Delete User attempts by login + * + * @param string $login + * @return Mage_Captcha_Model_Resource_Log + */ + public function deleteUserAttempts($login) + { + if ($login != null) { + $this->_getWriteAdapter()->delete( + $this->getMainTable(), + array('type = ?' => self::TYPE_LOGIN, 'value = ?' => $login) + ); + } + $ip = Mage::helper('core/http')->getRemoteAddr(); + if ($ip != null) { + $this->_getWriteAdapter()->delete( + $this->getMainTable(), array('type = ?' => self::TYPE_REMOTE_ADDRESS, 'value = ?' => $ip) + ); + } + + return $this; + } + + /** + * Get count attempts by ip + * + * @return null|int + */ + public function countAttemptsByRemoteAddress() + { + $ip = Mage::helper('core/http')->getRemoteAddr(); + if (!$ip) { + return 0; + } + $read = $this->_getReadAdapter(); + $select = $read->select()->from($this->getMainTable(), 'count')->where('type = ?', self::TYPE_REMOTE_ADDRESS) + ->where('value = ?', $ip); + return $read->fetchOne($select); + } + + /** + * Get count attempts by user login + * + * @param string $login + * @return null|int + */ + public function countAttemptsByUserLogin($login) + { + if (!$login) { + return 0; + } + $read = $this->_getReadAdapter(); + $select = $read->select()->from($this->getMainTable(), 'count')->where('type = ?', self::TYPE_LOGIN) + ->where('value = ?', $login); + return $read->fetchOne($select); + } + + /** + * Delete attempts with expired in update_at time + * + * @return void + */ + public function deleteOldAttempts() + { + $this->_getWriteAdapter()->delete( + $this->getMainTable(), + array('updated_at < ?' => Mage::getSingleton('core/date')->gmtDate(null, time() - 60*30)) + ); + } +} diff --git a/app/code/core/Mage/Captcha/Model/Resource/LoginAttempt.php b/app/code/core/Mage/Captcha/Model/Resource/LoginAttempt.php new file mode 100755 index 0000000000..3771a3953f --- /dev/null +++ b/app/code/core/Mage/Captcha/Model/Resource/LoginAttempt.php @@ -0,0 +1,160 @@ + + */ +class Mage_Captcha_Model_Resource_LoginAttempt extends Mage_Core_Model_Resource_Db_Abstract +{ + /** + * Define main table + * + */ + protected function _construct() + { + $this->_init('captcha/login_attempt', array('type','value')); + } + + /** + * Log Login + * + * @param string|null $login + * @return Mage_Captcha_Model_Resource_LoginAttempt + */ + public function logUserLogin($login){ + if ($login != null){ + + $this->_getWriteAdapter()->insertOnDuplicate( + $this->getMainTable(), + array( + 'type' => Mage_Captcha_Model_LoginAttempt::TYPE_LOGIN, + 'value' => md5($login), 'count' => 1, 'updated_at' => Mage::getSingleton('core/date')->gmtDate() + ), + array('count' => new Zend_Db_Expr('count+1'), 'updated_at') + ); + } + return $this; + } + + /** + * Log Ip + * + * @param string|null $ip + * @return Mage_Captcha_Model_Resource_LoginAttempt + */ + public function logRemoteAddress($ip){ + if ($ip != null) { + $this->_getWriteAdapter()->insertOnDuplicate( + $this->getMainTable(), + array( + 'type' => Mage_Captcha_Model_LoginAttempt::TYPE_REMOTE_ADDRESS, + 'value' => md5($ip), 'count' => 1, 'updated_at' => Mage::getSingleton('core/date')->gmtDate() + ), + array('count' => new Zend_Db_Expr('count+1'), 'updated_at') + ); + } + return $this; + } + + /** + * Delete attempts by remote address + * @param $ip + * @return Mage_Captcha_Model_Resource_LoginAttempt + */ + public function deleteByRemoteAddress($ip){ + if ($ip != null) { + $this->_getWriteAdapter()->delete( + $this->getMainTable(), + array('type = ?' => Mage_Captcha_Model_LoginAttempt::TYPE_REMOTE_ADDRESS, 'value = ?' => md5($ip)) + ); + } + return $this; + } + + /** + * Delete attempts by login + * + * @param $login + * @return Mage_Captcha_Model_Resource_LoginAttempt + */ + public function deleteByUserName($login){ + if ($login != null) { + $this->_getWriteAdapter()->delete( + $this->getMainTable(), + array('type = ?' => Mage_Captcha_Model_LoginAttempt::TYPE_LOGIN, 'value = ?' => md5($login)) + ); + } + return $this; + } + + /** + * Get count attempts by ip + * + * @param string $ip + * @return null|Mage_Captcha_Model_LoginAttempt + */ + public function countAttemptsByRemoteAddress($ip){ + if (!$ip) { + return 0; + } + $read = $this->_getReadAdapter(); + $select = $read->select() + ->from($this->getMainTable(), 'count') + ->where('type = ?', Mage_Captcha_Model_LoginAttempt::TYPE_REMOTE_ADDRESS) + ->where('value = ?', md5($ip)); + return $read->fetchOne($select); + } + + /** + * Get count attempts by user login + * + * @param string $login + * @return null|Mage_Captcha_Model_LoginAttempt + */ + public function countAttemptsByUserLogin($login){ + if (!$login) { + return 0; + } + $read = $this->_getReadAdapter(); + $select = $read->select() + ->from($this->getMainTable(), 'count') + ->where('type = ?', Mage_Captcha_Model_LoginAttempt::TYPE_LOGIN) + ->where('value = ?', md5($login)); + return $read->fetchOne($select); + } + + public function deleteOldAttempts(){ + $this->_getWriteAdapter()->delete( + $this->getMainTable(), + array('updated_at < ?' => Mage::getSingleton('core/date')->gmtDate(null, time() - 60*30)) + ); + } +} diff --git a/app/code/core/Mage/Captcha/Model/Resource/LoginAttempt/Collection.php b/app/code/core/Mage/Captcha/Model/Resource/LoginAttempt/Collection.php new file mode 100755 index 0000000000..2486e741cf --- /dev/null +++ b/app/code/core/Mage/Captcha/Model/Resource/LoginAttempt/Collection.php @@ -0,0 +1,45 @@ + + */ + +class Mage_Captcha_Model_Resource_LoginAttempt_Collection extends Mage_Core_Model_Resource_Db_Collection_Abstract +{ + /** + * Collection resource initialization + */ + protected function _construct() + { + $this->_init('captcha/loginAttempt'); + } +} diff --git a/app/code/core/Mage/Captcha/Model/Zend.php b/app/code/core/Mage/Captcha/Model/Zend.php new file mode 100755 index 0000000000..26cdfe2b48 --- /dev/null +++ b/app/code/core/Mage/Captcha/Model/Zend.php @@ -0,0 +1,496 @@ + + */ +class Mage_Captcha_Model_Zend extends Zend_Captcha_Image implements Mage_Captcha_Model_Interface +{ + /** + * Key in session for captcha code + */ + const SESSION_WORD = 'word'; + + /** + * Min captcha lengths default value + */ + const DEFAULT_WORD_LENGTH_FROM = 3; + + /** + * Max captcha lengths default value + */ + const DEFAULT_WORD_LENGTH_TO = 5; + + /** + * Key in session for keeping captcha attempts + */ + const SESSION_FAILED_ATTEMPTS = 'failed_attempts'; + + /** + * Helper Instance + * @var Mage_Captcha_Helper_Data + */ + protected $_helper = null; + + /** + * Captcha expire time + * @var int + */ + protected $_expiration; + + /** + * Override default value to prevent a captcha cut off + * @var int + * @see Zend_Captcha_Image::$_fsize + */ + protected $_fsize = 22; + + /** + * Captcha form id + * @var string + */ + protected $_formId; + + /** + * Zend captcha constructor + * + * @param array $params + */ + public function __construct($params) + { + if (!isset($params['formId'])) { + throw new Exception('formId is mandatory'); + } + $this->_formId = $params['formId']; + $this->setExpiration($this->getTimeout()); + } + + /** + * Get Block Name + * + * @return string + */ + public function getBlockName() + { + return 'captcha/captcha_zend'; + } + + + /** + * Whether captcha is required to be inserted to this form + * + * @param null|string $login + * @return bool + */ + public function isRequired($login = null) + { + if ($this->_isUserAuth() || !$this->_isEnabled() || !in_array($this->_formId, $this->_getTargetForms())) { + return false; + } + + return ($this->_isShowAlways() || $this->_isOverLimitAttempts($login)); + } + + /** + * Check is overlimit attempts + * + * @param string $login + * @return bool + */ + protected function _isOverLimitAttempts($login) + { + return ($this->_isOverLimitSessionAttempt() || $this->_isOverLimitIpAttempt() + || $this->_isOverLimitLoginAttempts($login)); + } + + /** + * Check is overlimit saved in session attempts + * + * @return bool + */ + protected function _isOverLimitSessionAttempt() + { + $key = $this->_formId . '_' . self::SESSION_FAILED_ATTEMPTS; + return $this->getSession()->getData($key) >= $this->_getHelper()->getConfigNode('failed_attempts'); + } + + /** + * Check is overlimit saved attempts from one ip + * + * @return bool + */ + protected function _isOverLimitIpAttempt() + { + $countAttemptsByIp = Mage::getResourceModel('captcha/log')->countAttemptsByRemoteAddress(); + if ($countAttemptsByIp >= $this->_getHelper()->getConfigNode('failed_attempts')) { + return true; + } + return false; + } + + /** + * Is Over Limit Login Attempts + * + * @param string $login + * @return bool + */ + protected function _isOverLimitLoginAttempts($login) + { + if ($login != false) { + $countAttemptsByLogin = Mage::getResourceModel('captcha/log')->countAttemptsByUserLogin($login); + if ($countAttemptsByLogin >= $this->_getHelper()->getConfigNode('failed_attempts')) { + $this->getSession()->setData( + $this->_formId . '_' . self::SESSION_FAILED_ATTEMPTS, $countAttemptsByLogin + ); + return true; + } + } + return false; + } + + /** + * Check is user auth + * + * @return bool + */ + protected function _isUserAuth() + { + return Mage::app()->getStore()->isAdmin() + ? Mage::getSingleton('admin/session')->isLoggedIn() + : Mage::getSingleton('customer/session')->isLoggedIn(); + } + + /** + * Whether to respect case while checking the answer + * + * @return bool + */ + public function isCaseSensitive() + { + return (string)$this->_getHelper()->getConfigNode('case_sensitive'); + } + + /** + * Get font to use when generating captcha + * + * @return string + */ + public function getFont() + { + return $this->_getFontPath(); + } + + /** + * After this time isCorrect() is going to return FALSE even if word was guessed correctly + * + * @return int + */ + public function getTimeout() + { + if (!$this->_expiration) { + /** + * as "timeout" configuration parameter specifies timeout in minutes - we multiply it on 60 to set + * expiration in seconds + */ + $this->_expiration = (int)$this->_getHelper()->getConfigNode('timeout') * 60; + } + return $this->_expiration; + } + + /** + * Get captcha image directory + * + * @return string + */ + public function getImgDir() + { + return $this->_helper->getImgDir(); + } + + /** + * Get captcha image base URL + * + * @return string + */ + public function getImgUrl() + { + return $this->_helper->getImgUrl(); + } + + /** + * Checks whether captcha was guessed correctly by user + * + * @param string $word + * @return bool + */ + public function isCorrect($word) + { + $storedWord = $this->getWord(); + $this->_clearWord(); + + if (!$word || !$storedWord){ + return false; + } + + if (!$this->isCaseSensitive()) { + $storedWord = strtolower($storedWord); + $word = strtolower($word); + } + return $word == $storedWord; + } + + /** + * Returns session instance + * + * @return Mage_Captcha_Model_Session + */ + public function getSession() + { + return Mage::getSingleton('customer/session'); + } + + /** + * Return full URL to captcha image + * + * @return string + */ + public function getImgSrc() + { + return $this->getImgUrl() . $this->getId() . $this->getSuffix(); + } + + /** + * log Attempt + * + * @param string $login + * @return Mage_Captcha_Model_Zend + */ + public function logAttempt($login) + { + if ($this->_isEnabled() && in_array($this->_formId, $this->_getTargetForms())) { + $attemptCount = (int)$this->getSession()->getData($this->_formId . '_' . self::SESSION_FAILED_ATTEMPTS); + $attemptCount++; + $this->getSession()->setData($this->_formId . '_' . self::SESSION_FAILED_ATTEMPTS, $attemptCount); + Mage::getResourceModel('captcha/log')->logAttempt($login); + } + return $this; + } + + /** + * Returns path for the font file, chosen to generate captcha + * + * @return string + */ + protected function _getFontPath() + { + $font = (string)$this->_getHelper()->getConfigNode('font'); + $fonts = $this->_getHelper()->getFonts(); + + if (isset($fonts[$font])) { + $fontPath = $fonts[$font]['path']; + } else { + $fontData = array_shift($fonts); + $fontPath = $fontData['path']; + } + + return $fontPath; + } + + /** + * Returns captcha helper + * + * @return Mage_Captcha_Helper_Interface + */ + protected function _getHelper() + { + if (empty($this->_helper)) { + $this->_helper = Mage::helper('captcha'); + } + return $this->_helper; + } + + /** + * Generate word used for captcha render + * + * @return string + */ + protected function _generateWord() + { + $word = ''; + $symbols = $this->_getSymbols(); + $wordLen = $this->_getWordLen(); + for ($i = 0; $i < $wordLen; $i++) { + $word .= $symbols[array_rand($symbols)]; + } + return $word; + } + + /** + * Get symbols array to use for word generation + * + * @return array + */ + protected function _getSymbols() + { + return str_split((string)$this->_getHelper()->getConfigNode('symbols')); + } + + /** + * Returns length for generating captcha word. This value may be dynamic. + * + * @return int + */ + protected function _getWordLen() + { + $from = 0; + $to = 0; + $length = (string)$this->_getHelper()->getConfigNode('length'); + if (!is_numeric($length)) { + if (preg_match('/(\d+)-(\d+)/', $length, $matches)) { + $from = (int)$matches[1]; + $to = (int)$matches[2]; + } + } else { + $from = (int)$length; + $to = (int)$length; + } + + if (($to < $from) || ($from < 1) || ($to < 1)) { + $from = self::DEFAULT_WORD_LENGTH_FROM; + $to = self::DEFAULT_WORD_LENGTH_TO; + } + + return mt_rand($from, $to); + } + + /** + * Whether to show captcha for this form every time + * + * @return bool + */ + protected function _isShowAlways() + { + if ((string)$this->_getHelper()->getConfigNode('mode') == Mage_Captcha_Helper_Data::MODE_ALWAYS){ + return true; + } + + $alwaysFor = $this->_getHelper()->getConfigNode('always_for'); + foreach ($alwaysFor as $nodeFormId => $isAlwaysFor) { + if ($isAlwaysFor && $this->_formId == $nodeFormId) { + return true; + } + } + + return false; + } + + /** + * Whether captcha is enabled at this area + * + * @return bool + */ + protected function _isEnabled() + { + return (string)$this->_getHelper()->getConfigNode('enable'); + } + + /** + * Retrieve list of forms where captcha must be shown + * + * For frontend this list is based on current website + * + * @return array + */ + protected function _getTargetForms() + { + $formsString = (string) $this->_getHelper()->getConfigNode('forms'); + return explode(',', $formsString); + } + + /** + * Get captcha word + * + * @return string + */ + public function getWord() + { + $sessionData = $this->getSession()->getData($this->_formId . '_' . self::SESSION_WORD); + return time() < $sessionData['expires'] ? $sessionData['data'] : null; + } + + /** + * Set captcha word + * + * @param string $word + * @return Zend_Captcha_Word + */ + protected function _setWord($word) + { + $this->getSession()->setData($this->_formId . '_' . self::SESSION_WORD, + array('data' => $word, 'expires' => time() + $this->getTimeout()) + ); + $this->_word = $word; + return $this; + } + + /** + * Set captcha word + * + * @return Mage_Captcha_Model_Zend + */ + protected function _clearWord() + { + $this->getSession()->unsetData($this->_formId . '_' . self::SESSION_WORD); + $this->_word = null; + return $this; + } + + /** + * Override function to generate less curly captcha that will not cut off + * + * @see Zend_Captcha_Image::_randomSize() + * @return int + */ + protected function _randomSize() + { + return mt_rand(280, 300) / 100; + } + + /** + * Overlap of the parent method + * + * Now deleting old captcha images make crontab script + * @see Mage_Captcha_Model_Observer::deleteExpiredImages + */ + protected function _gc() + { + //do nothing + } +} diff --git a/app/code/core/Mage/Captcha/controllers/Adminhtml/RefreshController.php b/app/code/core/Mage/Captcha/controllers/Adminhtml/RefreshController.php new file mode 100755 index 0000000000..1184e1af0d --- /dev/null +++ b/app/code/core/Mage/Captcha/controllers/Adminhtml/RefreshController.php @@ -0,0 +1,50 @@ + + */ +class Mage_Captcha_Adminhtml_RefreshController extends Mage_Adminhtml_Controller_Action +{ + /** + * Refreshes captcha and returns JSON encoded URL to image (AJAX action) + * Example: {'imgSrc': 'http://example.com/media/captcha/67842gh187612ngf8s.png'} + * + * @return null + */ + public function refreshAction() + { + $formId = $this->getRequest()->getPost('formId'); + $captchaModel = Mage::helper('captcha')->getCaptcha($formId); + $this->getLayout()->createBlock($captchaModel->getBlockName())->setFormId($formId)->setIsAjax(true)->toHtml(); + $this->getResponse()->setBody(json_encode(array('imgSrc' => $captchaModel->getImgSrc()))); + $this->setFlag('', self::FLAG_NO_POST_DISPATCH, true); + } +} diff --git a/app/code/core/Mage/Captcha/controllers/RefreshController.php b/app/code/core/Mage/Captcha/controllers/RefreshController.php new file mode 100755 index 0000000000..476c80baa9 --- /dev/null +++ b/app/code/core/Mage/Captcha/controllers/RefreshController.php @@ -0,0 +1,50 @@ + + */ +class Mage_Captcha_RefreshController extends Mage_Core_Controller_Front_Action +{ + /** + * Refreshes captcha and returns JSON encoded URL to image (AJAX action) + * Example: {'imgSrc': 'http://example.com/media/captcha/67842gh187612ngf8s.png'} + * + * @return null + */ + public function indexAction() + { + $formId = $this->getRequest()->getPost('formId'); + $captchaModel = Mage::helper('captcha')->getCaptcha($formId); + $this->getLayout()->createBlock($captchaModel->getBlockName())->setFormId($formId)->setIsAjax(true)->toHtml(); + $this->getResponse()->setBody(json_encode(array('imgSrc' => $captchaModel->getImgSrc()))); + $this->setFlag('', self::FLAG_NO_POST_DISPATCH, true); + } +} diff --git a/app/code/core/Mage/Captcha/etc/config.xml b/app/code/core/Mage/Captcha/etc/config.xml new file mode 100755 index 0000000000..9151e538f5 --- /dev/null +++ b/app/code/core/Mage/Captcha/etc/config.xml @@ -0,0 +1,262 @@ + + + + + + 1.7.0.0.0 + + + + + + + Mage_Captcha + + + + + + Mage_Captcha_Model + captcha_resource + + + Mage_Captcha_Model_Resource + + +
captcha_log
+ + + + + + + + + captcha/observer + checkUserLogin + + + + + + + captcha/observer + checkForgotPassword + + + + + + + captcha/observer + createUser + + + + + + + captcha/observer + checkUserForgotPasswordBackend + + + + + + + captcha/observer + checkUserLoginBackend + + + + + + + captcha/observer + checkGuestCheckout + + + captcha/observer + checkRegisterCheckout + + + + + + + captcha/observer + resetAttemptForFrontend + + + + + + + captcha/observer + resetAttemptForBackend + + + + + + + + + standard + + Mage_Captcha + captcha + + + + + + + captcha.xml + + + + + + + + + + Mage_Captcha_Adminhtml + + + + + + + + + + captcha.xml + + + + + + + + zend + 0 + linlibertine + after_fail + backend_forgotpassword + 3 + 7 + 4-5 + ABCDEFGHJKMnpqrstuvwxyz23456789 + 0 + + 1 + + + + + + zend + 0 + linlibertine + after_fail + user_forgotpassword + 3 + 7 + 4-5 + ABCDEFGHJKMnpqrstuvwxyz23456789 + 0 + + 1 + 1 + 1 + 1 + + + + + + + + lib/LinLibertineFont/LinLibertine_Bd-2.8.1.ttf + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + */30 * * * * + + + captcha/observer::deleteOldAttempts + + + + + */10 * * * * + + + captcha/observer::deleteExpiredImages + + + + + + diff --git a/app/code/core/Mage/Captcha/etc/system.xml b/app/code/core/Mage/Captcha/etc/system.xml new file mode 100755 index 0000000000..b0e5851655 --- /dev/null +++ b/app/code/core/Mage/Captcha/etc/system.xml @@ -0,0 +1,250 @@ + + + + + + + + + text + 50 + 1 + 1 + 0 + + + + select + adminhtml/system_config_source_yesno + 1 + 1 + 0 + 0 + + + + select + captcha/config_font + 2 + 1 + 0 + 0 + 1 + + + + multiselect + captcha/config_form_backend + 3 + 1 + 0 + 0 + 1 + + + + select + captcha/config_mode + 4 + 1 + 0 + 0 + 1 + + + + text + If 0 is specified, CAPTCHA on the Login form will be always available. + 5 + 1 + 0 + 0 + + after_fail + 1 + + required-entry validate-digits + + + + text + 6 + 1 + 0 + 0 + 1 + required-entry validate-digits + + + + text + Please specify 8 symbols at the most. Range allowed (e.g. 3-5) + 7 + 1 + 0 + 0 + 1 + required-entry + + + + text + Similar looking characters (e.g. "i", "l", "1") decrease chance of correct recognition by customer.]]> + 8 + 1 + 0 + 0 + 1 + required-entry validate-alphanum + + + + select + adminhtml/system_config_source_yesno + 9 + 1 + 0 + 0 + 1 + + + + + + + + + + text + 110 + 1 + 1 + 0 + + + + select + adminhtml/system_config_source_yesno + 1 + 1 + 1 + 0 + + + + select + captcha/config_font + 2 + 1 + 1 + 0 + 1 + + + + multiselect + captcha/config_form_frontend + CAPTCHA for "Create user" and "Forgot password" forms is always enabled if chosen + 3 + 1 + 1 + 0 + 1 + + + + select + captcha/config_mode + 4 + 1 + 1 + 0 + 1 + + + + text + If 0 is specified, CAPTCHA on the Login form will be always available. + 5 + 1 + 1 + 0 + + 1 + after_fail + + required-entry validate-digits + + + + text + 6 + 1 + 1 + 0 + 1 + required-entry validate-digits + + + + text + Please specify 8 symbols at the most. Range allowed (e.g. 3-5) + 7 + 1 + 1 + 0 + 1 + required-entry + + + + text + Similar looking characters (e.g. "i", "l", "1") decrease chance of correct recognition by customer.]]> + 8 + 1 + 1 + 0 + 1 + required-entry validate-alphanum + + + + select + adminhtml/system_config_source_yesno + 9 + 1 + 1 + 0 + 1 + + + + + + + diff --git a/app/code/core/Mage/Captcha/sql/captcha_setup/install-1.7.0.0.0.php b/app/code/core/Mage/Captcha/sql/captcha_setup/install-1.7.0.0.0.php new file mode 100644 index 0000000000..966630c917 --- /dev/null +++ b/app/code/core/Mage/Captcha/sql/captcha_setup/install-1.7.0.0.0.php @@ -0,0 +1,51 @@ +startSetup(); + +$table = $installer->getConnection() + ->newTable($installer->getTable('captcha/log')) + ->addColumn('type', Varien_Db_Ddl_Table::TYPE_TEXT, 32, array( + 'nullable' => false, + 'primary' => true, + ), 'Type') + ->addColumn('value', Varien_Db_Ddl_Table::TYPE_TEXT, 32, array( + 'nullable' => false, + 'unsigned' => true, + 'primary' => true, + ), 'Value') + ->addColumn('count', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array( + 'unsigned' => true, + 'nullable' => false, + 'default' => '0', + ), 'Count') + ->addColumn('updated_at', Varien_Db_Ddl_Table::TYPE_TIMESTAMP, null, array(), 'Update Time') + ->setComment('Count Login Attempts'); +$installer->getConnection()->createTable($table); + +$installer->endSetup(); diff --git a/app/code/core/Mage/Catalog/Block/Product/Abstract.php b/app/code/core/Mage/Catalog/Block/Product/Abstract.php index e573a5163e..edecb0d2c2 100644 --- a/app/code/core/Mage/Catalog/Block/Product/Abstract.php +++ b/app/code/core/Mage/Catalog/Block/Product/Abstract.php @@ -344,12 +344,19 @@ public function getTierPrices($product = null) $res = array(); if (is_array($prices)) { foreach ($prices as $price) { - $price['price_qty'] = $price['price_qty']*1; - if ($product->getPrice() != $product->getFinalPrice()) { + $price['price_qty'] = $price['price_qty'] * 1; + + $_productPrice = $product->getPrice(); + if ($_productPrice != $product->getFinalPrice()) { $_productPrice = $product->getFinalPrice(); - } else { - $_productPrice = $product->getPrice(); } + + // Group price must be used for percent calculation if it is lower + $groupPrice = $product->getGroupPrice(); + if ($_productPrice > $groupPrice) { + $_productPrice = $groupPrice; + } + if ($price['price'] < $_productPrice) { $price['savePercent'] = ceil(100 - ((100 / $_productPrice) * $price['price'])); diff --git a/app/code/core/Mage/Catalog/Block/Product/New.php b/app/code/core/Mage/Catalog/Block/Product/New.php index 007386fef3..98503c8b4a 100644 --- a/app/code/core/Mage/Catalog/Block/Product/New.php +++ b/app/code/core/Mage/Catalog/Block/Product/New.php @@ -81,19 +81,26 @@ public function getCacheKeyInfo() */ protected function _beforeToHtml() { - $todayDate = Mage::app()->getLocale()->date()->toString(Varien_Date::DATETIME_INTERNAL_FORMAT); + $todayStartOfDayDate = Mage::app()->getLocale()->date() + ->setTime('00:00:00') + ->toString(Varien_Date::DATETIME_INTERNAL_FORMAT); + + $todayEndOfDayDate = Mage::app()->getLocale()->date() + ->setTime('23:59:59') + ->toString(Varien_Date::DATETIME_INTERNAL_FORMAT); $collection = Mage::getResourceModel('catalog/product_collection'); $collection->setVisibility(Mage::getSingleton('catalog/product_visibility')->getVisibleInCatalogIds()); + $collection = $this->_addProductAttributesAndPrices($collection) ->addStoreFilter() ->addAttributeToFilter('news_from_date', array('or'=> array( - 0 => array('date' => true, 'to' => $todayDate), + 0 => array('date' => true, 'to' => $todayEndOfDayDate), 1 => array('is' => new Zend_Db_Expr('null'))) ), 'left') ->addAttributeToFilter('news_to_date', array('or'=> array( - 0 => array('date' => true, 'from' => $todayDate), + 0 => array('date' => true, 'from' => $todayStartOfDayDate), 1 => array('is' => new Zend_Db_Expr('null'))) ), 'left') ->addAttributeToFilter( diff --git a/app/code/core/Mage/Catalog/Block/Product/Price.php b/app/code/core/Mage/Catalog/Block/Product/Price.php index 67b9d91e88..44521c711a 100644 --- a/app/code/core/Mage/Catalog/Block/Product/Price.php +++ b/app/code/core/Mage/Catalog/Block/Product/Price.php @@ -72,21 +72,26 @@ public function getTierPrices($product = null) if (is_null($product)) { $product = $this->getProduct(); } - $prices = $product->getFormatedTierPrice(); + $prices = $product->getFormatedTierPrice(); $res = array(); if (is_array($prices)) { foreach ($prices as $price) { - $price['price_qty'] = $price['price_qty']*1; + $price['price_qty'] = $price['price_qty'] * 1; + $productPrice = $product->getPrice(); if ($product->getPrice() != $product->getFinalPrice()) { $productPrice = $product->getFinalPrice(); - } else { - $productPrice = $product->getPrice(); } - if ($price['price']<$productPrice) { - $price['savePercent'] = ceil(100 - (( 100/$productPrice ) * $price['price'] )); + // Group price must be used for percent calculation if it is lower + $groupPrice = $product->getGroupPrice(); + if ($productPrice > $groupPrice) { + $productPrice = $groupPrice; + } + + if ($price['price'] < $productPrice) { + $price['savePercent'] = ceil(100 - ((100 / $productPrice) * $price['price'])); $tierPrice = Mage::app()->getStore()->convertPrice( Mage::helper('tax')->getPrice($product, $price['website_price']) diff --git a/app/code/core/Mage/Catalog/Block/Product/View.php b/app/code/core/Mage/Catalog/Block/Product/View.php index b83bece1db..337f43224c 100644 --- a/app/code/core/Mage/Catalog/Block/Product/View.php +++ b/app/code/core/Mage/Catalog/Block/Product/View.php @@ -28,9 +28,10 @@ /** * Product View block * - * @category Mage - * @package Mage_Catalog - * @module Catalog + * @category Mage + * @package Mage_Catalog + * @module Catalog + * @author Magento Core Team */ class Mage_Catalog_Block_Product_View extends Mage_Catalog_Block_Product_Abstract { @@ -121,14 +122,14 @@ public function getAddToCartUrl($product, $additional = array()) } $addUrlKey = Mage_Core_Controller_Front_Action::PARAM_NAME_URL_ENCODED; - $addUrlValue = Mage::getUrl('*/*/*', array('_use_rewrite' => true, '_current' => false)); + $addUrlValue = Mage::getUrl('*/*/*', array('_use_rewrite' => true, '_current' => true)); $additional[$addUrlKey] = Mage::helper('core')->urlEncode($addUrlValue); return $this->helper('checkout/cart')->getAddUrl($product, $additional); } /** - * Get JSON encripted configuration array which can be used for JS dynamic + * Get JSON encoded configuration array which can be used for JS dynamic * price calculation depending on product options * * @return string @@ -141,20 +142,29 @@ public function getJsonConfig() } $_request = Mage::getSingleton('tax/calculation')->getRateRequest(false, false, false); - $_request->setProductClassId($this->getProduct()->getTaxClassId()); + /* @var $product Mage_Catalog_Model_Product */ + $product = $this->getProduct(); + $_request->setProductClassId($product->getTaxClassId()); $defaultTax = Mage::getSingleton('tax/calculation')->getRate($_request); $_request = Mage::getSingleton('tax/calculation')->getRateRequest(); - $_request->setProductClassId($this->getProduct()->getTaxClassId()); + $_request->setProductClassId($product->getTaxClassId()); $currentTax = Mage::getSingleton('tax/calculation')->getRate($_request); - $_regularPrice = $this->getProduct()->getPrice(); - $_finalPrice = $this->getProduct()->getFinalPrice(); - $_priceInclTax = Mage::helper('tax')->getPrice($this->getProduct(), $_finalPrice, true); - $_priceExclTax = Mage::helper('tax')->getPrice($this->getProduct(), $_finalPrice); - + $_regularPrice = $product->getPrice(); + $_finalPrice = $product->getFinalPrice(); + $_priceInclTax = Mage::helper('tax')->getPrice($product, $_finalPrice, true); + $_priceExclTax = Mage::helper('tax')->getPrice($product, $_finalPrice); + $_tierPrices = array(); + $_tierPricesInclTax = array(); + foreach ($product->getTierPrice() as $tierPrice) { + $_tierPrices[] = Mage::helper('core')->currency($tierPrice['website_price'], false, false); + $_tierPricesInclTax[] = Mage::helper('core')->currency( + Mage::helper('tax')->getPrice($product, (int)$tierPrice['website_price'], true), + false, false); + } $config = array( - 'productId' => $this->getProduct()->getId(), + 'productId' => $product->getId(), 'priceFormat' => Mage::app()->getLocale()->getJsPriceFormat(), 'includeTax' => Mage::helper('tax')->priceIncludesTax() ? 'true' : 'false', 'showIncludeTax' => Mage::helper('tax')->displayPriceIncludingTax(), @@ -175,6 +185,8 @@ public function getJsonConfig() 'plusDisposition' => 0, 'oldMinusDisposition' => 0, 'minusDisposition' => 0, + 'tierPrices' => $_tierPrices, + 'tierPricesInclTax' => $_tierPricesInclTax, ); $responseObject = new Varien_Object(); @@ -228,8 +240,7 @@ public function isStartCustomization() * Get default qty - either as preconfigured, or as 1. * Also restricts it by minimal qty. * - * @param null|Mage_Catalog_Model_Product - * + * @param null|Mage_Catalog_Model_Product $product * @return int|float */ public function getProductDefaultQty($product = null) diff --git a/app/code/core/Mage/Catalog/Block/Product/View/Media.php b/app/code/core/Mage/Catalog/Block/Product/View/Media.php index 71e3ca0963..93456f5ee3 100644 --- a/app/code/core/Mage/Catalog/Block/Product/View/Media.php +++ b/app/code/core/Mage/Catalog/Block/Product/View/Media.php @@ -29,12 +29,22 @@ * * @category Mage * @package Mage_Catalog - * @author Magento Core Team + * @author Magento Core Team */ class Mage_Catalog_Block_Product_View_Media extends Mage_Catalog_Block_Product_View_Abstract { + /** + * Flag, that defines whether gallery is disabled + * + * @var boolean + */ protected $_isGalleryDisabled; + /** + * Retrieve list of gallery images + * + * @return array|Varien_Data_Collection + */ public function getGalleryImages() { if ($this->_isGalleryDisabled) { @@ -44,16 +54,24 @@ public function getGalleryImages() return $collection; } - public function getGalleryUrl($image=null) + /** + * Retrieve gallery url + * + * @param null|Varien_Object $image + * @return string + */ + public function getGalleryUrl($image = null) { - $params = array('id'=>$this->getProduct()->getId()); + $params = array('id' => $this->getProduct()->getId()); if ($image) { $params['image'] = $image->getValueId(); - return $this->getUrl('*/*/gallery', $params); } - return $this->getUrl('*/*/gallery', $params); + return $this->getUrl('catalog/product/gallery', $params); } + /** + * Disable gallery + */ public function disableGallery() { $this->_isGalleryDisabled = true; diff --git a/app/code/core/Mage/Catalog/Block/Product/View/Options.php b/app/code/core/Mage/Catalog/Block/Product/View/Options.php index e593ef85f2..29b32cf71b 100644 --- a/app/code/core/Mage/Catalog/Block/Product/View/Options.php +++ b/app/code/core/Mage/Catalog/Block/Product/View/Options.php @@ -146,7 +146,11 @@ public function getJsonConfig() $_tmpPriceValues = array(); foreach ($option->getValues() as $value) { /* @var $value Mage_Catalog_Model_Product_Option_Value */ - $_tmpPriceValues[$value->getId()] = Mage::helper('core')->currency($value->getPrice(true), false, false); + $id = $value->getId(); + $_tmpPriceValues[$id]['price'] = Mage::helper('core')->currency($value->getPrice(true), false, + false); + $_tmpPriceValues[$id]['oldPrice'] = Mage::helper('core')->currency($value->getPrice(false), false, + false); } $priceValue = $_tmpPriceValues; } else { diff --git a/app/code/core/Mage/Catalog/Helper/Image.php b/app/code/core/Mage/Catalog/Helper/Image.php index f7ec3693b7..345da6abd3 100644 --- a/app/code/core/Mage/Catalog/Helper/Image.php +++ b/app/code/core/Mage/Catalog/Helper/Image.php @@ -31,22 +31,87 @@ */ class Mage_Catalog_Helper_Image extends Mage_Core_Helper_Abstract { + /** + * Current model + * + * @var Mage_Catalog_Model_Product_Image + */ protected $_model; + + /** + * Scheduled for resize image + * + * @var bool + */ protected $_scheduleResize = false; + + /** + * Scheduled for rotate image + * + * @var bool + */ protected $_scheduleRotate = false; + + /** + * Angle + * + * @var int + */ protected $_angle; + /** + * Watermark file name + * + * @var string + */ protected $_watermark; + + /** + * Watermark Position + * + * @var string + */ protected $_watermarkPosition; + + /** + * Watermark Size + * + * @var string + */ protected $_watermarkSize; + + /** + * Watermark Image opacity + * + * @var int + */ protected $_watermarkImageOpacity; + /** + * Current Product + * + * @var Mage_Catalog_Model_Product + */ protected $_product; + + /** + * Image File + * + * @var string + */ protected $_imageFile; + + /** + * Image Placeholder + * + * @var string + */ protected $_placeholder; /** - * Reset all previos data + * Reset all previous data + * + * @return Mage_Catalog_Helper_Image */ protected function _reset() { @@ -63,6 +128,14 @@ protected function _reset() return $this; } + /** + * Initialize Helper to work with Image + * + * @param Mage_Catalog_Model_Product $product + * @param string $attributeName + * @param mixed $imageFile + * @return Mage_Catalog_Helper_Image + */ public function init(Mage_Catalog_Model_Product $product, $attributeName, $imageFile=null) { $this->_reset(); @@ -70,17 +143,24 @@ public function init(Mage_Catalog_Model_Product $product, $attributeName, $image $this->_getModel()->setDestinationSubdir($attributeName); $this->setProduct($product); - $this->setWatermark(Mage::getStoreConfig("design/watermark/{$this->_getModel()->getDestinationSubdir()}_image")); - $this->setWatermarkImageOpacity(Mage::getStoreConfig("design/watermark/{$this->_getModel()->getDestinationSubdir()}_imageOpacity")); - $this->setWatermarkPosition(Mage::getStoreConfig("design/watermark/{$this->_getModel()->getDestinationSubdir()}_position")); - $this->setWatermarkSize(Mage::getStoreConfig("design/watermark/{$this->_getModel()->getDestinationSubdir()}_size")); + $this->setWatermark( + Mage::getStoreConfig("design/watermark/{$this->_getModel()->getDestinationSubdir()}_image") + ); + $this->setWatermarkImageOpacity( + Mage::getStoreConfig("design/watermark/{$this->_getModel()->getDestinationSubdir()}_imageOpacity") + ); + $this->setWatermarkPosition( + Mage::getStoreConfig("design/watermark/{$this->_getModel()->getDestinationSubdir()}_position") + ); + $this->setWatermarkSize( + Mage::getStoreConfig("design/watermark/{$this->_getModel()->getDestinationSubdir()}_size") + ); if ($imageFile) { $this->setImageFile($imageFile); - } - else { + } else { // add for work original size - $this->_getModel()->setBaseFile( $this->getProduct()->getData($this->_getModel()->getDestinationSubdir()) ); + $this->_getModel()->setBaseFile($this->getProduct()->getData($this->_getModel()->getDestinationSubdir())); } return $this; } @@ -198,6 +278,12 @@ public function backgroundColor($colorRGB) return $this; } + /** + * Rotate image into specified angle + * + * @param int $angle + * @return Mage_Catalog_Helper_Image + */ public function rotate($angle) { $this->setAngle($angle); @@ -225,11 +311,22 @@ public function watermark($fileName, $position, $size=null, $imageOpacity=null) return $this; } + /** + * Set placeholder + * + * @param string $fileName + * @return void + */ public function placeholder($fileName) { $this->_placeholder = $fileName; } + /** + * Get Placeholder + * + * @return string + */ public function getPlaceholder() { if (!$this->_placeholder) { @@ -239,41 +336,49 @@ public function getPlaceholder() return $this->_placeholder; } + /** + * Return Image URL + * + * @return string + */ public function __toString() { try { - if( $this->getImageFile() ) { - $this->_getModel()->setBaseFile( $this->getImageFile() ); + $model = $this->_getModel(); + + if ($this->getImageFile()) { + $model->setBaseFile($this->getImageFile()); } else { - $this->_getModel()->setBaseFile( $this->getProduct()->getData($this->_getModel()->getDestinationSubdir()) ); + $model->setBaseFile($this->getProduct()->getData($model->getDestinationSubdir())); } - if( $this->_getModel()->isCached() ) { - return $this->_getModel()->getUrl(); + if ($model->isCached()) { + return $model->getUrl(); } else { - if( $this->_scheduleRotate ) { - $this->_getModel()->rotate( $this->getAngle() ); + if ($this->_scheduleRotate) { + $model->rotate($this->getAngle()); } if ($this->_scheduleResize) { - $this->_getModel()->resize(); + $model->resize(); } - if( $this->getWatermark() ) { - $this->_getModel()->setWatermark($this->getWatermark()); + if ($this->getWatermark()) { + $model->setWatermark($this->getWatermark()); } - $url = $this->_getModel()->saveFile()->getUrl(); + $url = $model->saveFile()->getUrl(); } - } catch( Exception $e ) { + } catch (Exception $e) { $url = Mage::getDesign()->getSkinUrl($this->getPlaceholder()); } return $url; } /** - * Enter description here... + * Set current Image model * + * @param Mage_Catalog_Model_Product_Image $model * @return Mage_Catalog_Helper_Image */ protected function _setModel($model) @@ -283,7 +388,7 @@ protected function _setModel($model) } /** - * Enter description here... + * Get current Image model * * @return Mage_Catalog_Model_Product_Image */ @@ -292,12 +397,23 @@ protected function _getModel() return $this->_model; } + /** + * Set Rotation Angle + * + * @param int $angle + * @return Mage_Catalog_Helper_Image + */ protected function setAngle($angle) { $this->_angle = $angle; return $this; } + /** + * Get Rotation Angle + * + * @return int + */ protected function getAngle() { return $this->_angle; @@ -393,44 +509,67 @@ public function setWatermarkImageOpacity($imageOpacity) */ protected function getWatermarkImageOpacity() { - if( $this->_watermarkImageOpacity ) { + if ($this->_watermarkImageOpacity) { return $this->_watermarkImageOpacity; } return $this->_getModel()->getWatermarkImageOpacity(); } + /** + * Set current Product + * + * @param Mage_Catalog_Model_Product $product + * @return Mage_Catalog_Helper_Image + */ protected function setProduct($product) { $this->_product = $product; return $this; } + /** + * Get current Product + * + * @return Mage_Catalog_Model_Product + */ protected function getProduct() { return $this->_product; } + /** + * Set Image file + * + * @param string $file + * @return Mage_Catalog_Helper_Image + */ protected function setImageFile($file) { $this->_imageFile = $file; return $this; } + /** + * Get Image file + * + * @return string + */ protected function getImageFile() { return $this->_imageFile; } /** - * Enter description here... + * Retrieve size from string * - * @return array + * @param string $string + * @return array|bool */ protected function parseSize($string) { $size = explode('x', strtolower($string)); - if( sizeof($size) == 2 ) { + if (sizeof($size) == 2) { return array( 'width' => ($size[0] > 0) ? $size[0] : null, 'heigth' => ($size[1] > 0) ? $size[1] : null, @@ -485,17 +624,19 @@ public function getOriginalSizeArray() } /** - * Check - is this file an image + * Check - is this file an image * * @param string $filePath * @return bool - * @throw Mage_Core_Exception + * @throws Mage_Core_Exception */ public function validateUploadFile($filePath) { if (!getimagesize($filePath)) { Mage::throwException($this->__('Disallowed file type.')); } - return true; + + $_processor = new Varien_Image($filePath); + return $_processor->getMimeType() !== null; } } diff --git a/app/code/core/Mage/Catalog/Helper/Product.php b/app/code/core/Mage/Catalog/Helper/Product.php index bf9b7c26c1..14130e20b1 100644 --- a/app/code/core/Mage/Catalog/Helper/Product.php +++ b/app/code/core/Mage/Catalog/Helper/Product.php @@ -407,35 +407,30 @@ public function addParamsToBuyRequest($buyRequest, $params) * @param string $identifierType * @return Mage_Catalog_Model_Product */ - public function getProduct($productId, $store, $identifierType = null) { - $loadByIdOnFalse = false; - if ($identifierType == null) { + public function getProduct($productId, $store, $identifierType = null) + { + /** @var $product Mage_Catalog_Model_Product */ + $product = Mage::getModel('catalog/product')->setStoreId(Mage::app()->getStore($store)->getId()); + + $expectedIdType = false; + if ($identifierType === null) { if (is_string($productId) && !preg_match("/^[+-]?[1-9][0-9]*$|^0$/", $productId)) { - $identifierType = 'sku'; - $loadByIdOnFalse = true; - } else { - $identifierType = 'id'; + $expectedIdType = 'sku'; } } - /** @var $product Mage_Catalog_Model_Product */ - $product = Mage::getModel('catalog/product'); - if ($store !== null) { - $product->setStoreId($store); - } - if ($identifierType == 'sku') { + if ($identifierType == 'sku' || $expectedIdType == 'sku') { $idBySku = $product->getIdBySku($productId); if ($idBySku) { $productId = $idBySku; - } - if ($loadByIdOnFalse) { - $identifierType = 'id'; + } else if ($identifierType == 'sku') { + // Return empty product because it was not found by originally specified SKU identifier + return $product; } } - if ($identifierType == 'id' && is_numeric($productId)) { - $productId = !is_float($productId) ? (int) $productId : 0; - $product->load($productId); + if ($productId && is_numeric($productId)) { + $product->load((int) $productId); } return $product; diff --git a/app/code/core/Mage/Catalog/Model/Category/Api.php b/app/code/core/Mage/Catalog/Model/Category/Api.php index 3b51f812f4..1aaddbbb35 100644 --- a/app/code/core/Mage/Catalog/Model/Category/Api.php +++ b/app/code/core/Mage/Catalog/Model/Category/Api.php @@ -444,7 +444,7 @@ public function assignedProducts($categoryId, $store = null) 'type' => $product->getTypeId(), 'set' => $product->getAttributeSetId(), 'sku' => $product->getSku(), - 'position' => $product->getPosition() + 'position' => $product->getCatIndexPosition() ); } diff --git a/app/code/core/Mage/Catalog/Model/Category/Indexer/Flat.php b/app/code/core/Mage/Catalog/Model/Category/Indexer/Flat.php index 23e11b754c..9a3e140caa 100644 --- a/app/code/core/Mage/Catalog/Model/Category/Indexer/Flat.php +++ b/app/code/core/Mage/Catalog/Model/Category/Indexer/Flat.php @@ -34,6 +34,11 @@ */ class Mage_Catalog_Model_Category_Indexer_Flat extends Mage_Index_Model_Indexer_Abstract { + /** + * Data key for matching result to be saved in + */ + const EVENT_MATCH_RESULT_KEY = 'catalog_category_flat_match_result'; + /** * Matched entity events * @@ -96,21 +101,22 @@ public function matchEvent(Mage_Index_Model_Event $event) return false; } - $data = $event->getNewData(); - $resultKey = 'catalog_category_flat_match_result'; - if (isset($data[$resultKey])) { - return $data[$resultKey]; + $data = $event->getNewData(); + if (isset($data[self::EVENT_MATCH_RESULT_KEY])) { + return $data[self::EVENT_MATCH_RESULT_KEY]; } - $result = null; $entity = $event->getEntity(); if ($entity == Mage_Core_Model_Store::ENTITY) { if ($event->getType() == Mage_Index_Model_Event::TYPE_DELETE) { $result = true; - } else if ($event->getType() == Mage_Index_Model_Event::TYPE_SAVE) { - /* @var $store Mage_Core_Model_Store */ + } elseif ($event->getType() == Mage_Index_Model_Event::TYPE_SAVE) { + /** @var $store Mage_Core_Model_Store */ $store = $event->getDataObject(); - if ($store->isObjectNew() || $store->dataHasChangedFor('group_id') || $store->dataHasChangedFor('root_catefory_id')) { + if ($store && ($store->isObjectNew() + || $store->dataHasChangedFor('group_id') + || $store->dataHasChangedFor('root_category_id') + )) { $result = true; } else { $result = false; @@ -118,10 +124,12 @@ public function matchEvent(Mage_Index_Model_Event $event) } else { $result = false; } - } else if ($entity == Mage_Core_Model_Store_Group::ENTITY) { - /* @var $storeGroup Mage_Core_Model_Store_Group */ + } elseif ($entity == Mage_Core_Model_Store_Group::ENTITY) { + /** @var $storeGroup Mage_Core_Model_Store_Group */ $storeGroup = $event->getDataObject(); - if ($storeGroup->dataHasChangedFor('website_id')) { + if ($storeGroup + && ($storeGroup->dataHasChangedFor('website_id') || $storeGroup->dataHasChangedFor('root_category_id')) + ) { $result = true; } else { $result = false; @@ -130,7 +138,7 @@ public function matchEvent(Mage_Index_Model_Event $event) $result = parent::matchEvent($event); } - $event->addNewData($resultKey, $result); + $event->addNewData(self::EVENT_MATCH_RESULT_KEY, $result); return $result; } @@ -142,6 +150,7 @@ public function matchEvent(Mage_Index_Model_Event $event) */ protected function _registerEvent(Mage_Index_Model_Event $event) { + $event->addNewData(self::EVENT_MATCH_RESULT_KEY, true); switch ($event->getEntity()) { case Mage_Catalog_Model_Category::ENTITY: $this->_registerCatalogCategoryEvent($event); @@ -234,6 +243,6 @@ protected function _processEvent(Mage_Index_Model_Event $event) */ public function reindexAll() { - $this->_getIndexer()->rebuild(); + $this->_getIndexer()->reindexAll(); } } diff --git a/app/code/core/Mage/Catalog/Model/Category/Indexer/Product.php b/app/code/core/Mage/Catalog/Model/Category/Indexer/Product.php index 36b560d7b3..1c84e4541a 100644 --- a/app/code/core/Mage/Catalog/Model/Category/Indexer/Product.php +++ b/app/code/core/Mage/Catalog/Model/Category/Indexer/Product.php @@ -53,6 +53,11 @@ */ class Mage_Catalog_Model_Category_Indexer_Product extends Mage_Index_Model_Indexer_Abstract { + /** + * Data key for matching result to be saved in + */ + const EVENT_MATCH_RESULT_KEY = 'catalog_category_product_match_result'; + /** * @var array */ @@ -113,25 +118,23 @@ public function getDescription() public function matchEvent(Mage_Index_Model_Event $event) { $data = $event->getNewData(); - $resultKey = 'catalog_category_product_match_result'; - if (isset($data[$resultKey])) { - return $data[$resultKey]; + if (isset($data[self::EVENT_MATCH_RESULT_KEY])) { + return $data[self::EVENT_MATCH_RESULT_KEY]; } - $result = null; $entity = $event->getEntity(); if ($entity == Mage_Core_Model_Store::ENTITY) { $store = $event->getDataObject(); - if ($store->isObjectNew() || $store->dataHasChangedFor('group_id')) { + if ($store && ($store->isObjectNew() || $store->dataHasChangedFor('group_id'))) { $result = true; } else { $result = false; } } elseif ($entity == Mage_Core_Model_Store_Group::ENTITY) { $storeGroup = $event->getDataObject(); - $hasDataChanges = $storeGroup->dataHasChangedFor('root_category_id') - || $storeGroup->dataHasChangedFor('website_id'); - if (!$storeGroup->isObjectNew() && $hasDataChanges) { + $hasDataChanges = $storeGroup && ($storeGroup->dataHasChangedFor('root_category_id') + || $storeGroup->dataHasChangedFor('website_id')); + if ($storeGroup && !$storeGroup->isObjectNew() && $hasDataChanges) { $result = true; } else { $result = false; @@ -140,7 +143,7 @@ public function matchEvent(Mage_Index_Model_Event $event) $result = parent::matchEvent($event); } - $event->addNewData($resultKey, $result); + $event->addNewData(self::EVENT_MATCH_RESULT_KEY, $result); return $result; } @@ -154,6 +157,7 @@ public function matchEvent(Mage_Index_Model_Event $event) */ protected function _registerEvent(Mage_Index_Model_Event $event) { + $event->addNewData(self::EVENT_MATCH_RESULT_KEY, true); $entity = $event->getEntity(); switch ($entity) { case Mage_Catalog_Model_Product::ENTITY: diff --git a/app/code/core/Mage/Catalog/Model/Convert/Adapter/Product.php b/app/code/core/Mage/Catalog/Model/Convert/Adapter/Product.php index 4d55942957..2af60b468d 100644 --- a/app/code/core/Mage/Catalog/Model/Convert/Adapter/Product.php +++ b/app/code/core/Mage/Catalog/Model/Convert/Adapter/Product.php @@ -563,10 +563,7 @@ public function save() } catch (Exception $e) { if (!$e instanceof Mage_Dataflow_Model_Convert_Exception) { $this->addException( - Mage::helper('catalog')->__( - 'An error occurred while saving the collection, aborting. Error message: %s', - $e->getMessage() - ), + Mage::helper('catalog')->__('An error occurred while saving the collection, aborting. Error message: %s', $e->getMessage()), Mage_Dataflow_Model_Convert_Exception::FATAL ); } @@ -593,10 +590,7 @@ public function saveRow(array $importData) if (!is_null($this->getBatchParams('store'))) { $store = $this->getStoreById($this->getBatchParams('store')); } else { - $message = Mage::helper('catalog')->__( - 'Skipping import row, required field "%s" is not defined.', - 'store' - ); + $message = Mage::helper('catalog')->__('Skipping import row, required field "%s" is not defined.', 'store'); Mage::throwException($message); } } else { @@ -604,10 +598,7 @@ public function saveRow(array $importData) } if ($store === false) { - $message = Mage::helper('catalog')->__( - 'Skipping import row, store "%s" field does not exist.', - $importData['store'] - ); + $message = Mage::helper('catalog')->__('Skipping import row, store "%s" field does not exist.', $importData['store']); Mage::throwException($message); } @@ -629,11 +620,7 @@ public function saveRow(array $importData) */ if (empty($importData['type']) || !isset($productTypes[strtolower($importData['type'])])) { $value = isset($importData['type']) ? $importData['type'] : ''; - $message = Mage::helper('catalog')->__( - 'Skip import row, is not valid value "%s" for field "%s"', - $value, - 'type' - ); + $message = Mage::helper('catalog')->__('Skip import row, is not valid value "%s" for field "%s"', $value, 'type'); Mage::throwException($message); } $product->setTypeId($productTypes[strtolower($importData['type'])]); @@ -642,11 +629,7 @@ public function saveRow(array $importData) */ if (empty($importData['attribute_set']) || !isset($productAttributeSets[$importData['attribute_set']])) { $value = isset($importData['attribute_set']) ? $importData['attribute_set'] : ''; - $message = Mage::helper('catalog')->__( - 'Skip import row, the value "%s" is invalid for field "%s"', - $value, - 'attribute_set' - ); + $message = Mage::helper('catalog')->__('Skip import row, the value "%s" is invalid for field "%s"', $value, 'attribute_set'); Mage::throwException($message); } $product->setAttributeSetId($productAttributeSets[$importData['attribute_set']]); @@ -654,10 +637,7 @@ public function saveRow(array $importData) foreach ($this->_requiredFields as $field) { $attribute = $this->getAttribute($field); if (!isset($importData[$field]) && $attribute && $attribute->getIsRequired()) { - $message = Mage::helper('catalog')->__( - 'Skipping import row, required field "%s" for new products is not defined.', - $field - ); + $message = Mage::helper('catalog')->__('Skipping import row, required field "%s" for new products is not defined.', $field); Mage::throwException($message); } } diff --git a/app/code/core/Mage/Catalog/Model/Indexer/Url.php b/app/code/core/Mage/Catalog/Model/Indexer/Url.php index aa7ab758df..d84aef427a 100644 --- a/app/code/core/Mage/Catalog/Model/Indexer/Url.php +++ b/app/code/core/Mage/Catalog/Model/Indexer/Url.php @@ -35,6 +35,11 @@ */ class Mage_Catalog_Model_Indexer_Url extends Mage_Index_Model_Indexer_Abstract { + /** + * Data key for matching result to be saved in + */ + const EVENT_MATCH_RESULT_KEY = 'catalog_url_match_result'; + /** * Index math: product save, category save, store save * store group save, config save @@ -98,33 +103,30 @@ public function getDescription() public function matchEvent(Mage_Index_Model_Event $event) { $data = $event->getNewData(); - $resultKey = 'catalog_url_match_result'; - if (isset($data[$resultKey])) { - return $data[$resultKey]; + if (isset($data[self::EVENT_MATCH_RESULT_KEY])) { + return $data[self::EVENT_MATCH_RESULT_KEY]; } - $result = null; $entity = $event->getEntity(); if ($entity == Mage_Core_Model_Store::ENTITY) { $store = $event->getDataObject(); - if ($store->isObjectNew() || $store->dataHasChangedFor('group_id')) { + if ($store && ($store->isObjectNew() || $store->dataHasChangedFor('group_id'))) { $result = true; } else { $result = false; } } else if ($entity == Mage_Core_Model_Store_Group::ENTITY) { $storeGroup = $event->getDataObject(); - $hasDataChanges = $storeGroup->dataHasChangedFor('root_category_id') - || $storeGroup->dataHasChangedFor('website_id'); - if (!$storeGroup->isObjectNew() && $hasDataChanges) { + $hasDataChanges = $storeGroup && ($storeGroup->dataHasChangedFor('root_category_id') + || $storeGroup->dataHasChangedFor('website_id')); + if ($storeGroup && !$storeGroup->isObjectNew() && $hasDataChanges) { $result = true; } else { $result = false; } } else if ($entity == Mage_Core_Model_Config_Data::ENTITY) { $configData = $event->getDataObject(); - $path = $configData->getPath(); - if (in_array($path, $this->_relatedConfigSettings)) { + if ($configData && in_array($configData->getPath(), $this->_relatedConfigSettings)) { $result = $configData->isValueChanged(); } else { $result = false; @@ -133,7 +135,7 @@ public function matchEvent(Mage_Index_Model_Event $event) $result = parent::matchEvent($event); } - $event->addNewData($resultKey, $result); + $event->addNewData(self::EVENT_MATCH_RESULT_KEY, $result); return $result; } @@ -145,6 +147,7 @@ public function matchEvent(Mage_Index_Model_Event $event) */ protected function _registerEvent(Mage_Index_Model_Event $event) { + $event->addNewData(self::EVENT_MATCH_RESULT_KEY, true); $entity = $event->getEntity(); switch ($entity) { case Mage_Catalog_Model_Product::ENTITY: @@ -246,6 +249,15 @@ protected function _processEvent(Mage_Index_Model_Event $event) */ public function reindexAll() { - Mage::getSingleton('catalog/url')->refreshRewrites(); + /** @var $resourceModel Mage_Catalog_Model_Resource_Url */ + $resourceModel = Mage::getResourceSingleton('catalog/url'); + $resourceModel->beginTransaction(); + try { + Mage::getSingleton('catalog/url')->refreshRewrites(); + $resourceModel->commit(); + } catch (Exception $e) { + $resourceModel->rollBack(); + throw $e; + } } } diff --git a/app/code/core/Mage/Catalog/Model/Layer/Filter/Attribute.php b/app/code/core/Mage/Catalog/Model/Layer/Filter/Attribute.php index 534eafb6de..8f5be3c794 100644 --- a/app/code/core/Mage/Catalog/Model/Layer/Filter/Attribute.php +++ b/app/code/core/Mage/Catalog/Model/Layer/Filter/Attribute.php @@ -69,7 +69,7 @@ protected function _getResource() * Get option text from frontend model by option id * * @param int $optionId - * @return unknown + * @return string|bool */ protected function _getOptionText($optionId) { @@ -90,7 +90,7 @@ public function apply(Zend_Controller_Request_Abstract $request, $filterBlock) return $this; } $text = $this->_getOptionText($filter); - if ($filter && $text) { + if ($filter && strlen($text)) { $this->_getResource()->applyFilterToCollection($this, $filter); $this->getLayer()->getState()->addFilter($this->_createItem($text, $filter)); $this->_items = array(); diff --git a/app/code/core/Mage/Catalog/Model/Layer/Filter/Price.php b/app/code/core/Mage/Catalog/Model/Layer/Filter/Price.php index b697ef264f..90c36aa453 100644 --- a/app/code/core/Mage/Catalog/Model/Layer/Filter/Price.php +++ b/app/code/core/Mage/Catalog/Model/Layer/Filter/Price.php @@ -31,10 +31,16 @@ * @package Mage_Catalog * @author Magento Core Team */ + +/** + * @method Mage_Catalog_Model_Layer_Filter_Price setInterval(array) + * @method array getInterval() + */ class Mage_Catalog_Model_Layer_Filter_Price extends Mage_Catalog_Model_Layer_Filter_Abstract { const XML_PATH_RANGE_CALCULATION = 'catalog/layered_navigation/price_range_calculation'; const XML_PATH_RANGE_STEP = 'catalog/layered_navigation/price_range_step'; + const XML_PATH_RANGE_MAX_INTERVALS = 'catalog/layered_navigation/price_range_max_intervals'; const RANGE_CALCULATION_AUTO = 'auto'; const RANGE_CALCULATION_MANUAL = 'manual'; @@ -102,10 +108,6 @@ public function getPriceRange() } } - while (ceil($maxPrice / $range) > 25) { - $range *= 10; - } - $this->setData('price_range', $range); } @@ -141,6 +143,19 @@ public function getRangeItemCounts($range) $items = $this->getData($rangeKey); if (is_null($items)) { $items = $this->_getResource()->getCount($this, $range); + // checking max number of intervals + $i = 0; + $lastIndex = null; + $maxIntervalsNumber = $this->getMaxIntervalsNumber(); + foreach ($items as $k => $v) { + ++$i; + if ($i > 1 && $i > $maxIntervalsNumber) { + $items[$lastIndex] += $v; + unset($items[$k]); + } else { + $lastIndex = $k; + } + } $this->setData($rangeKey, $items); } @@ -150,6 +165,7 @@ public function getRangeItemCounts($range) /** * Prepare text of item label * + * @deprecated since 1.7.0.0 * @param int $range * @param float $value * @return string @@ -157,12 +173,32 @@ public function getRangeItemCounts($range) protected function _renderItemLabel($range, $value) { $store = Mage::app()->getStore(); - $fromPrice = $store->formatPrice(($value-1)*$range); + $fromPrice = $store->formatPrice(($value - 1) * $range); $toPrice = $store->formatPrice($value*$range); return Mage::helper('catalog')->__('%s - %s', $fromPrice, $toPrice); } + /** + * Prepare text of range label + * + * @param float|string $fromPrice + * @param float|string $toPrice + * @return string + */ + protected function _renderRangeLabel($fromPrice, $toPrice) + { + $store = Mage::app()->getStore(); + $formattedFromPrice = $store->formatPrice($fromPrice); + if (empty($toPrice)) { + return Mage::helper('catalog')->__('%s and above', $formattedFromPrice); + } elseif ($fromPrice == $toPrice) { + return $formattedFromPrice; + } else { + return Mage::helper('catalog')->__('%s - %s', $formattedFromPrice, $store->formatPrice($toPrice - .01)); + } + } + /** * Get price aggreagation data cache key * @deprecated after 1.4 @@ -182,6 +218,34 @@ protected function _getCacheKey() return $key; } + /** + * Get data generated by algorithm for build price filter items + * + * @return array + */ + protected function _getCalculatedItemsData() + { + $appliedInterval = $this->getInterval(); + if ($appliedInterval) { + return array(); + } + + /** @var $algorithmModel Mage_Catalog_Model_Layer_Filter_Price_Algorithm */ + $algorithmModel = Mage::getSingleton('catalog/layer_filter_price_algorithm'); + $this->_getResource()->loadAllPrices($algorithmModel, $this); + + $items = array(); + foreach ($algorithmModel->calculateSeparators() as $separator) { + $items[] = array( + 'label' => $this->_renderRangeLabel($separator['from'], $separator['to']), + 'value' => (($separator['from'] == 0) ? '' : $separator['from']) . '-' . $separator['to'], + 'count' => $separator['count'], + ); + } + + return $items; + } + /** * Get data for build price filter items * @@ -189,16 +253,33 @@ protected function _getCacheKey() */ protected function _getItemsData() { + // check if filter is already applied + if ($this->getInterval()) { + return array(); + } + + if (Mage::app()->getStore()->getConfig(self::XML_PATH_RANGE_CALCULATION) == self::RANGE_CALCULATION_AUTO) { + return $this->_getCalculatedItemsData(); + } + $range = $this->getPriceRange(); $dbRanges = $this->getRangeItemCounts($range); $data = array(); - foreach ($dbRanges as $index=>$count) { - $data[] = array( - 'label' => $this->_renderItemLabel($range, $index), - 'value' => $index . ',' . $range, - 'count' => $count, - ); + if (!empty($dbRanges)) { + $lastIndex = array_keys($dbRanges); + $lastIndex = $lastIndex[count($lastIndex) - 1]; + + foreach ($dbRanges as $index => $count) { + $fromPrice = ($index == 1) ? '' : (($index - 1) * $range); + $toPrice = ($index == $lastIndex) ? '' : ($index * $range); + + $data[] = array( + 'label' => $this->_renderRangeLabel($fromPrice, $toPrice), + 'value' => $fromPrice . '-' . $toPrice, + 'count' => $count, + ); + } } return $data; @@ -207,6 +288,17 @@ protected function _getItemsData() /** * Apply price range filter to collection * + * @return Mage_Catalog_Model_Layer_Filter_Price + */ + protected function _applyPriceRange() + { + $this->_getResource()->applyPriceRange($this); + return $this; + } + + /** + * Apply price range filter + * * @param Zend_Controller_Request_Abstract $request * @param $filterBlock * @@ -215,36 +307,41 @@ protected function _getItemsData() public function apply(Zend_Controller_Request_Abstract $request, $filterBlock) { /** - * Filter must be string: $index,$range + * Filter must be string: $fromPrice-$toPrice */ $filter = $request->getParam($this->getRequestVar()); if (!$filter) { return $this; } - $filter = explode(',', $filter); + //validate filter + $filter = explode('-', $filter); if (count($filter) != 2) { return $this; } + foreach ($filter as $v) { + if ($v !== '' && (float)$v <= 0) { + return $this; + } + } - list($index, $range) = $filter; + list($from, $to) = $filter; - if ((int)$index && (int)$range) { - $this->setPriceRange((int)$range); + $this->setInterval(array($from, $to)); - $this->_applyToCollection($range, $index); - $this->getLayer()->getState()->addFilter( - $this->_createItem($this->_renderItemLabel($range, $index), $filter) - ); + $this->_applyPriceRange(); + $this->getLayer()->getState()->addFilter($this->_createItem( + $this->_renderRangeLabel(empty($from) ? 0 : $from, $to), + $filter + )); - $this->_items = array(); - } return $this; } /** * Apply filter value to product collection based on filter range and selected value * + * @deprecated since 1.7.0.0 * @param int $range * @param int $index * @return Mage_Catalog_Model_Layer_Filter_Price @@ -307,4 +404,14 @@ public function setCurrencyRate($rate) { return $this->setData('currency_rate', $rate); } + + /** + * Get maximum number of intervals + * + * @return int + */ + public function getMaxIntervalsNumber() + { + return (int)Mage::app()->getStore()->getConfig(self::XML_PATH_RANGE_MAX_INTERVALS); + } } diff --git a/app/code/core/Mage/Catalog/Model/Layer/Filter/Price/Algorithm.php b/app/code/core/Mage/Catalog/Model/Layer/Filter/Price/Algorithm.php new file mode 100644 index 0000000000..0a060376df --- /dev/null +++ b/app/code/core/Mage/Catalog/Model/Layer/Filter/Price/Algorithm.php @@ -0,0 +1,420 @@ + + */ +class Mage_Catalog_Model_Layer_Filter_Price_Algorithm +{ + const MIN_POSSIBLE_PRICE = .01; + const TEN_POWER_ROUNDING_FACTOR = 4; + + /** + * Sorted array of all products prices + * + * @var array + */ + protected $_prices = null; + + /** + * Number of segmentation intervals + * + * @var null|int + */ + protected $_intervalsNumber = null; + + /** + * Upper limits of skipped quantiles + * + * @var array + */ + protected $_skippedQuantilesUpperLimits = array(); + + /** + * Set products prices + * + * @param array $prices + * @return Mage_Catalog_Model_Layer_Filter_Price_Algorithm + */ + public function setPrices(array $prices) + { + $this->_prices = $prices; + sort($this->_prices); + $this->_intervalsNumber = null; + $this->_skippedQuantilesUpperLimits = array(); + + return $this; + } + + /** + * Get min price + * + * @return float + */ + public function getMinPrice() + { + return empty($this->_prices) ? 0 : $this->_prices[0]; + } + + /** + * Get max price + * + * @return float + */ + public function getMaxPrice() + { + return (empty($this->_prices)) ? 0 : $this->_prices[count($this->_prices) - 1]; + } + + /** + * Get amount of segmentation intervals + * + * @return int + */ + public function getIntervalsNumber() + { + if (!is_null($this->_intervalsNumber)) { + return $this->_intervalsNumber; + } + + $pricesCount = count($this->_prices); + if ($pricesCount < 2 || ($this->getMaxPrice() - $this->getMinPrice() == 0)) { + //Same price couldn't be separated with several intervals + $this->_intervalsNumber = 1; + return $this->_intervalsNumber; + } + + $sum = 0; + $sumSquares = 0; + foreach ($this->_prices as $price) { + $sum += $price; + $sumSquares += $price * $price; + } + + if ($pricesCount * $sumSquares - $sum * $sum <= 0) { + $intervalsNumber = 1000; + } else { + $intervalsNumber = ($this->getMaxPrice() - $this->getMinPrice()) * pow($pricesCount, 5 / 6) + * sqrt(($pricesCount - 1) / ($pricesCount * $sumSquares - $sum * $sum)) / 3.5; + } + $this->_intervalsNumber = min(max(ceil($intervalsNumber), 2), 10); + + return $this->_intervalsNumber; + } + + /** + * Get quantile + * + * @param int $quantileNumber should be from 1 to n-1 where n is number of intervals + * @return float|null + */ + protected function _getQuantile($quantileNumber) + { + if ($quantileNumber < 1 || $quantileNumber >= $this->getIntervalsNumber()) { + return 0; + } + + return $quantileNumber * count($this->_prices) / $this->getIntervalsNumber() - .5; + } + + /** + * Get quantile interval + * + * @param int $quantileNumber should be from 1 to n-1 where n is number of intervals + * @return null|array [floatMin,floatMax] + */ + protected function _getQuantileInterval($quantileNumber) + { + if ($quantileNumber < 1 || $quantileNumber >= $this->getIntervalsNumber()) { + return null; + } + $pricesCount = count($this->_prices); + $quantile = $this->_getQuantile($quantileNumber); + $deflectionLimit = floor($pricesCount / 2 / $this->getIntervalsNumber()); + $limits = array( + min(floor($quantile - $deflectionLimit), floor($quantile)), + max(ceil($quantile + $deflectionLimit - 1), ceil($quantile)), + ); + + $deflection = $this->_getStandardNormalDistribution() + * sqrt($pricesCount * $quantileNumber * ($this->getIntervalsNumber() - $quantileNumber)) + / $this->getIntervalsNumber(); + $left = max(floor($quantile - $deflection - 1), $limits[0], 0); + if (array_key_exists($quantileNumber - 1, $this->_skippedQuantilesUpperLimits) + && $left > $this->_skippedQuantilesUpperLimits[$quantileNumber - 1] + ) { + $left = $this->_skippedQuantilesUpperLimits[$quantileNumber - 1]; + } + $right = min(ceil($quantile + $deflection), $limits[1], $pricesCount - 1); + return array($left, $right); + } + + /** + * Get standard normal distribution + * + * @return float + */ + protected function _getStandardNormalDistribution() + { + return 1.96; + } + + /** + * Find max rounding factor with given price range + * + * @param float $lowerPrice + * @param float $upperPrice + * @param bool $returnEmpty whether empty result is acceptable + * @param null|float $roundingFactor if given, checks for range to contain the factor + * @return false|array + */ + protected function _findRoundPrice($lowerPrice, $upperPrice, $returnEmpty = true, $roundingFactor = null) + { + $lowerPrice = round($lowerPrice, 3); + $upperPrice = round($upperPrice, 3); + + if (!is_null($roundingFactor)) { + // Can't separate if prices are equal + if ($lowerPrice >= $upperPrice) { + if ($lowerPrice > $upperPrice || $returnEmpty) { + return false; + } + } + // round is used for such examples: (1194.32 / 0.02) or (5 / 100000) + $lowerDivision = ceil(round($lowerPrice / $roundingFactor, self::TEN_POWER_ROUNDING_FACTOR + 3)); + $upperDivision = floor(round($upperPrice / $roundingFactor, self::TEN_POWER_ROUNDING_FACTOR + 3)); + if ($lowerDivision > $upperDivision) { + return false; + } + $averageDivision = ($lowerDivision + $upperDivision) / 2; + $lowerAverageDivision = floor($averageDivision); + $result = array(round($lowerAverageDivision * $roundingFactor, 2)); + if ($averageDivision != $lowerAverageDivision) { + $upperAverageDivision = ceil($averageDivision); + $result[] = round($upperAverageDivision * $roundingFactor, 2); + } + return $result; + } + + $tenPower = pow(10, self::TEN_POWER_ROUNDING_FACTOR); + $roundingFactorCoefficients = array(10, 5, 2); + while ($tenPower >= self::MIN_POSSIBLE_PRICE) { + if ($tenPower == self::MIN_POSSIBLE_PRICE) { + $roundingFactorCoefficients[] = 1; + } + foreach ($roundingFactorCoefficients as $roundingFactorCoefficient) { + $roundingFactorCoefficient *= $tenPower; + $roundPrices = $this->_findRoundPrice( + $lowerPrice, $upperPrice, $returnEmpty, $roundingFactorCoefficient + ); + if ($roundPrices) { + return array($roundingFactorCoefficient, $roundPrices); + } + } + $tenPower /= 10; + } + + return array(self::MIN_POSSIBLE_PRICE, array()); + } + + /** + * Find price separator for the quantile + * + * @param int $quantileNumber should be from 1 to n-1 where n is number of intervals + * @return array|null + */ + protected function _findPriceSeparator($quantileNumber) + { + if ($quantileNumber < 1 || $quantileNumber >= $this->getIntervalsNumber()) { + return null; + } + $quantile = $this->_getQuantile($quantileNumber); + $lowerQuantile = floor($quantile); + $upperQuantile = ceil($quantile); + + $quantileInterval = $this->_getQuantileInterval($quantileNumber); + $quantileDeflection = 0; + $maxRoundingFactor = self::MIN_POSSIBLE_PRICE; + $bestRoundPrice = array(); + + if ($this->_prices[$quantileInterval[0]] == $this->_prices[$quantileInterval[1]]) { + if ($quantileNumber == 1) { + $i = $quantileInterval[0]; + while ($i >= 0 && ($this->_prices[$i] == $this->_prices[$quantileInterval[1]])) { + --$i; + } + if ($i >= 0) { + list($roundingFactor, $bestRoundPrice) = $this->_findRoundPrice( + $this->_prices[$i] + self::MIN_POSSIBLE_PRICE / 10, + $this->_prices[$quantileInterval[1]], + false + ); + } + } + if ($quantileNumber == $this->getIntervalsNumber() - 1) { + $pricesCount = count($this->_prices); + $i = $quantileInterval[1]; + while ($i < $pricesCount && ($this->_prices[$quantileInterval[0]] == $this->_prices[$i])) { + ++$i; + } + if ($i < $pricesCount) { + list($upperRoundingFactor, $upperBestRoundPrice) = $this->_findRoundPrice( + $this->_prices[$quantileInterval[0]] + self::MIN_POSSIBLE_PRICE / 10, + $this->_prices[$i], + false + ); + if (!empty($bestRoundPrice)) { + if ($upperRoundingFactor >= $roundingFactor) { + if ($upperRoundingFactor > $roundingFactor) { + $bestRoundPrice = $upperBestRoundPrice; + } else { + $bestRoundPrice = array_merge($bestRoundPrice, $upperBestRoundPrice); + } + } + } else { + $bestRoundPrice = $upperBestRoundPrice; + } + } + } + } else { + while ($lowerQuantile - $quantileDeflection >= $quantileInterval[0] + || $upperQuantile + $quantileDeflection <= $quantileInterval[1] + ) { + $leftIndex = max($quantileInterval[0], $lowerQuantile - $quantileDeflection); + $rightIndex = min($quantileInterval[1], $upperQuantile + $quantileDeflection); + + list($roundingFactor, $roundPrice) = $this->_findRoundPrice( + $this->_prices[$leftIndex] + self::MIN_POSSIBLE_PRICE / 10, + $this->_prices[$rightIndex] + ); + + if ($roundingFactor >= $maxRoundingFactor) { + if ($roundingFactor == $maxRoundingFactor) { + $bestRoundPrice = array_unique(array_merge($bestRoundPrice, $roundPrice)); + } else { + $bestRoundPrice = $roundPrice; + $maxRoundingFactor = $roundingFactor; + } + } + ++$quantileDeflection; + } + } + + if (empty($bestRoundPrice)) { + $this->_skippedQuantilesUpperLimits[$quantileNumber] = $quantileInterval[1]; + return $bestRoundPrice; + } + + sort($bestRoundPrice); + return $bestRoundPrice; + } + + /** + * Calculate separators, each contains 'from', 'to' and 'count' + * + * @return array + */ + public function calculateSeparators() + { + $separators = array(); + for ($i = 1; $i < $this->getIntervalsNumber(); ++$i) { + $separators[] = $this->_findPriceSeparator($i); + } + $pricesCount = count($this->_prices); + + $i = 0; + $result = array(); + $lastSeparator = 0; + $quantile = 0; + while (!empty($separators) && ($i < $pricesCount)) { + while (!empty($separators) && empty($separators[0])) { + array_shift($separators); + } + if (empty($separators)) { + break; + } + if ($this->_prices[$i] < $separators[0][0]) { + ++$i; + } else { + $separator = array_shift($separators[0]); + $separatorData = array( + 'from' => $lastSeparator, + 'to' => $separator, + 'count' => $i, + ); + + $deflection = abs(($quantile + 1) / $this->getIntervalsNumber() - $i / $pricesCount); + if (!array_key_exists($quantile, $result)) { + $result[$quantile] = array($deflection, $separatorData); + } elseif ($deflection < $result[$quantile][0]) { + $result[$quantile] = array($deflection, $separatorData); + } + + if (empty($separators[0])) { + array_shift($separators); + if (!array_key_exists($quantile - 1, $result) + || $result[$quantile - 1][1]['count'] < $result[$quantile][1]['count'] + ) { + $lastSeparator = $result[$quantile][1]['to']; + } + ++$quantile; + } + } + } + if ($i < $pricesCount || empty($result)) { + $result[$quantile] = array(0, array( + 'from' => $lastSeparator, + 'to' => '', + 'count' => $pricesCount, + )); + } + + for ($i = count($result) - 1; $i >= 0; --$i) { + $rangeCount = ($i == 0) ? $result[$i][1]['count'] : ($result[$i][1]['count'] - $result[$i-1][1]['count']); + if ($rangeCount > 0) { + $result[$i] = $result[$i][1]; + $firstPriceInRange = $this->_prices[$result[$i]['count'] - $rangeCount]; + if ($this->_prices[$result[$i]['count'] - 1] == $firstPriceInRange) { + $result[$i]['from'] = $firstPriceInRange; + $result[$i]['to'] = $firstPriceInRange; + } + $result[$i]['from'] = round($result[$i]['from'], 2); + if (!empty($result[$i]['to'])) { + $result[$i]['to'] = round($result[$i]['to'], 2); + } + $result[$i]['count'] = $rangeCount; + } else { + unset($result[$i]); + } + } + + return array_values($result); + } +} diff --git a/app/code/core/Mage/Catalog/Model/Observer.php b/app/code/core/Mage/Catalog/Model/Observer.php index 4a3adc3237..5551a354a9 100644 --- a/app/code/core/Mage/Catalog/Model/Observer.php +++ b/app/code/core/Mage/Catalog/Model/Observer.php @@ -231,4 +231,17 @@ public function catalogCheckIsUsingStaticUrlsAllowed(Varien_Event_Observer $obse $result = $observer->getEvent()->getData('result'); $result->isAllowed = Mage::helper('catalog')->setStoreId($storeId)->isUsingStaticUrlsAllowed(); } + + /** + * Cron job method for product prices to reindex + * + * @param Mage_Cron_Model_Schedule $schedule + */ + public function reindexProductPrices(Mage_Cron_Model_Schedule $schedule) + { + $indexProcess = Mage::getSingleton('index/indexer')->getProcessByCode('catalog_product_price'); + if ($indexProcess) { + $indexProcess->reindexAll(); + } + } } diff --git a/app/code/core/Mage/Catalog/Model/Product.php b/app/code/core/Mage/Catalog/Model/Product.php index 1470ccbfdf..e0620dfb69 100644 --- a/app/code/core/Mage/Catalog/Model/Product.php +++ b/app/code/core/Mage/Catalog/Model/Product.php @@ -276,8 +276,8 @@ public function getTypeInstance($singleton = false) /** * Set type instance for external * - * @param Mage_Catalog_Model_Product_Type_Abstract $singleton - * @param bool $singleton + * @param Mage_Catalog_Model_Product_Type_Abstract $instance Product type instance + * @param bool $singleton Whether instance is singleton * @return Mage_Catalog_Model_Product */ public function setTypeInstance($instance, $singleton = false) @@ -434,13 +434,13 @@ public function getStoreIds() /** * Retrieve product attributes - * * if $groupId is null - retrieve all product attributes * - * @param int $groupId - * @return array + * @param int $groupId Retrieve attributes of the specified group + * @param bool $skipSuper Not used + * @return array */ - public function getAttributes($groupId = null, $skipSuper=false) + public function getAttributes($groupId = null, $skipSuper = false) { $productAttributes = $this->getTypeInstance(true)->getEditableAttributes($this); if ($groupId) { @@ -641,6 +641,16 @@ public function getPriceModel() return Mage::getSingleton('catalog/product_type')->priceFactory($this->getTypeId()); } + /** + * Get product group price + * + * @return float + */ + public function getGroupPrice() + { + return $this->getPriceModel()->getGroupPrice($this); + } + /** * Get product tier price by qty * @@ -690,6 +700,7 @@ public function getFormatedPrice() * products it's called very often in Item->getProduct(). So removing chain of magic with more cpu consuming * algorithms gives nice optimization boost. * + * @param float $price Price amount * @return Mage_Catalog_Model_Product */ public function setFinalPrice($price) @@ -713,26 +724,51 @@ public function getFinalPrice($qty=null) return $this->getPriceModel()->getFinalPrice($qty, $this); } + /** + * Returns calculated final price + * + * @return float + */ public function getCalculatedFinalPrice() { return $this->_getData('calculated_final_price'); } + /** + * Returns minimal price + * + * @return float + */ public function getMinimalPrice() { return max($this->_getData('minimal_price'), 0); } + /** + * Returns special price + * + * @return float + */ public function getSpecialPrice() { return $this->_getData('special_price'); } + /** + * Returns starting date of the special price + * + * @return mixed + */ public function getSpecialFromDate() { return $this->_getData('special_from_date'); } + /** + * Returns end date of the special price + * + * @return mixed + */ public function getSpecialToDate() { return $this->_getData('special_to_date'); @@ -779,6 +815,8 @@ public function getRelatedProductIds() /** * Retrieve collection related product + * + * @return Mage_Catalog_Model_Resource_Product_Link_Product_Collection */ public function getRelatedProductCollection() { @@ -791,6 +829,8 @@ public function getRelatedProductCollection() /** * Retrieve collection related link + * + * @return Mage_Catalog_Model_Resource_Product_Link_Collection */ public function getRelatedLinkCollection() { @@ -839,6 +879,8 @@ public function getUpSellProductIds() /** * Retrieve collection up sell product + * + * @return Mage_Catalog_Model_Resource_Product_Link_Product_Collection */ public function getUpSellProductCollection() { @@ -851,6 +893,8 @@ public function getUpSellProductCollection() /** * Retrieve collection up sell link + * + * @return Mage_Catalog_Model_Resource_Product_Link_Collection */ public function getUpSellLinkCollection() { @@ -913,6 +957,8 @@ public function getCrossSellProductCollection() /** * Retrieve collection cross sell link + * + * @return Mage_Catalog_Model_Resource_Product_Link_Collection */ public function getCrossSellLinkCollection() { @@ -927,6 +973,8 @@ public function getCrossSellLinkCollection() /** * Retrieve collection grouped link + * + * @return Mage_Catalog_Model_Resource_Product_Link_Collection */ public function getGroupedLinkCollection() { @@ -990,9 +1038,10 @@ public function getMediaGalleryImages() * * @param string $file file path of image in file system * @param string|array $mediaAttribute code of attribute with type 'media_image', - * leave blank if image should be only in gallery + * leave blank if image should be only in gallery * @param boolean $move if true, it will move source file * @param boolean $exclude mark image as disabled in product page view + * @return Mage_Catalog_Model_Product */ public function addImageToMediaGallery($file, $mediaAttribute=null, $move=false, $exclude=true) { @@ -1038,18 +1087,18 @@ public function duplicate() Mage::dispatchEvent( 'catalog_model_product_duplicate', - array('current_product'=>$this, 'new_product'=>$newProduct) + array('current_product' => $this, 'new_product' => $newProduct) ); /* @var $newProduct Mage_Catalog_Model_Product */ -// $newOptionsArray = array(); -// $newProduct->setCanSaveCustomOptions(true); -// foreach ($this->getOptions() as $_option) { -// /* @var $_option Mage_Catalog_Model_Product_Option */ -// $newOptionsArray[] = $_option->prepareOptionForDuplicate(); -// } -// $newProduct->setProductOptions($newOptionsArray); + $newOptionsArray = array(); + $newProduct->setCanSaveCustomOptions(true); + foreach ($this->getOptions() as $_option) { + /* @var $_option Mage_Catalog_Model_Product_Option */ + $newOptionsArray[] = $_option->prepareOptionForDuplicate(); + } + $newProduct->setProductOptions($newOptionsArray); /* Prepare Related*/ $data = array(); @@ -1057,7 +1106,7 @@ public function duplicate() $attributes = array(); foreach ($this->getLinkInstance()->getAttributes() as $_attribute) { if (isset($_attribute['code'])) { - $attributes[]=$_attribute['code']; + $attributes[] = $_attribute['code']; } } foreach ($this->getRelatedLinkCollection() as $_link) { @@ -1071,7 +1120,7 @@ public function duplicate() $attributes = array(); foreach ($this->getLinkInstance()->getAttributes() as $_attribute) { if (isset($_attribute['code'])) { - $attributes[]=$_attribute['code']; + $attributes[] = $_attribute['code']; } } foreach ($this->getUpSellLinkCollection() as $_link) { @@ -1085,7 +1134,7 @@ public function duplicate() $attributes = array(); foreach ($this->getLinkInstance()->getAttributes() as $_attribute) { if (isset($_attribute['code'])) { - $attributes[]=$_attribute['code']; + $attributes[] = $_attribute['code']; } } foreach ($this->getCrossSellLinkCollection() as $_link) { @@ -1099,7 +1148,7 @@ public function duplicate() $attributes = array(); foreach ($this->getLinkInstance()->getAttributes() as $_attribute) { if (isset($_attribute['code'])) { - $attributes[]=$_attribute['code']; + $attributes[] = $_attribute['code']; } } foreach ($this->getGroupedLinkCollection() as $_link) { @@ -1128,11 +1177,21 @@ public function duplicate() return $newProduct; } + /** + * Is product grouped + * + * @return bool + */ public function isSuperGroup() { return $this->getTypeId() == Mage_Catalog_Model_Product_Type::TYPE_GROUPED; } + /** + * Alias for isConfigurable() + * + * @return bool + */ public function isSuperConfig() { return $this->isConfigurable(); @@ -1157,11 +1216,21 @@ public function isConfigurable() return $this->getTypeId() == Mage_Catalog_Model_Product_Type::TYPE_CONFIGURABLE; } + /** + * Whether product configurable or grouped + * + * @return bool + */ public function isSuper() { return $this->isConfigurable() || $this->isGrouped(); } + /** + * Returns visible status IDs in catalog + * + * @return array + */ public function getVisibleInCatalogStatuses() { return Mage::getSingleton('catalog/product_status')->getVisibleStatusIds(); @@ -1303,16 +1372,32 @@ public function isRecurring() return $this->getIsRecurring() == '1'; } + /** + * Alias for isSalable() + * + * @return bool + */ public function isSaleable() { return $this->isSalable(); } + /** + * Whether product available in stock + * + * @return bool + */ public function isInStock() { return $this->getStatus() == Mage_Catalog_Model_Product_Status::STATUS_ENABLED; } + /** + * Get attribute text by its code + * + * @param $attributeCode Code of the attribute + * @return string + */ public function getAttributeText($attributeCode) { return $this->getResource() @@ -1321,6 +1406,11 @@ public function getAttributeText($attributeCode) ->getOptionText($this->getData($attributeCode)); } + /** + * Returns array with dates for custom design + * + * @return array + */ public function getCustomDesignDate() { $result = array(); @@ -1352,6 +1442,12 @@ public function getUrlInStore($params = array()) return $this->getUrlModel()->getUrlInStore($this, $params); } + /** + * Formats URL key + * + * @param $str URL + * @return string + */ public function formatUrlKey($str) { return $this->getUrlModel()->formatUrlKey($str); @@ -1368,6 +1464,14 @@ public function getUrlPath($category=null) return $this->getUrlModel()->getUrlPath($this, $category); } + /** + * Save current attribute with code $code and assign new value + * + * @param string $code Attribute code + * @param mixed $value New attribute value + * @param int $store Store ID + * @return void + */ public function addAttributeUpdate($code, $value, $store) { $oldValue = $this->getData($code); @@ -1381,6 +1485,12 @@ public function addAttributeUpdate($code, $value, $store) $this->setStoreId($oldStore); } + /** + * Renders the object to array + * + * @param array $arrAttributes Attribute array + * @return array + */ public function toArray(array $arrAttributes=array()) { $data = parent::toArray($arrAttributes); @@ -1391,6 +1501,12 @@ public function toArray(array $arrAttributes=array()) return $data; } + /** + * Same as setData(), but also initiates the stock item (if it is there) + * + * @param array $data Array to form the object from + * @return Mage_Catalog_Model_Product + */ public function fromArray($data) { if (isset($data['stock_item'])) { @@ -1415,6 +1531,11 @@ public function loadParentProductIds() return $this->setParentProductIds(array()); } + /** + * Delete product + * + * @return Mage_Catalog_Model_Product + */ public function delete() { parent::delete(); @@ -1422,6 +1543,11 @@ public function delete() return $this; } + /** + * Returns request path + * + * @return string + */ public function getRequestPath() { return $this->_getData('request_path'); @@ -1429,6 +1555,7 @@ public function getRequestPath() /** * Custom function for other modules + * @return string */ public function getGiftMessageAvailable() @@ -1436,6 +1563,11 @@ public function getGiftMessageAvailable() return $this->_getData('gift_message_available'); } + /** + * Returns rating summary + * + * @return mixed + */ public function getRatingSummary() { return $this->_getData('rating_summary'); @@ -1558,9 +1690,9 @@ public function getIsVirtual() /** * Add custom option information to product * - * @param string $code - * @param mixed $value - * @param int $productId + * @param string $code Option code + * @param mixed $value Value of the option + * @param int $product Product ID * @return Mage_Catalog_Model_Product */ public function addCustomOption($code, $value, $product=null) @@ -1577,6 +1709,12 @@ public function addCustomOption($code, $value, $product=null) return $this; } + /** + * Sets custom options for the product + * + * @param array $options Array of options + * @return void + */ public function setCustomOptions(array $options) { $this->_customOptions = $options; @@ -1917,6 +2055,7 @@ protected function _clearOptionReferences() /** * Retrieve product entities info as array * + * @param string|array $columns One or several columns * @return array */ public function getProductEntitiesInfo($columns = null) diff --git a/app/code/core/Mage/Catalog/Model/Product/Attribute/Backend/Groupprice.php b/app/code/core/Mage/Catalog/Model/Product/Attribute/Backend/Groupprice.php new file mode 100644 index 0000000000..437bf449a6 --- /dev/null +++ b/app/code/core/Mage/Catalog/Model/Product/Attribute/Backend/Groupprice.php @@ -0,0 +1,57 @@ + + */ +class Mage_Catalog_Model_Product_Attribute_Backend_Groupprice + extends Mage_Catalog_Model_Product_Attribute_Backend_Groupprice_Abstract +{ + /** + * Retrieve resource instance + * + * @return Mage_Catalog_Model_Resource_Eav_Mysql4_Product_Attribute_Backend_Tierprice + */ + protected function _getResource() + { + return Mage::getResourceSingleton('catalog/product_attribute_backend_groupprice'); + } + + /** + * Error message when duplicates + * + * @return string + */ + protected function _getDuplicateErrorMessage() + { + return Mage::helper('catalog')->__('Duplicate website group price customer group.'); + } +} diff --git a/app/code/core/Mage/Catalog/Model/Product/Attribute/Backend/Groupprice/Abstract.php b/app/code/core/Mage/Catalog/Model/Product/Attribute/Backend/Groupprice/Abstract.php new file mode 100644 index 0000000000..b6ce55439b --- /dev/null +++ b/app/code/core/Mage/Catalog/Model/Product/Attribute/Backend/Groupprice/Abstract.php @@ -0,0 +1,369 @@ + + */ +abstract class Mage_Catalog_Model_Product_Attribute_Backend_Groupprice_Abstract + extends Mage_Catalog_Model_Product_Attribute_Backend_Price +{ + /** + * Website currency codes and rates + * + * @var array + */ + protected $_rates; + + /** + * Error message when duplicates + * + * @abstract + * @return string + */ + abstract protected function _getDuplicateErrorMessage(); + + /** + * Retrieve websites currency rates and base currency codes + * + * @return array + */ + protected function _getWebsiteCurrencyRates() + { + if (is_null($this->_rates)) { + $this->_rates = array(); + $baseCurrency = Mage::app()->getBaseCurrencyCode(); + foreach (Mage::app()->getWebsites() as $website) { + /* @var $website Mage_Core_Model_Website */ + if ($website->getBaseCurrencyCode() != $baseCurrency) { + $rate = Mage::getModel('directory/currency') + ->load($baseCurrency) + ->getRate($website->getBaseCurrencyCode()); + if (!$rate) { + $rate = 1; + } + $this->_rates[$website->getId()] = array( + 'code' => $website->getBaseCurrencyCode(), + 'rate' => $rate + ); + } else { + $this->_rates[$website->getId()] = array( + 'code' => $baseCurrency, + 'rate' => 1 + ); + } + } + } + return $this->_rates; + } + + /** + * Get additional unique fields + * + * @param array $objectArray + * @return array + */ + protected function _getAdditionalUniqueFields($objectArray) + { + return array(); + } + + /** + * Whether group price value fixed or percent of original price + * + * @param Mage_Catalog_Model_Product_Type_Price $priceObject + * @return bool + */ + protected function _isPriceFixed($priceObject) + { + return $priceObject->isGroupPriceFixed(); + } + + /** + * Validate group price data + * + * @param Mage_Catalog_Model_Product $object + * @throws Mage_Core_Exception + * @return bool + */ + public function validate($object) + { + $attribute = $this->getAttribute(); + $priceRows = $object->getData($attribute->getName()); + if (empty($priceRows)) { + return true; + } + + // validate per website + $duplicates = array(); + foreach ($priceRows as $priceRow) { + if (!empty($priceRow['delete'])) { + continue; + } + $compare = join('-', array_merge( + array($priceRow['website_id'], $priceRow['cust_group']), + $this->_getAdditionalUniqueFields($priceRow) + )); + if (isset($duplicates[$compare])) { + Mage::throwException($this->_getDuplicateErrorMessage()); + } + $duplicates[$compare] = true; + } + + // if attribute scope is website and edit in store view scope + // add global group prices for duplicates find + if (!$attribute->isScopeGlobal() && $object->getStoreId()) { + $origGroupPrices = $object->getOrigData($attribute->getName()); + foreach ($origGroupPrices as $price) { + if ($price['website_id'] == 0) { + $compare = join('-', array_merge( + array($price['website_id'], $price['cust_group']), + $this->_getAdditionalUniqueFields($price) + )); + $duplicates[$compare] = true; + } + } + } + + // validate currency + $baseCurrency = Mage::app()->getBaseCurrencyCode(); + $rates = $this->_getWebsiteCurrencyRates(); + foreach ($priceRows as $priceRow) { + if (!empty($priceRow['delete'])) { + continue; + } + if ($priceRow['website_id'] == 0) { + continue; + } + + $globalCompare = join('-', array_merge( + array(0, $priceRow['cust_group']), + $this->_getAdditionalUniqueFields($priceRow) + )); + $websiteCurrency = $rates[$priceRow['website_id']]['code']; + + if ($baseCurrency == $websiteCurrency && isset($duplicates[$globalCompare])) { + Mage::throwException($this->_getDuplicateErrorMessage()); + } + } + + return true; + } + + /** + * Prepare group prices data for website + * + * @param array $priceData + * @param string $productTypeId + * @param int $websiteId + * @return array + */ + public function preparePriceData(array $priceData, $productTypeId, $websiteId) + { + $rates = $this->_getWebsiteCurrencyRates(); + $data = array(); + $price = Mage::getSingleton('catalog/product_type')->priceFactory($productTypeId); + foreach ($priceData as $v) { + $key = join('-', array_merge(array($v['cust_group']), $this->_getAdditionalUniqueFields($v))); + if ($v['website_id'] == $websiteId) { + $data[$key] = $v; + $data[$key]['website_price'] = $v['price']; + } else if ($v['website_id'] == 0 && !isset($data[$key])) { + $data[$key] = $v; + $data[$key]['website_id'] = $websiteId; + if ($this->_isPriceFixed($price)) { + $data[$key]['price'] = $v['price'] * $rates[$websiteId]['rate']; + $data[$key]['website_price'] = $v['price'] * $rates[$websiteId]['rate']; + } + } + } + + return $data; + } + + /** + * Assign group prices to product data + * + * @param Mage_Catalog_Model_Product $object + * @return Mage_Catalog_Model_Product_Attribute_Backend_Groupprice_Abstract + */ + public function afterLoad($object) + { + $storeId = $object->getStoreId(); + $websiteId = null; + if ($this->getAttribute()->isScopeGlobal()) { + $websiteId = 0; + } else if ($storeId) { + $websiteId = Mage::app()->getStore($storeId)->getWebsiteId(); + } + + $data = $this->_getResource()->loadPriceData($object->getId(), $websiteId); + foreach ($data as $k => $v) { + $data[$k]['website_price'] = $v['price']; + if ($v['all_groups']) { + $data[$k]['cust_group'] = Mage_Customer_Model_Group::CUST_GROUP_ALL; + } + } + + if (!$object->getData('_edit_mode') && $websiteId) { + $data = $this->preparePriceData($data, $object->getTypeId(), $websiteId); + } + + $object->setData($this->getAttribute()->getName(), $data); + $object->setOrigData($this->getAttribute()->getName(), $data); + + $valueChangedKey = $this->getAttribute()->getName() . '_changed'; + $object->setOrigData($valueChangedKey, 0); + $object->setData($valueChangedKey, 0); + + return $this; + } + + /** + * After Save Attribute manipulation + * + * @param Mage_Catalog_Model_Product $object + * @return Mage_Catalog_Model_Product_Attribute_Backend_Groupprice_Abstract + */ + public function afterSave($object) + { + $websiteId = Mage::app()->getStore($object->getStoreId())->getWebsiteId(); + $isGlobal = $this->getAttribute()->isScopeGlobal() || $websiteId == 0; + + $priceRows = $object->getData($this->getAttribute()->getName()); + if (empty($priceRows)) { + if ($isGlobal) { + $this->_getResource()->deletePriceData($object->getId()); + } else { + $this->_getResource()->deletePriceData($object->getId(), $websiteId); + } + return $this; + } + + $old = array(); + $new = array(); + + // prepare original data for compare + $origGroupPrices = $object->getOrigData($this->getAttribute()->getName()); + if (!is_array($origGroupPrices)) { + $origGroupPrices = array(); + } + foreach ($origGroupPrices as $data) { + if ($data['website_id'] > 0 || ($data['website_id'] == '0' && $isGlobal)) { + $key = join('-', array_merge( + array($data['website_id'], $data['cust_group']), + $this->_getAdditionalUniqueFields($data) + )); + $old[$key] = $data; + } + } + + // prepare data for save + foreach ($priceRows as $data) { + $hasEmptyData = false; + foreach ($this->_getAdditionalUniqueFields($data) as $field) { + if (empty($field)) { + $hasEmptyData = true; + break; + } + } + + if ($hasEmptyData || !isset($data['cust_group']) || !empty($data['delete'])) { + continue; + } + if ($this->getAttribute()->isScopeGlobal() && $data['website_id'] > 0) { + continue; + } + if (!$isGlobal && (int)$data['website_id'] == 0) { + continue; + } + + $key = join('-', array_merge( + array($data['website_id'], $data['cust_group']), + $this->_getAdditionalUniqueFields($data) + )); + + $useForAllGroups = $data['cust_group'] == Mage_Customer_Model_Group::CUST_GROUP_ALL; + $customerGroupId = !$useForAllGroups ? $data['cust_group'] : 0; + + $new[$key] = array_merge(array( + 'website_id' => $data['website_id'], + 'all_groups' => $useForAllGroups ? 1 : 0, + 'customer_group_id' => $customerGroupId, + 'value' => $data['price'], + ), $this->_getAdditionalUniqueFields($data)); + } + + $delete = array_diff_key($old, $new); + $insert = array_diff_key($new, $old); + $update = array_intersect_key($new, $old); + + $isChanged = false; + $productId = $object->getId(); + + if (!empty($delete)) { + foreach ($delete as $data) { + $this->_getResource()->deletePriceData($productId, null, $data['price_id']); + $isChanged = true; + } + } + + if (!empty($insert)) { + foreach ($insert as $data) { + $price = new Varien_Object($data); + $price->setEntityId($productId); + $this->_getResource()->savePriceData($price); + + $isChanged = true; + } + } + + if (!empty($update)) { + foreach ($update as $k => $v) { + if ($old[$k]['price'] != $v['value']) { + $price = new Varien_Object(array( + 'value_id' => $old[$k]['price_id'], + 'value' => $v['value'] + )); + $this->_getResource()->savePriceData($price); + + $isChanged = true; + } + } + } + + if ($isChanged) { + $valueChangedKey = $this->getAttribute()->getName() . '_changed'; + $object->setData($valueChangedKey, 1); + } + + return $this; + } +} diff --git a/app/code/core/Mage/Catalog/Model/Product/Attribute/Backend/Tierprice.php b/app/code/core/Mage/Catalog/Model/Product/Attribute/Backend/Tierprice.php index 6d1ac5d71e..aa0b2b0e4a 100644 --- a/app/code/core/Mage/Catalog/Model/Product/Attribute/Backend/Tierprice.php +++ b/app/code/core/Mage/Catalog/Model/Product/Attribute/Backend/Tierprice.php @@ -32,15 +32,9 @@ * @package Mage_Catalog * @author Magento Core Team */ -class Mage_Catalog_Model_Product_Attribute_Backend_Tierprice extends Mage_Catalog_Model_Product_Attribute_Backend_Price +class Mage_Catalog_Model_Product_Attribute_Backend_Tierprice + extends Mage_Catalog_Model_Product_Attribute_Backend_Groupprice_Abstract { - /** - * Website currency codes and rates - * - * @var array - */ - protected $_rates; - /** * Retrieve resource instance * @@ -54,278 +48,45 @@ protected function _getResource() /** * Retrieve websites rates and base currency codes * + * @deprecated since 1.12.0.0 * @return array */ public function _getWebsiteRates() { - if (is_null($this->_rates)) { - $this->_rates = array(); - $baseCurrency = Mage::app()->getBaseCurrencyCode(); - foreach (Mage::app()->getWebsites() as $website) { - /* @var $website Mage_Core_Model_Website */ - if ($website->getBaseCurrencyCode() != $baseCurrency) { - $rate = Mage::getModel('directory/currency') - ->load($baseCurrency) - ->getRate($website->getBaseCurrencyCode()); - if (!$rate) { - $rate = 1; - } - $this->_rates[$website->getId()] = array( - 'code' => $website->getBaseCurrencyCode(), - 'rate' => $rate - ); - } else { - $this->_rates[$website->getId()] = array( - 'code' => $baseCurrency, - 'rate' => 1 - ); - } - } - } - return $this->_rates; - } - - /** - * Validate tier price data - * - * @param Mage_Catalog_Model_Product $object - * @throws Mage_Core_Exception - * @return bool - */ - public function validate($object) - { - $attribute = $this->getAttribute(); - $tiers = $object->getData($attribute->getName()); - if (empty($tiers)) { - return true; - } - - // validate per website - $duplicates = array(); - foreach ($tiers as $tier) { - if (!empty($tier['delete'])) { - continue; - } - $compare = join('-', array($tier['website_id'], $tier['cust_group'], $tier['price_qty'] * 1)); - if (isset($duplicates[$compare])) { - Mage::throwException( - Mage::helper('catalog')->__('Duplicate website tier price customer group and quantity.') - ); - } - $duplicates[$compare] = true; - } - - // if attribute scope is website and edit in store view scope - // add global tier prices for duplicates find - if (!$attribute->isScopeGlobal() && $object->getStoreId()) { - $origTierPrices = $object->getOrigData($attribute->getName()); - foreach ($origTierPrices as $tier) { - if ($tier['website_id'] == 0) { - $compare = join('-', array($tier['website_id'], $tier['cust_group'], $tier['price_qty'] * 1)); - $duplicates[$compare] = true; - } - } - } - - // validate currency - $baseCurrency = Mage::app()->getBaseCurrencyCode(); - $rates = $this->_getWebsiteRates(); - foreach ($tiers as $tier) { - if (!empty($tier['delete'])) { - continue; - } - if ($tier['website_id'] == 0) { - continue; - } - - $compare = join('-', array($tier['website_id'], $tier['cust_group'], $tier['price_qty'])); - $globalCompare = join('-', array(0, $tier['cust_group'], $tier['price_qty'] * 1)); - $websiteCurrency = $rates[$tier['website_id']]['code']; - - if ($baseCurrency == $websiteCurrency && isset($duplicates[$globalCompare])) { - Mage::throwException( - Mage::helper('catalog')->__('Duplicate website tier price customer group and quantity.') - ); - } - } - - return true; + return $this->_getWebsiteCurrencyRates(); } /** - * Prepare tier prices data for website + * Add price qty to unique fields * - * @param array $priceData - * @param string $productTypeId - * @param int $websiteId + * @param array $objectArray * @return array */ - public function preparePriceData(array $priceData, $productTypeId, $websiteId) + protected function _getAdditionalUniqueFields($objectArray) { - $rates = $this->_getWebsiteRates(); - $data = array(); - $price = Mage::getSingleton('catalog/product_type')->priceFactory($productTypeId); - foreach ($priceData as $v) { - $key = join('-', array($v['cust_group'], $v['price_qty'])); - if ($v['website_id'] == $websiteId) { - $data[$key] = $v; - $data[$key]['website_price'] = $v['price']; - } else if ($v['website_id'] == 0 && !isset($data[$key])) { - $data[$key] = $v; - $data[$key]['website_id'] = $websiteId; - if ($price->isTierPriceFixed()) { - $data[$key]['price'] = $v['price'] * $rates[$websiteId]['rate']; - $data[$key]['website_price'] = $v['price'] * $rates[$websiteId]['rate']; - } - } - } - - return $data; + $uniqueFields = parent::_getAdditionalUniqueFields($objectArray); + $uniqueFields['qty'] = $objectArray['price_qty'] * 1; + return $uniqueFields; } /** - * Assign tier prices to product data + * Error message when duplicates * - * @param Mage_Catalog_Model_Product $object - * @return Mage_Catalog_Model_Product_Attribute_Backend_Tierprice + * @return string */ - public function afterLoad($object) + protected function _getDuplicateErrorMessage() { - $storeId = $object->getStoreId(); - $websiteId = null; - if ($this->getAttribute()->isScopeGlobal()) { - $websiteId = 0; - } else if ($storeId) { - $websiteId = Mage::app()->getStore($storeId)->getWebsiteId(); - } - - $data = $this->_getResource()->loadPriceData($object->getId(), $websiteId); - foreach ($data as $k => $v) { - $data[$k]['website_price'] = $v['price']; - if ($v['all_groups']) { - $data[$k]['cust_group'] = Mage_Customer_Model_Group::CUST_GROUP_ALL; - } - } - - if (!$object->getData('_edit_mode') && $websiteId) { - $data = $this->preparePriceData($data, $object->getTypeId(), $websiteId); - } - - $object->setData($this->getAttribute()->getName(), $data); - $object->setOrigData($this->getAttribute()->getName(), $data); - - $valueChangedKey = $this->getAttribute()->getName() . '_changed'; - $object->setOrigData($valueChangedKey, 0); - $object->setData($valueChangedKey, 0); - - return $this; + return Mage::helper('catalog')->__('Duplicate website tier price customer group and quantity.'); } /** - * After Save Attribute manipulation + * Whether tier price value fixed or percent of original price * - * @param Mage_Catalog_Model_Product $object - * @return Mage_Catalog_Model_Product_Attribute_Backend_Tierprice + * @param Mage_Catalog_Model_Product_Type_Price $priceObject + * @return bool */ - public function afterSave($object) + protected function _isPriceFixed($priceObject) { - $websiteId = Mage::app()->getStore($object->getStoreId())->getWebsiteId(); - $isGlobal = $this->getAttribute()->isScopeGlobal() || $websiteId == 0; - - $tierPrices = $object->getData($this->getAttribute()->getName()); - if (empty($tierPrices)) { - if ($isGlobal) { - $this->_getResource()->deletePriceData($object->getId()); - } else { - $this->_getResource()->deletePriceData($object->getId(), $websiteId); - } - return $this; - } - - $old = array(); - $new = array(); - - // prepare original data for compare - $origTierPrices = $object->getOrigData($this->getAttribute()->getName()); - if (!is_array($origTierPrices)) { - $origTierPrices = array(); - } - foreach ($origTierPrices as $data) { - if ($data['website_id'] > 0 || ($data['website_id'] == '0' && $isGlobal)) { - $key = join('-', array($data['website_id'], $data['cust_group'], $data['price_qty'] * 1)); - $old[$key] = $data; - } - } - - // prepare data for save - foreach ($tierPrices as $data) { - if (empty($data['price_qty']) || !isset($data['cust_group']) || !empty($data['delete'])) { - continue; - } - if ($this->getAttribute()->isScopeGlobal() && $data['website_id'] > 0) { - continue; - } - if (!$isGlobal && (int)$data['website_id'] == 0) { - continue; - } - - $key = join('-', array($data['website_id'], $data['cust_group'], $data['price_qty'] * 1)); - - $useForAllGroups = $data['cust_group'] == Mage_Customer_Model_Group::CUST_GROUP_ALL; - $customerGroupId = !$useForAllGroups ? $data['cust_group'] : 0; - - $new[$key] = array( - 'website_id' => $data['website_id'], - 'all_groups' => $useForAllGroups ? 1 : 0, - 'customer_group_id' => $customerGroupId, - 'qty' => $data['price_qty'], - 'value' => $data['price'], - ); - } - - $delete = array_diff_key($old, $new); - $insert = array_diff_key($new, $old); - $update = array_intersect_key($new, $old); - - $isChanged = false; - $productId = $object->getId(); - - if (!empty($delete)) { - foreach ($delete as $data) { - $this->_getResource()->deletePriceData($productId, null, $data['price_id']); - $isChanged = true; - } - } - - if (!empty($insert)) { - foreach ($insert as $data) { - $price = new Varien_Object($data); - $price->setEntityId($productId); - $this->_getResource()->savePriceData($price); - - $isChanged = true; - } - } - - if (!empty($update)) { - foreach ($update as $k => $v) { - if ($old[$k]['price'] != $v['value']) { - $price = new Varien_Object(array( - 'value_id' => $old[$k]['price_id'], - 'value' => $v['value'] - )); - $this->_getResource()->savePriceData($price); - - $isChanged = true; - } - } - } - - if ($isChanged) { - $valueChangedKey = $this->getAttribute()->getName() . '_changed'; - $object->setData($valueChangedKey, 1); - } - - return $this; + return $priceObject->isTierPriceFixed(); } } diff --git a/app/code/core/Mage/Catalog/Model/Product/Attribute/Source/Countryofmanufacture.php b/app/code/core/Mage/Catalog/Model/Product/Attribute/Source/Countryofmanufacture.php index 54f19c66c2..2efb512a13 100644 --- a/app/code/core/Mage/Catalog/Model/Product/Attribute/Source/Countryofmanufacture.php +++ b/app/code/core/Mage/Catalog/Model/Product/Attribute/Source/Countryofmanufacture.php @@ -41,13 +41,11 @@ class Mage_Catalog_Model_Product_Attribute_Source_Countryofmanufacture */ public function getAllOptions() { - $cacheKey = 'DIRECTORY_COUNTRY_SELECT_STORE_'.Mage::app()->getStore()->getCode(); + $cacheKey = 'DIRECTORY_COUNTRY_SELECT_STORE_' . Mage::app()->getStore()->getCode(); if (Mage::app()->useCache('config') && $cache = Mage::app()->loadCache($cacheKey)) { $options = unserialize($cache); - } - else { - $collection = Mage::getModel('directory/country')->getResourceCollection() - ->loadByStore(); + } else { + $collection = Mage::getModel('directory/country')->getResourceCollection(); $options = $collection->toOptionArray(); if (Mage::app()->useCache('config')) { Mage::app()->saveCache(serialize($options), $cacheKey, array('config')); diff --git a/app/code/core/Mage/Catalog/Model/Product/Flat/Indexer.php b/app/code/core/Mage/Catalog/Model/Product/Flat/Indexer.php index b2f9ffdd7a..2221908129 100644 --- a/app/code/core/Mage/Catalog/Model/Product/Flat/Indexer.php +++ b/app/code/core/Mage/Catalog/Model/Product/Flat/Indexer.php @@ -53,6 +53,16 @@ */ class Mage_Catalog_Model_Product_Flat_Indexer extends Mage_Core_Model_Abstract { + /** + * Catalog product flat entity for indexers + */ + const ENTITY = 'catalog_product_flat'; + + /** + * Indexers rebuild event type + */ + const EVENT_TYPE_REBUILD = 'catalog_product_flat_rebuild'; + /** * Standart model resource initialization * @@ -70,7 +80,16 @@ protected function _construct() */ public function rebuild($store = null) { - $this->_getResource()->rebuild($store); + if (is_null($store)) { + $this->_getResource()->prepareFlatTables(); + } else { + $this->_getResource()->prepareFlatTable($store); + } + Mage::getSingleton('index/indexer')->processEntityAction( + new Varien_Object(array('id' => $store)), + self::ENTITY, + self::EVENT_TYPE_REBUILD + ); return $this; } @@ -218,7 +237,7 @@ public function saveProduct($productIds, $store = null) } return $this; } - + $resource = $this->_getResource(); $resource->beginTransaction(); try { @@ -266,4 +285,25 @@ public function deleteStore($store) $this->_getResource()->deleteFlatTable($store); return $this; } + + /** + * Rebuild Catalog Product Flat Data for all stores + * + * @return Mage_Catalog_Model_Product_Flat_Indexer + */ + public function reindexAll() + { + $this->_getResource()->reindexAll(); + return $this; + } + + /** + * Retrieve list of attribute codes for flat + * + * @return array + */ + public function getAttributeCodes() + { + return $this->_getResource()->getAttributeCodes(); + } } diff --git a/app/code/core/Mage/Catalog/Model/Product/Indexer/Flat.php b/app/code/core/Mage/Catalog/Model/Product/Indexer/Flat.php index fe0a10176a..6d0665a4db 100644 --- a/app/code/core/Mage/Catalog/Model/Product/Indexer/Flat.php +++ b/app/code/core/Mage/Catalog/Model/Product/Indexer/Flat.php @@ -25,6 +25,11 @@ */ class Mage_Catalog_Model_Product_Indexer_Flat extends Mage_Index_Model_Indexer_Abstract { + /** + * Data key for matching result to be saved in + */ + const EVENT_MATCH_RESULT_KEY = 'catalog_product_flat_match_result'; + /** * Index math Entities array * @@ -48,7 +53,10 @@ class Mage_Catalog_Model_Product_Indexer_Flat extends Mage_Index_Model_Indexer_A ), Mage_Catalog_Model_Convert_Adapter_Product::ENTITY => array( Mage_Index_Model_Event::TYPE_SAVE - ) + ), + Mage_Catalog_Model_Product_Flat_Indexer::ENTITY => array( + Mage_Catalog_Model_Product_Flat_Indexer::EVENT_TYPE_REBUILD, + ), ); /** @@ -95,34 +103,32 @@ public function matchEvent(Mage_Index_Model_Event $event) return false; } - $data = $event->getNewData(); - $resultKey = 'catalog_product_flat_match_result'; - if (isset($data[$resultKey])) { - return $data[$resultKey]; + $data = $event->getNewData(); + if (isset($data[self::EVENT_MATCH_RESULT_KEY])) { + return $data[self::EVENT_MATCH_RESULT_KEY]; } - $result = null; $entity = $event->getEntity(); if ($entity == Mage_Catalog_Model_Resource_Eav_Attribute::ENTITY) { /* @var $attribute Mage_Catalog_Model_Resource_Eav_Attribute */ $attribute = $event->getDataObject(); $addFilterable = Mage::helper('catalog/product_flat')->isAddFilterableAttributes(); - $enableBefore = ($attribute->getOrigData('backend_type') == 'static') + $enableBefore = $attribute && (($attribute->getOrigData('backend_type') == 'static') || ($addFilterable && $attribute->getOrigData('is_filterable') > 0) || ($attribute->getOrigData('used_in_product_listing') == 1) || ($attribute->getOrigData('is_used_for_promo_rules') == 1) - || ($attribute->getOrigData('used_for_sort_by') == 1); + || ($attribute->getOrigData('used_for_sort_by') == 1)); - $enableAfter = ($attribute->getData('backend_type') == 'static') + $enableAfter = $attribute && (($attribute->getData('backend_type') == 'static') || ($addFilterable && $attribute->getData('is_filterable') > 0) || ($attribute->getData('used_in_product_listing') == 1) || ($attribute->getData('is_used_for_promo_rules') == 1) - || ($attribute->getData('used_for_sort_by') == 1); + || ($attribute->getData('used_for_sort_by') == 1)); - if ($event->getType() == Mage_Index_Model_Event::TYPE_DELETE) { + if ($attribute && $event->getType() == Mage_Index_Model_Event::TYPE_DELETE) { $result = $enableBefore; - } else if ($event->getType() == Mage_Index_Model_Event::TYPE_SAVE) { + } elseif ($attribute && $event->getType() == Mage_Index_Model_Event::TYPE_SAVE) { if ($enableAfter || $enableBefore) { $result = true; } else { @@ -137,7 +143,7 @@ public function matchEvent(Mage_Index_Model_Event $event) } else { /* @var $store Mage_Core_Model_Store */ $store = $event->getDataObject(); - if ($store->isObjectNew()) { + if ($store && $store->isObjectNew()) { $result = true; } else { $result = false; @@ -146,7 +152,7 @@ public function matchEvent(Mage_Index_Model_Event $event) } else if ($entity == Mage_Core_Model_Store_Group::ENTITY) { /* @var $storeGroup Mage_Core_Model_Store_Group */ $storeGroup = $event->getDataObject(); - if ($storeGroup->dataHasChangedFor('website_id')) { + if ($storeGroup && $storeGroup->dataHasChangedFor('website_id')) { $result = true; } else { $result = false; @@ -155,7 +161,7 @@ public function matchEvent(Mage_Index_Model_Event $event) $result = parent::matchEvent($event); } - $event->addNewData($resultKey, $result); + $event->addNewData(self::EVENT_MATCH_RESULT_KEY, $result); return $result; } @@ -167,6 +173,7 @@ public function matchEvent(Mage_Index_Model_Event $event) */ protected function _registerEvent(Mage_Index_Model_Event $event) { + $event->addNewData(self::EVENT_MATCH_RESULT_KEY, true); switch ($event->getEntity()) { case Mage_Catalog_Model_Product::ENTITY: $this->_registerCatalogProductEvent($event); @@ -185,6 +192,12 @@ protected function _registerEvent(Mage_Index_Model_Event $event) $process = $event->getProcess(); $process->changeStatus(Mage_Index_Model_Process::STATUS_REQUIRE_REINDEX); break; + case Mage_Catalog_Model_Product_Flat_Indexer::ENTITY: + switch ($event->getType()) { + case Mage_Catalog_Model_Product_Flat_Indexer::EVENT_TYPE_REBUILD: + $event->addNewData('id', $event->getDataObject()->getId()); + } + break; } } @@ -192,7 +205,7 @@ protected function _registerEvent(Mage_Index_Model_Event $event) * Register data required by catalog product process in event object * * @param Mage_Index_Model_Event $event - * @return Mage_CatalogSearch_Model_Indexer_Search + * @return Mage_Catalog_Model_Product_Indexer_Flat */ protected function _registerCatalogProductEvent(Mage_Index_Model_Event $event) { @@ -224,6 +237,16 @@ protected function _registerCatalogProductEvent(Mage_Index_Model_Event $event) $reindexData['catalog_product_flat_action_type'] = $actionObject->getActionType(); } + $flatAttributes = array(); + if (is_array($attrData)) { + $flatAttributes = array_intersect($this->_getFlatAttributes(), array_keys($attrData)); + } + + if (count($flatAttributes) > 0) { + $reindexFlat = true; + $reindexData['catalog_product_flat_force_update'] = true; + } + // register affected products if ($reindexFlat) { $reindexData['catalog_product_flat_product_ids'] = $actionObject->getProductIds(); @@ -261,6 +284,12 @@ protected function _registerCoreStoreEvent(Mage_Index_Model_Event $event) protected function _processEvent(Mage_Index_Model_Event $event) { $data = $event->getNewData(); + if ($event->getType() == Mage_Catalog_Model_Product_Flat_Indexer::EVENT_TYPE_REBUILD) { + $this->_getIndexer()->getResource()->rebuild($data['id']); + return; + } + + if (!empty($data['catalog_product_flat_reindex_all'])) { $this->reindexAll(); } else if (!empty($data['catalog_product_flat_product_id'])) { @@ -289,6 +318,10 @@ protected function _processEvent(Mage_Index_Model_Event $event) $status = $data['catalog_product_flat_status']; $this->_getIndexer()->updateProductStatus($productIds, $status); } + + if (isset($data['catalog_product_flat_force_update'])) { + $this->_getIndexer()->updateProduct($productIds); + } } else if (!empty($data['catalog_product_flat_delete_store_id'])) { $this->_getIndexer()->deleteStore($data['catalog_product_flat_delete_store_id']); } @@ -300,6 +333,16 @@ protected function _processEvent(Mage_Index_Model_Event $event) */ public function reindexAll() { - $this->_getIndexer()->rebuild(); + $this->_getIndexer()->reindexAll(); + } + + /** + * Retrieve list of attribute codes, that are used in flat + * + * @return array + */ + protected function _getFlatAttributes() + { + return Mage::getModel('catalog/product_flat_indexer')->getAttributeCodes(); } } diff --git a/app/code/core/Mage/Catalog/Model/Product/Indexer/Price.php b/app/code/core/Mage/Catalog/Model/Product/Indexer/Price.php index 15a11bf922..95f726c236 100644 --- a/app/code/core/Mage/Catalog/Model/Product/Indexer/Price.php +++ b/app/code/core/Mage/Catalog/Model/Product/Indexer/Price.php @@ -52,6 +52,16 @@ */ class Mage_Catalog_Model_Product_Indexer_Price extends Mage_Index_Model_Indexer_Abstract { + /** + * Data key for matching result to be saved in + */ + const EVENT_MATCH_RESULT_KEY = 'catalog_product_price_match_result'; + + /** + * Reindex price event type + */ + const EVENT_TYPE_REINDEX_PRICE = 'catalog_reindex_price'; + /** * Matched Entities instruction array * @@ -62,6 +72,7 @@ class Mage_Catalog_Model_Product_Indexer_Price extends Mage_Index_Model_Indexer_ Mage_Index_Model_Event::TYPE_SAVE, Mage_Index_Model_Event::TYPE_DELETE, Mage_Index_Model_Event::TYPE_MASS_ACTION, + self::EVENT_TYPE_REINDEX_PRICE, ), Mage_Core_Model_Config_Data::ENTITY => array( Mage_Index_Model_Event::TYPE_SAVE @@ -137,26 +148,24 @@ protected function _getDependentAttributes() public function matchEvent(Mage_Index_Model_Event $event) { $data = $event->getNewData(); - $resultKey = 'catalog_product_price_match_result'; - if (isset($data[$resultKey])) { - return $data[$resultKey]; + if (isset($data[self::EVENT_MATCH_RESULT_KEY])) { + return $data[self::EVENT_MATCH_RESULT_KEY]; } - $result = null; if ($event->getEntity() == Mage_Core_Model_Config_Data::ENTITY) { $data = $event->getDataObject(); - if (in_array($data->getPath(), $this->_relatedConfigSettings)) { + if ($data && in_array($data->getPath(), $this->_relatedConfigSettings)) { $result = $data->isValueChanged(); } else { $result = false; } } elseif ($event->getEntity() == Mage_Customer_Model_Group::ENTITY) { - $result = $event->getDataObject()->isObjectNew(); + $result = $event->getDataObject() && $event->getDataObject()->isObjectNew(); } else { $result = parent::matchEvent($event); } - $event->addNewData($resultKey, $result); + $event->addNewData(self::EVENT_MATCH_RESULT_KEY, $result); return $result; } @@ -238,6 +247,7 @@ protected function _registerCatalogProductMassActionEvent(Mage_Index_Model_Event */ protected function _registerEvent(Mage_Index_Model_Event $event) { + $event->addNewData(self::EVENT_MATCH_RESULT_KEY, true); $entity = $event->getEntity(); if ($entity == Mage_Core_Model_Config_Data::ENTITY || $entity == Mage_Customer_Model_Group::ENTITY) { @@ -258,6 +268,9 @@ protected function _registerEvent(Mage_Index_Model_Event $event) case Mage_Index_Model_Event::TYPE_MASS_ACTION: $this->_registerCatalogProductMassActionEvent($event); break; + case self::EVENT_TYPE_REINDEX_PRICE: + $event->addNewData('id', $event->getDataObject()->getId()); + break; } // call product type indexers registerEvent @@ -276,6 +289,10 @@ protected function _registerEvent(Mage_Index_Model_Event $event) protected function _processEvent(Mage_Index_Model_Event $event) { $data = $event->getNewData(); + if ($event->getType() == self::EVENT_TYPE_REINDEX_PRICE) { + $this->_getResource()->reindexProductIds($data['id']); + return; + } if (!empty($data['catalog_product_price_reindex_all'])) { $this->reindexAll(); } diff --git a/app/code/core/Mage/Catalog/Model/Product/Option/Type/File.php b/app/code/core/Mage/Catalog/Model/Product/Option/Type/File.php index 0e45a3aa44..2ce93af2ac 100644 --- a/app/code/core/Mage/Catalog/Model/Product/Option/Type/File.php +++ b/app/code/core/Mage/Catalog/Model/Product/Option/Type/File.php @@ -415,19 +415,15 @@ protected function _getValidatorErrors($errors, $fileInfo) $result = array(); foreach ($errors as $errorCode) { if ($errorCode == Zend_Validate_File_ExcludeExtension::FALSE_EXTENSION) { - $result[] = Mage::helper('catalog')->__("The file '%s' for '%s' has an invalid extension", - $fileInfo['title'], $option->getTitle()); + $result[] = Mage::helper('catalog')->__("The file '%s' for '%s' has an invalid extension", $fileInfo['title'], $option->getTitle()); } elseif ($errorCode == Zend_Validate_File_Extension::FALSE_EXTENSION) { - $result[] = Mage::helper('catalog')->__("The file '%s' for '%s' has an invalid extension", - $fileInfo['title'], $option->getTitle()); + $result[] = Mage::helper('catalog')->__("The file '%s' for '%s' has an invalid extension", $fileInfo['title'], $option->getTitle()); } elseif ($errorCode == Zend_Validate_File_ImageSize::WIDTH_TOO_BIG || $errorCode == Zend_Validate_File_ImageSize::HEIGHT_TOO_BIG) { - $result[] = Mage::helper('catalog')->__("Maximum allowed image size for '%s' is %sx%s px.", - $option->getTitle(), $option->getImageSizeX(), $option->getImageSizeY()); + $result[] = Mage::helper('catalog')->__("Maximum allowed image size for '%s' is %sx%s px.", $option->getTitle(), $option->getImageSizeX(), $option->getImageSizeY()); } elseif ($errorCode == Zend_Validate_File_FilesSize::TOO_BIG) { - $result[] = Mage::helper('catalog')->__("The file '%s' you uploaded is larger than %s Megabytes allowed by server", - $fileInfo['title'], $this->_bytesToMbytes($this->_getUploadMaxFilesize())); + $result[] = Mage::helper('catalog')->__("The file '%s' you uploaded is larger than %s Megabytes allowed by server", $fileInfo['title'], $this->_bytesToMbytes($this->_getUploadMaxFilesize())); } } return $result; @@ -513,7 +509,9 @@ protected function _getOptionHtml($optionValue) { $value = $this->_unserializeValue($optionValue); try { - if (isset($value) && isset($value['width']) && isset($value['height']) && $value['width'] > 0 && $value['height'] > 0) { + if (isset($value) && isset($value['width']) && isset($value['height']) + && $value['width'] > 0 && $value['height'] > 0 + ) { $sizes = $value['width'] . ' x ' . $value['height'] . ' ' . Mage::helper('catalog')->__('px.'); } else { $sizes = ''; diff --git a/app/code/core/Mage/Catalog/Model/Product/Type/Price.php b/app/code/core/Mage/Catalog/Model/Product/Type/Price.php index 830e76ee84..147a63e3c2 100644 --- a/app/code/core/Mage/Catalog/Model/Product/Type/Price.php +++ b/app/code/core/Mage/Catalog/Model/Product/Type/Price.php @@ -48,24 +48,26 @@ public function getPrice($product) } /** - * Get product final price + * Retrieve product final price * - * @param double $qty - * @param Mage_Catalog_Model_Product $product - * @return double + * @param float|null $qty + * @param Mage_Catalog_Model_Product $product + * @return float */ - public function getFinalPrice($qty=null, $product) + public function getFinalPrice($qty = null, $product) { if (is_null($qty) && !is_null($product->getCalculatedFinalPrice())) { return $product->getCalculatedFinalPrice(); } $finalPrice = $product->getPrice(); + + $finalPrice = $this->_applyGroupPrice($product, $finalPrice); $finalPrice = $this->_applyTierPrice($product, $qty, $finalPrice); $finalPrice = $this->_applySpecialPrice($product, $finalPrice); $product->setFinalPrice($finalPrice); - Mage::dispatchEvent('catalog_product_get_final_price', array('product'=>$product, 'qty' => $qty)); + Mage::dispatchEvent('catalog_product_get_final_price', array('product' => $product, 'qty' => $qty)); $finalPrice = $product->getData('final_price'); $finalPrice = $this->_applyOptionsPrice($product, $qty, $finalPrice); @@ -78,6 +80,58 @@ public function getChildFinalPrice($product, $productQty, $childProduct, $childP return $this->getFinalPrice($childProductQty, $childProduct); } + /** + * Apply group price for product + * + * @param Mage_Catalog_Model_Product $product + * @param float $finalPrice + * @return float + */ + protected function _applyGroupPrice($product, $finalPrice) + { + $groupPrice = $product->getGroupPrice(); + if (is_numeric($groupPrice)) { + $finalPrice = min($finalPrice, $groupPrice); + } + return $finalPrice; + } + + /** + * Get product group price + * + * @param Mage_Catalog_Model_Product $product + * @return float + */ + public function getGroupPrice($product) + { + + $groupPrices = $product->getData('group_price'); + + if (is_null($groupPrices)) { + $attribute = $product->getResource()->getAttribute('group_price'); + if ($attribute) { + $attribute->getBackend()->afterLoad($product); + $groupPrices = $product->getData('group_price'); + } + } + + if (is_null($groupPrices) || !is_array($groupPrices)) { + return $product->getPrice(); + } + + $customerGroup = $this->_getCustomerGroupId($product); + + $matchedPrice = $product->getPrice(); + foreach ($groupPrices as $groupPrice) { + if ($groupPrice['cust_group'] == $customerGroup && $groupPrice['website_price'] < $matchedPrice) { + $matchedPrice = $groupPrice['website_price']; + break; + } + } + + return $matchedPrice; + } + /** * Apply tier price for product if not return price that was before * @@ -347,6 +401,16 @@ public static function calculateSpecialPrice($finalPrice, $specialPrice, $specia * @return bool */ public function isTierPriceFixed() + { + return $this->isGroupPriceFixed(); + } + + /** + * Check is group price value fixed or percent of original price + * + * @return bool + */ + public function isGroupPriceFixed() { return true; } diff --git a/app/code/core/Mage/Catalog/Model/Resource/Abstract.php b/app/code/core/Mage/Catalog/Model/Resource/Abstract.php index 75c9eabf17..0152ccb744 100755 --- a/app/code/core/Mage/Catalog/Model/Resource/Abstract.php +++ b/app/code/core/Mage/Catalog/Model/Resource/Abstract.php @@ -202,7 +202,7 @@ protected function _setAttributeValue($object, $valueRow) if (!$isDefaultStore) { $object->setExistsStoreValueFlag($attributeCode); } - $attribute->getBackend()->setValueId($valueId); + $attribute->getBackend()->setEntityValueId($object, $valueId); } return $this; diff --git a/app/code/core/Mage/Catalog/Model/Resource/Category/Flat.php b/app/code/core/Mage/Catalog/Model/Resource/Category/Flat.php index fa15c8abca..06ac767fbf 100755 --- a/app/code/core/Mage/Catalog/Model/Resource/Category/Flat.php +++ b/app/code/core/Mage/Catalog/Model/Resource/Category/Flat.php @@ -32,7 +32,7 @@ * @package Mage_Catalog * @author Magento Core Team */ -class Mage_Catalog_Model_Resource_Category_Flat extends Mage_Core_Model_Resource_Db_Abstract +class Mage_Catalog_Model_Resource_Category_Flat extends Mage_Index_Model_Resource_Abstract { /** * Store id @@ -97,6 +97,13 @@ class Mage_Catalog_Model_Resource_Category_Flat extends Mage_Core_Model_Resource */ protected $_storesRootCategories; + /** + * Whether table changes are allowed + * + * @var bool + */ + protected $_allowTableChanges = true; + /** * Resource initializations * @@ -462,7 +469,9 @@ public function rebuild($stores = null) $categoriesIds = array(); /* @var $store Mage_Core_Model_Store */ foreach ($stores as $store) { - $this->_createTable($store->getId()); + if ($this->_allowTableChanges) { + $this->_createTable($store->getId()); + } if (!isset($categories[$store->getRootCategoryId()])) { $select = $this->_getWriteAdapter()->select() @@ -1389,4 +1398,48 @@ public function getStoresRootCategories($storeIds = null) return $this->_storesRootCategories; } + + /** + * Creating table and adding attributes as fields to table for all stores + * + * @return Mage_Catalog_Model_Resource_Category_Flat + */ + protected function _createTables() + { + if ($this->_allowTableChanges) { + foreach (Mage::app()->getStores() as $store) { + $this->_createTable($store->getId()); + } + } + return $this; + } + + /** + * Transactional rebuild flat data from eav + * + * @return Mage_Catalog_Model_Resource_Category_Flat + */ + public function reindexAll() + { + $this->_createTables(); + $allowTableChanges = $this->_allowTableChanges; + if ($allowTableChanges) { + $this->_allowTableChanges = false; + } + $this->beginTransaction(); + try { + $this->rebuild(); + $this->commit(); + if ($allowTableChanges) { + $this->_allowTableChanges = true; + } + } catch (Exception $e) { + $this->rollBack(); + if ($allowTableChanges) { + $this->_allowTableChanges = true; + } + throw $e; + } + return $this; + } } diff --git a/app/code/core/Mage/Catalog/Model/Resource/Category/Indexer/Product.php b/app/code/core/Mage/Catalog/Model/Resource/Category/Indexer/Product.php index 5f7a5df146..621494629e 100755 --- a/app/code/core/Mage/Catalog/Model/Resource/Category/Indexer/Product.php +++ b/app/code/core/Mage/Catalog/Model/Resource/Category/Indexer/Product.php @@ -773,168 +773,179 @@ protected function _getStatusAttributeInfo() public function reindexAll() { $this->useIdxTable(true); - $this->clearTemporaryIndexTable(); - $idxTable = $this->getIdxTable(); - $idxAdapter = $this->_getIndexAdapter(); - $stores = $this->_getStoresInfo(); - /** - * Build index for each store - */ - foreach ($stores as $storeData) { - $storeId = $storeData['store_id']; - $websiteId = $storeData['website_id']; - $rootPath = $storeData['root_path']; - $rootId = $storeData['root_id']; + $this->beginTransaction(); + try { + $this->clearTemporaryIndexTable(); + $idxTable = $this->getIdxTable(); + $idxAdapter = $this->_getIndexAdapter(); + $stores = $this->_getStoresInfo(); /** - * Prepare visibility for all enabled store products + * Build index for each store */ - $enabledTable = $this->_prepareEnabledProductsVisibility($websiteId, $storeId); - /** - * Select information about anchor categories - */ - $anchorTable = $this->_prepareAnchorCategories($storeId, $rootPath); - /** - * Add relations between not anchor categories and products - */ - $select = $idxAdapter->select(); - /** @var $select Varien_Db_Select */ - $select->from( - array('cp' => $this->_categoryProductTable), - array('category_id', 'product_id', 'position', 'is_parent' => new Zend_Db_Expr('1'), - 'store_id' => new Zend_Db_Expr($storeId)) - ) - ->joinInner(array('pv' => $enabledTable), 'pv.product_id=cp.product_id', array('visibility')) - ->joinLeft(array('ac' => $anchorTable), 'ac.category_id=cp.category_id', array()) - ->where('ac.category_id IS NULL'); - - $query = $select->insertFromSelect( - $idxTable, - array('category_id', 'product_id', 'position', 'is_parent', 'store_id', 'visibility'), - false - ); - $idxAdapter->query($query); + foreach ($stores as $storeData) { + $storeId = $storeData['store_id']; + $websiteId = $storeData['website_id']; + $rootPath = $storeData['root_path']; + $rootId = $storeData['root_id']; + /** + * Prepare visibility for all enabled store products + */ + $enabledTable = $this->_prepareEnabledProductsVisibility($websiteId, $storeId); + /** + * Select information about anchor categories + */ + $anchorTable = $this->_prepareAnchorCategories($storeId, $rootPath); + /** + * Add relations between not anchor categories and products + */ + $select = $idxAdapter->select(); + /** @var $select Varien_Db_Select */ + $select->from( + array('cp' => $this->_categoryProductTable), + array('category_id', 'product_id', 'position', 'is_parent' => new Zend_Db_Expr('1'), + 'store_id' => new Zend_Db_Expr($storeId)) + ) + ->joinInner(array('pv' => $enabledTable), 'pv.product_id=cp.product_id', array('visibility')) + ->joinLeft(array('ac' => $anchorTable), 'ac.category_id=cp.category_id', array()) + ->where('ac.category_id IS NULL'); + + $query = $select->insertFromSelect( + $idxTable, + array('category_id', 'product_id', 'position', 'is_parent', 'store_id', 'visibility'), + false + ); + $idxAdapter->query($query); - /** - * Assign products not associated to any category to root category in index - */ + /** + * Assign products not associated to any category to root category in index + */ - $select = $idxAdapter->select(); - $select->from( - array('pv' => $enabledTable), - array(new Zend_Db_Expr($rootId), 'product_id', new Zend_Db_Expr('0'), new Zend_Db_Expr('1'), - new Zend_Db_Expr($storeId), 'visibility') - ) - ->joinLeft(array('cp' => $this->_categoryProductTable), 'pv.product_id=cp.product_id', array()) - ->where('cp.product_id IS NULL'); + $select = $idxAdapter->select(); + $select->from( + array('pv' => $enabledTable), + array(new Zend_Db_Expr($rootId), 'product_id', new Zend_Db_Expr('0'), new Zend_Db_Expr('1'), + new Zend_Db_Expr($storeId), 'visibility') + ) + ->joinLeft(array('cp' => $this->_categoryProductTable), 'pv.product_id=cp.product_id', array()) + ->where('cp.product_id IS NULL'); - $query = $select->insertFromSelect( - $idxTable, - array('category_id', 'product_id', 'position', 'is_parent', 'store_id', 'visibility'), - false - ); - $idxAdapter->query($query); + $query = $select->insertFromSelect( + $idxTable, + array('category_id', 'product_id', 'position', 'is_parent', 'store_id', 'visibility'), + false + ); + $idxAdapter->query($query); + + /** + * Prepare anchor categories products + */ + $anchorProductsTable = $this->_getAnchorCategoriesProductsTemporaryTable(); + $idxAdapter->delete($anchorProductsTable); + + $position = 'MIN('. + $idxAdapter->getCheckSql( + 'ca.category_id = ce.entity_id', + $idxAdapter->quoteIdentifier('cp.position'), + '('.$idxAdapter->quoteIdentifier('ce.position').' + 1) * ' + .'('.$idxAdapter->quoteIdentifier('ce.level').' + 1 * 10000)' + .' + '.$idxAdapter->quoteIdentifier('cp.position') + ) + .')'; - /** - * Prepare anchor categories products - */ - $anchorProductsTable = $this->_getAnchorCategoriesProductsTemporaryTable(); - $idxAdapter->delete($anchorProductsTable); - $position = 'MIN('. - $idxAdapter->getCheckSql( - 'ca.category_id = ce.entity_id', - $idxAdapter->quoteIdentifier('cp.position'), - '('.$idxAdapter->quoteIdentifier('ce.position').' + 1) * ' - .'('.$idxAdapter->quoteIdentifier('ce.level').' + 1 * 10000)' - .' + '.$idxAdapter->quoteIdentifier('cp.position') + $select = $idxAdapter->select() + ->useStraightJoin(true) + ->distinct(true) + ->from(array('ca' => $anchorTable), array('category_id')) + ->joinInner( + array('ce' => $this->_categoryTable), + $idxAdapter->quoteIdentifier('ce.path') . ' LIKE ' . + $idxAdapter->quoteIdentifier('ca.path') . ' OR ce.entity_id = ca.category_id', + array() ) - .')'; + ->joinInner( + array('cp' => $this->_categoryProductTable), + 'cp.category_id = ce.entity_id', + array('product_id') + ) + ->joinInner( + array('pv' => $enabledTable), + 'pv.product_id = cp.product_id', + array('position' => $position) + ) + ->group(array('ca.category_id', 'cp.product_id')); + $query = $select->insertFromSelect($anchorProductsTable, + array('category_id', 'product_id', 'position'), false); + $idxAdapter->query($query); + + /** + * Add anchor categories products to index + */ + $select = $idxAdapter->select() + ->from( + array('ap' => $anchorProductsTable), + array('category_id', 'product_id', + 'position', // => new Zend_Db_Expr('MIN('. $idxAdapter->quoteIdentifier('ap.position').')'), + 'is_parent' => $idxAdapter->getCheckSql('cp.product_id > 0', 1, 0), + 'store_id' => new Zend_Db_Expr($storeId)) + ) + ->joinLeft( + array('cp' => $this->_categoryProductTable), + 'cp.category_id=ap.category_id AND cp.product_id=ap.product_id', + array() + ) + ->joinInner(array('pv' => $enabledTable), 'pv.product_id = ap.product_id', array('visibility')); + $query = $select->insertFromSelect( + $idxTable, + array('category_id', 'product_id', 'position', 'is_parent', 'store_id', 'visibility'), + false + ); + $idxAdapter->query($query); + + $select = $idxAdapter->select() + ->from(array('e' => $this->getTable('catalog/product')), null) + ->join( + array('ei' => $enabledTable), + 'ei.product_id = e.entity_id', + array()) + ->joinLeft( + array('i' => $idxTable), + 'i.product_id = e.entity_id AND i.category_id = :category_id AND i.store_id = :store_id', + array()) + ->where('i.product_id IS NULL') + ->columns(array( + 'category_id' => new Zend_Db_Expr($rootId), + 'product_id' => 'e.entity_id', + 'position' => new Zend_Db_Expr('0'), + 'is_parent' => new Zend_Db_Expr('1'), + 'store_id' => new Zend_Db_Expr($storeId), + 'visibility' => 'ei.visibility' + )); + + $query = $select->insertFromSelect( + $idxTable, + array('category_id', 'product_id', 'position', 'is_parent', 'store_id', 'visibility'), + false + ); - $select = $idxAdapter->select() - ->useStraightJoin(true) - ->distinct(true) - ->from(array('ca' => $anchorTable), array('category_id')) - ->joinInner( - array('ce' => $this->_categoryTable), - $idxAdapter->quoteIdentifier('ce.path') . ' LIKE ' . - $idxAdapter->quoteIdentifier('ca.path') . ' OR ce.entity_id = ca.category_id', - array() - ) - ->joinInner( - array('cp' => $this->_categoryProductTable), - 'cp.category_id = ce.entity_id', - array('product_id') - ) - ->joinInner(array('pv' => $enabledTable), 'pv.product_id = cp.product_id', array('position' => $position)) - ->group(array('ca.category_id', 'cp.product_id')); - $query = $select->insertFromSelect($anchorProductsTable, - array('category_id', 'product_id', 'position'), false); - $idxAdapter->query($query); + $idxAdapter->query($query, array('store_id' => $storeId, 'category_id' => $rootId)); + } + + $this->syncData(); /** - * Add anchor categories products to index + * Clean up temporary tables */ - $select = $idxAdapter->select() - ->from( - array('ap' => $anchorProductsTable), - array('category_id', 'product_id', - 'position', // => new Zend_Db_Expr('MIN('. $idxAdapter->quoteIdentifier('ap.position').')'), - 'is_parent' => $idxAdapter->getCheckSql('cp.product_id > 0', 1, 0), - 'store_id' => new Zend_Db_Expr($storeId)) - ) - ->joinLeft( - array('cp' => $this->_categoryProductTable), - 'cp.category_id=ap.category_id AND cp.product_id=ap.product_id', - array() - ) - ->joinInner(array('pv' => $enabledTable), 'pv.product_id = ap.product_id', array('visibility')); - - $query = $select->insertFromSelect( - $idxTable, - array('category_id', 'product_id', 'position', 'is_parent', 'store_id', 'visibility'), - false - ); - $idxAdapter->query($query); - - $select = $idxAdapter->select() - ->from(array('e' => $this->getTable('catalog/product')), null) - ->join( - array('ei' => $enabledTable), - 'ei.product_id = e.entity_id', - array()) - ->joinLeft( - array('i' => $idxTable), - 'i.product_id = e.entity_id AND i.category_id = :category_id AND i.store_id = :store_id', - array()) - ->where('i.product_id IS NULL') - ->columns(array( - 'category_id' => new Zend_Db_Expr($rootId), - 'product_id' => 'e.entity_id', - 'position' => new Zend_Db_Expr('0'), - 'is_parent' => new Zend_Db_Expr('1'), - 'store_id' => new Zend_Db_Expr($storeId), - 'visibility' => 'ei.visibility' - )); - - $query = $select->insertFromSelect( - $idxTable, - array('category_id', 'product_id', 'position', 'is_parent', 'store_id', 'visibility'), - false - ); - - $idxAdapter->query($query, array('store_id' => $storeId, 'category_id' => $rootId)); + $this->clearTemporaryIndexTable(); + $idxAdapter->delete($enabledTable); + $idxAdapter->delete($anchorTable); + $idxAdapter->delete($anchorProductsTable); + $this->commit(); + } catch (Exception $e) { + $this->rollBack(); + throw $e; } - - $this->syncData(); - - /** - * Clean up temporary tables - */ - $this->clearTemporaryIndexTable(); - $idxAdapter->delete($enabledTable); - $idxAdapter->delete($anchorTable); - $idxAdapter->delete($anchorProductsTable); return $this; } diff --git a/app/code/core/Mage/Catalog/Model/Resource/Eav/Mysql4/Product/Indexer/Abstract.php b/app/code/core/Mage/Catalog/Model/Resource/Eav/Mysql4/Product/Indexer/Abstract.php index 111cf69723..e42f750385 100644 --- a/app/code/core/Mage/Catalog/Model/Resource/Eav/Mysql4/Product/Indexer/Abstract.php +++ b/app/code/core/Mage/Catalog/Model/Resource/Eav/Mysql4/Product/Indexer/Abstract.php @@ -32,7 +32,7 @@ * @package Mage_Catalog * @author Magento Core Team */ -class Mage_Catalog_Model_Resource_Eav_Mysql4_Product_Indexer_Abstract +abstract class Mage_Catalog_Model_Resource_Eav_Mysql4_Product_Indexer_Abstract extends Mage_Catalog_Model_Resource_Product_Indexer_Abstract { } diff --git a/app/code/core/Mage/Catalog/Model/Resource/Eav/Mysql4/Product/Indexer/Eav/Abstract.php b/app/code/core/Mage/Catalog/Model/Resource/Eav/Mysql4/Product/Indexer/Eav/Abstract.php index e3853c0e9b..268c6b6861 100644 --- a/app/code/core/Mage/Catalog/Model/Resource/Eav/Mysql4/Product/Indexer/Eav/Abstract.php +++ b/app/code/core/Mage/Catalog/Model/Resource/Eav/Mysql4/Product/Indexer/Eav/Abstract.php @@ -32,7 +32,7 @@ * @package Mage_Catalog * @author Magento Core Team */ -class Mage_Catalog_Model_Resource_Eav_Mysql4_Product_Indexer_Eav_Abstract +abstract class Mage_Catalog_Model_Resource_Eav_Mysql4_Product_Indexer_Eav_Abstract extends Mage_Catalog_Model_Resource_Product_Indexer_Eav_Abstract { } diff --git a/app/code/core/Mage/Catalog/Model/Resource/Eav/Mysql4/Product/Indexer/Price/Interface.php b/app/code/core/Mage/Catalog/Model/Resource/Eav/Mysql4/Product/Indexer/Price/Interface.php index 304605a2f5..458f3cbada 100644 --- a/app/code/core/Mage/Catalog/Model/Resource/Eav/Mysql4/Product/Indexer/Price/Interface.php +++ b/app/code/core/Mage/Catalog/Model/Resource/Eav/Mysql4/Product/Indexer/Price/Interface.php @@ -32,7 +32,7 @@ * @package Mage_Catalog * @author Magento Core Team */ -class Mage_Catalog_Model_Resource_Eav_Mysql4_Product_Indexer_Price_Interface +interface Mage_Catalog_Model_Resource_Eav_Mysql4_Product_Indexer_Price_Interface extends Mage_Catalog_Model_Resource_Product_Indexer_Price_Interface { } diff --git a/app/code/core/Mage/Catalog/Model/Resource/Layer/Filter/Price.php b/app/code/core/Mage/Catalog/Model/Resource/Layer/Filter/Price.php index 3842effaec..a964c2af26 100755 --- a/app/code/core/Mage/Catalog/Model/Resource/Layer/Filter/Price.php +++ b/app/code/core/Mage/Catalog/Model/Resource/Layer/Filter/Price.php @@ -164,20 +164,18 @@ public function getCount($filter, $range) 'range' => $rangeExpr, 'count' => $countExpr )); - $select->group($rangeExpr); + $select->group($rangeExpr)->order("$rangeExpr ASC"); return $connection->fetchPairs($select); } /** - * Apply attribute filter to product collection + * Prepare filter apply * * @param Mage_Catalog_Model_Layer_Filter_Price $filter - * @param int $range - * @param int $index the range factor - * @return Mage_Catalog_Model_Resource_Layer_Filter_Price + * @return array */ - public function applyFilterToCollection($filter, $range, $index) + protected function _prepareApply($filter) { $collection = $filter->getLayer()->getProductCollection(); $collection->addPriceData($filter->getCustomerGroupId(), $filter->getWebsiteId()); @@ -188,12 +186,89 @@ public function applyFilterToCollection($filter, $range, $index) $table = $this->_getIndexTableAlias(); $additional = join('', $response->getAdditionalCalculations()); $rate = $filter->getCurrencyRate(); - $priceExpr = new Zend_Db_Expr("(({$table}.min_price {$additional}) * {$rate})"); + $priceExpr = new Zend_Db_Expr("ROUND(({$table}.min_price {$additional}) * {$rate}, 2)"); + + return array($select, $priceExpr); + } + /** + * Apply attribute filter to product collection + * + * @param Mage_Catalog_Model_Layer_Filter_Price $filter + * @param int $range + * @param int $index the range factor + * @return Mage_Catalog_Model_Resource_Layer_Filter_Price + */ + public function applyFilterToCollection($filter, $range, $index) + { + list($select, $priceExpr) = $this->_prepareApply($filter); $select ->where($priceExpr . ' >= ?', ($range * ($index - 1))) ->where($priceExpr . ' < ?', ($range * $index)); return $this; } + + /** + * Load all product prices to algorithm model + * + * @param Mage_Catalog_Model_Layer_Filter_Price_Algorithm $algorithm + * @param Mage_Catalog_Model_Layer_Filter_Price $filter + * @return array + */ + public function loadAllPrices($algorithm, $filter) + { + $select = $this->_getSelect($filter); + $connection = $this->_getReadAdapter(); + $response = $this->_dispatchPreparePriceEvent($filter, $select); + + $table = $this->_getIndexTableAlias(); + + $additional = join('', $response->getAdditionalCalculations()); + $maxPriceExpr = new Zend_Db_Expr( + "ROUND(({$table}.min_price {$additional}) * " . $connection->quote($filter->getCurrencyRate()) . ", 2)" + ); + + $select->columns(array($maxPriceExpr)); + + $prices = $connection->fetchCol($select); + $algorithm->setPrices($prices); + + return $prices; + } + + /** + * Apply price range filter to product collection + * + * @param Mage_Catalog_Model_Layer_Filter_Price $filter + * @return Mage_Catalog_Model_Resource_Layer_Filter_Price + */ + public function applyPriceRange($filter) + { + $interval = $filter->getInterval(); + if (!$interval) { + return $this; + } + + list($from, $to) = $interval; + if ($from === '' && $to === '') { + return $this; + } + + list($select, $priceExpr) = $this->_prepareApply($filter); + + if ($from == $to && !empty($to)) { + $select->where($priceExpr . ' = ?', $from); + } else { + if ($from !== '') { + $select->where($priceExpr . ' >= ?', $from); + } + if ($to !== '') { + $select->where($priceExpr . ' < ?', $to); + } + } + + return $this; + + } } diff --git a/app/code/core/Mage/Catalog/Model/Resource/Product/Attribute/Backend/Groupprice.php b/app/code/core/Mage/Catalog/Model/Resource/Product/Attribute/Backend/Groupprice.php new file mode 100644 index 0000000000..06a2c3f593 --- /dev/null +++ b/app/code/core/Mage/Catalog/Model/Resource/Product/Attribute/Backend/Groupprice.php @@ -0,0 +1,46 @@ + + */ +class Mage_Catalog_Model_Resource_Product_Attribute_Backend_Groupprice + extends Mage_Catalog_Model_Resource_Product_Attribute_Backend_Groupprice_Abstract +{ + /** + * Initialize connection and define main table + * + */ + protected function _construct() + { + $this->_init('catalog/product_attribute_group_price', 'value_id'); + } +} diff --git a/app/code/core/Mage/Catalog/Model/Resource/Product/Attribute/Backend/Groupprice/Abstract.php b/app/code/core/Mage/Catalog/Model/Resource/Product/Attribute/Backend/Groupprice/Abstract.php new file mode 100644 index 0000000000..3498c02774 --- /dev/null +++ b/app/code/core/Mage/Catalog/Model/Resource/Product/Attribute/Backend/Groupprice/Abstract.php @@ -0,0 +1,147 @@ + + */ +abstract class Mage_Catalog_Model_Resource_Product_Attribute_Backend_Groupprice_Abstract + extends Mage_Core_Model_Resource_Db_Abstract +{ + /** + * Load Tier Prices for product + * + * @param int $productId + * @param int $websiteId + * @return Mage_Catalog_Model_Resource_Product_Attribute_Backend_Tierprice + */ + public function loadPriceData($productId, $websiteId = null) + { + $adapter = $this->_getReadAdapter(); + + $columns = array( + 'price_id' => $this->getIdFieldName(), + 'website_id' => 'website_id', + 'all_groups' => 'all_groups', + 'cust_group' => 'customer_group_id', + 'price' => 'value', + ); + + $columns = $this->_loadPriceDataColumns($columns); + + $select = $adapter->select() + ->from($this->getMainTable(), $columns) + ->where('entity_id=?', $productId); + + $this->_loadPriceDataSelect($select); + + if (!is_null($websiteId)) { + if ($websiteId == '0') { + $select->where('website_id = ?', $websiteId); + } else { + $select->where('website_id IN(?)', array(0, $websiteId)); + } + } + + return $adapter->fetchAll($select); + } + + /** + * Load specific sql columns + * + * @param array $columns + * @return array + */ + protected function _loadPriceDataColumns($columns) + { + return $columns; + } + + /** + * Load specific db-select data + * + * @param Varien_Db_Select $select + * @return Varien_Db_Select + */ + protected function _loadPriceDataSelect($select) + { + return $select; + } + + /** + * Delete Tier Prices for product + * + * @param int $productId + * @param int $websiteId + * @param int $priceId + * @return int The number of affected rows + */ + public function deletePriceData($productId, $websiteId = null, $priceId = null) + { + $adapter = $this->_getWriteAdapter(); + + $conds = array( + $adapter->quoteInto('entity_id = ?', $productId) + ); + + if (!is_null($websiteId)) { + $conds[] = $adapter->quoteInto('website_id = ?', $websiteId); + } + + if (!is_null($priceId)) { + $conds[] = $adapter->quoteInto($this->getIdFieldName() . ' = ?', $priceId); + } + + $where = implode(' AND ', $conds); + + return $adapter->delete($this->getMainTable(), $where); + } + + /** + * Save tier price object + * + * @param Varien_Object $priceObject + * @return Mage_Catalog_Model_Resource_Product_Attribute_Backend_Tierprice + */ + public function savePriceData(Varien_Object $priceObject) + { + $adapter = $this->_getWriteAdapter(); + $data = $this->_prepareDataForTable($priceObject, $this->getMainTable()); + + if (!empty($data[$this->getIdFieldName()])) { + $where = $adapter->quoteInto($this->getIdFieldName() . ' = ?', $data[$this->getIdFieldName()]); + unset($data[$this->getIdFieldName()]); + $adapter->update($this->getMainTable(), $data, $where); + } else { + $adapter->insert($this->getMainTable(), $data); + } + return $this; + } +} diff --git a/app/code/core/Mage/Catalog/Model/Resource/Product/Attribute/Backend/Tierprice.php b/app/code/core/Mage/Catalog/Model/Resource/Product/Attribute/Backend/Tierprice.php index 023668a3c5..b86300c7d7 100755 --- a/app/code/core/Mage/Catalog/Model/Resource/Product/Attribute/Backend/Tierprice.php +++ b/app/code/core/Mage/Catalog/Model/Resource/Product/Attribute/Backend/Tierprice.php @@ -32,7 +32,8 @@ * @package Mage_Catalog * @author Magento Core Team */ -class Mage_Catalog_Model_Resource_Product_Attribute_Backend_Tierprice extends Mage_Core_Model_Resource_Db_Abstract +class Mage_Catalog_Model_Resource_Product_Attribute_Backend_Tierprice + extends Mage_Catalog_Model_Resource_Product_Attribute_Backend_Groupprice_Abstract { /** * Initialize connection and define main table @@ -44,89 +45,28 @@ protected function _construct() } /** - * Load Tier Prices for product + * Add qty column * - * @param int $productId - * @param int $websiteId - * @return Mage_Catalog_Model_Resource_Product_Attribute_Backend_Tierprice - */ - public function loadPriceData($productId, $websiteId = null) - { - $adapter = $this->_getReadAdapter(); - - $columns = array( - 'price_id' => $this->getIdFieldName(), - 'website_id' => 'website_id', - 'all_groups' => 'all_groups', - 'cust_group' => 'customer_group_id', - 'price_qty' => 'qty', - 'price' => 'value', - ); - - $select = $adapter->select() - ->from($this->getMainTable(), $columns) - ->where('entity_id=?', $productId) - ->order('qty'); - - if (!is_null($websiteId)) { - if ($websiteId == '0') { - $select->where('website_id = ?', $websiteId); - } else { - $select->where('website_id IN(?)', array(0, $websiteId)); - } - } - - return $adapter->fetchAll($select); - } - - /** - * Delete Tier Prices for product - * - * @param int $productId - * @param int $websiteId - * @param int $priceId - * @return int The number of affected rows + * @param array $columns + * @return array */ - public function deletePriceData($productId, $websiteId = null, $priceId = null) + protected function _loadPriceDataColumns($columns) { - $adapter = $this->_getWriteAdapter(); - - $conds = array( - $adapter->quoteInto('entity_id = ?', $productId) - ); - - if (!is_null($websiteId)) { - $conds[] = $adapter->quoteInto('website_id = ?', $websiteId); - } - - if (!is_null($priceId)) { - $conds[] = $adapter->quoteInto($this->getIdFieldName() . ' = ?', $priceId); - } - - $where = implode(' AND ', $conds); - - return $adapter->delete($this->getMainTable(), $where); + $columns = parent::_loadPriceDataColumns($columns); + $columns['price_qty'] = 'qty'; + return $columns; } /** - * Save tier price object + * Order by qty * - * @param Varien_Object $priceObject - * @return Mage_Catalog_Model_Resource_Product_Attribute_Backend_Tierprice + * @param Varien_Db_Select $select + * @return Varien_Db_Select */ - public function savePriceData(Varien_Object $priceObject) + protected function _loadPriceDataSelect($select) { - $adapter = $this->_getWriteAdapter(); - $data = $this->_prepareDataForTable($priceObject, $this->getMainTable()); - - if (!empty($data[$this->getIdFieldName()])) { - $where = $adapter->quoteInto($this->getIdFieldName() . ' = ?', $data[$this->getIdFieldName()]); - unset($data[$this->getIdFieldName()]); - $adapter->update($this->getMainTable(), $data, $where); - } else { - $adapter->insert($this->getMainTable(), $data); - } - return $this; + $select->order('qty'); + return $select; } /** diff --git a/app/code/core/Mage/Catalog/Model/Resource/Product/Attribute/Collection.php b/app/code/core/Mage/Catalog/Model/Resource/Product/Attribute/Collection.php index cad040e4d2..9e83df6ba7 100755 --- a/app/code/core/Mage/Catalog/Model/Resource/Product/Attribute/Collection.php +++ b/app/code/core/Mage/Catalog/Model/Resource/Product/Attribute/Collection.php @@ -173,11 +173,13 @@ public function addToIndexFilter($addRequiredCodes = false) 'additional_table.is_searchable = 1', 'additional_table.is_visible_in_advanced_search = 1', 'additional_table.is_filterable > 0', - 'additional_table.is_filterable_in_search = 1' + 'additional_table.is_filterable_in_search = 1', + 'used_for_sort_by = 1' ); if ($addRequiredCodes) { - $conditions[] = $this->getConnection()->quoteInto('main_table.attribute_code IN (?)', array('status', 'visibility')); + $conditions[] = $this->getConnection()->quoteInto('main_table.attribute_code IN (?)', + array('status', 'visibility')); } $this->getSelect()->where(sprintf('(%s)', implode(' OR ', $conditions))); diff --git a/app/code/core/Mage/Catalog/Model/Resource/Product/Collection.php b/app/code/core/Mage/Catalog/Model/Resource/Product/Collection.php index 81aa8a6bd9..df5ed002a2 100755 --- a/app/code/core/Mage/Catalog/Model/Resource/Product/Collection.php +++ b/app/code/core/Mage/Catalog/Model/Resource/Product/Collection.php @@ -1332,7 +1332,7 @@ public function addAttributeToSort($attribute, $dir = self::SORT_ORDER_ASC) { if ($attribute == 'position') { if (isset($this->_joinFields[$attribute])) { - $this->getSelect()->order("{$attribute} {$dir}"); + $this->getSelect()->order($this->_getAttributeFieldName($attribute) . ' ' . $dir); return $this; } if ($this->isEnabledFlat()) { @@ -1685,6 +1685,10 @@ protected function _applyZeroStoreProductLimitations() array('cat_index_position' => 'position') ); } + $this->_joinFields['position'] = array( + 'table' => 'cat_pro', + 'field' => 'position', + ); return $this; } diff --git a/app/code/core/Mage/Catalog/Model/Resource/Product/Flat/Indexer.php b/app/code/core/Mage/Catalog/Model/Resource/Product/Flat/Indexer.php index 5842734835..c09d5d48dc 100755 --- a/app/code/core/Mage/Catalog/Model/Resource/Product/Flat/Indexer.php +++ b/app/code/core/Mage/Catalog/Model/Resource/Product/Flat/Indexer.php @@ -32,7 +32,7 @@ * @package Mage_Catalog * @author Magento Core Team */ -class Mage_Catalog_Model_Resource_Product_Flat_Indexer extends Mage_Core_Model_Resource_Db_Abstract +class Mage_Catalog_Model_Resource_Product_Flat_Indexer extends Mage_Index_Model_Resource_Abstract { const XML_NODE_MAX_INDEX_COUNT = 'global/catalog/product/flat/max_index_count'; const XML_NODE_ATTRIBUTE_NODES = 'global/catalog/product/flat/attribute_nodes'; @@ -93,6 +93,13 @@ class Mage_Catalog_Model_Resource_Product_Flat_Indexer extends Mage_Core_Model_R */ protected $_existsFlatTables = array(); + /** + * Flat tables which were prepared + * + * @var array + */ + protected $_preparedFlatTables = array(); + /** * Initialize connection * @@ -608,6 +615,9 @@ public function getFkName($priTableName, $priColumnName, $refTableName, $refColu */ public function prepareFlatTable($storeId) { + if (isset($this->_preparedFlatTables[$storeId])) { + return $this; + } $adapter = $this->_getWriteAdapter(); $tableName = $this->getFlatTableName($storeId); @@ -743,7 +753,7 @@ public function prepareFlatTable($storeId) $adapter->dropForeignKey($tableName, $foreignChildKey); } if ($isAddChildData && !isset($describe['is_child'])) { - $adapter->truncateTable($tableName); + $adapter->delete($tableName); $dropIndexes['PRIMARY'] = $indexesNow['PRIMARY']; $addIndexes['PRIMARY'] = $indexesNeed['PRIMARY']; @@ -807,6 +817,8 @@ public function prepareFlatTable($storeId) } } + $this->_preparedFlatTables[$storeId] = true; + return $this; } @@ -1366,4 +1378,28 @@ protected function _arrayNextKey(array $array, $key) } return false; } + + /** + * Transactional rebuild Catalog Product Flat Data + * + * @return Mage_Catalog_Model_Resource_Product_Flat_Indexer + */ + public function reindexAll() + { + foreach (Mage::app()->getStores() as $storeId => $store) { + $this->prepareFlatTable($storeId); + $this->beginTransaction(); + try { + $this->rebuild($store); + $this->commit(); + } catch (Exception $e) { + $this->rollBack(); + throw $e; + } + } + $flag = $this->getFlatHelper()->getFlag(); + $flag->setIsBuild(true)->save(); + + return $this; + } } diff --git a/app/code/core/Mage/Catalog/Model/Resource/Product/Indexer/Eav/Abstract.php b/app/code/core/Mage/Catalog/Model/Resource/Product/Indexer/Eav/Abstract.php index 63ec5f66a4..aeb9ad7dd1 100755 --- a/app/code/core/Mage/Catalog/Model/Resource/Product/Indexer/Eav/Abstract.php +++ b/app/code/core/Mage/Catalog/Model/Resource/Product/Indexer/Eav/Abstract.php @@ -44,12 +44,19 @@ abstract class Mage_Catalog_Model_Resource_Product_Indexer_Eav_Abstract public function reindexAll() { $this->useIdxTable(true); - $this->clearTemporaryIndexTable(); - $this->_prepareIndex(); - $this->_prepareRelationIndex(); - $this->_removeNotVisibleEntityFromIndex(); + $this->beginTransaction(); + try { + $this->clearTemporaryIndexTable(); + $this->_prepareIndex(); + $this->_prepareRelationIndex(); + $this->_removeNotVisibleEntityFromIndex(); - $this->syncData(); + $this->syncData(); + $this->commit(); + } catch (Exception $e) { + $this->rollBack(); + throw $e; + } return $this; } @@ -153,7 +160,13 @@ protected function _removeNotVisibleEntityFromIndex() ->from($idxTable, null); $condition = $write->quoteInto('=?',Mage_Catalog_Model_Product_Visibility::VISIBILITY_NOT_VISIBLE); - $this->_addAttributeToSelect($select, 'visibility', $idxTable . '.entity_id', $idxTable . '.store_id', $condition); + $this->_addAttributeToSelect( + $select, + 'visibility', + $idxTable . '.entity_id', + $idxTable . '.store_id', + $condition + ); $query = $select->deleteFromSelect($idxTable); $write->query($query); diff --git a/app/code/core/Mage/Catalog/Model/Resource/Product/Indexer/Price.php b/app/code/core/Mage/Catalog/Model/Resource/Product/Indexer/Price.php index 099a1a1393..101a364745 100755 --- a/app/code/core/Mage/Catalog/Model/Resource/Product/Indexer/Price.php +++ b/app/code/core/Mage/Catalog/Model/Resource/Product/Indexer/Price.php @@ -120,7 +120,7 @@ public function catalogProductDelete(Mage_Index_Model_Event $event) protected function _copyIndexDataToMainTable($processIds) { $write = $this->_getWriteAdapter(); - $write->beginTransaction(); + $this->beginTransaction(); try { // remove old index $where = $write->quoteInto('entity_id IN(?)', $processIds); @@ -172,6 +172,7 @@ public function catalogProductSave(Mage_Index_Model_Event $event) if ($indexer->getIsComposite()) { $this->_copyRelationIndexData($productId); $this->_prepareTierPriceIndex($productId); + $this->_prepareGroupPriceIndex($productId); $indexer->reindexEntity($productId); } else { $parentIds = $this->getProductParentsByChild($productId); @@ -180,6 +181,7 @@ public function catalogProductSave(Mage_Index_Model_Event $event) $processIds = array_merge($processIds, array_keys($parentIds)); $this->_copyRelationIndexData(array_keys($parentIds), $productId); $this->_prepareTierPriceIndex($processIds); + $this->_prepareGroupPriceIndex($processIds); $indexer->reindexEntity($productId); $parentByType = array(); @@ -192,6 +194,7 @@ public function catalogProductSave(Mage_Index_Model_Event $event) } } else { $this->_prepareTierPriceIndex($productId); + $this->_prepareGroupPriceIndex($productId); $indexer->reindexEntity($productId); } } @@ -369,23 +372,25 @@ public function getTypeIndexers() public function reindexAll() { $this->useIdxTable(true); - $this->clearTemporaryIndexTable(); - $this->_prepareWebsiteDateTable(); - $this->_prepareTierPriceIndex(); - - $indexers = $this->getTypeIndexers(); - foreach ($indexers as $indexer) { - /** @var $indexer Mage_Catalog_Model_Resource_Product_Indexer_Price_Interface */ - if (!$this->_allowTableChanges && is_callable(array($indexer, 'setAllowTableChanges'))) { - $indexer->setAllowTableChanges(false); - } - $indexer->reindexAll(); - if (!$this->_allowTableChanges && is_callable(array($indexer, 'setAllowTableChanges'))) { - $indexer->setAllowTableChanges(true); + $this->beginTransaction(); + try { + $this->clearTemporaryIndexTable(); + $this->_prepareWebsiteDateTable(); + $this->_prepareTierPriceIndex(); + $this->_prepareGroupPriceIndex(); + + $indexers = $this->getTypeIndexers(); + foreach ($indexers as $indexer) { + /** @var $indexer Mage_Catalog_Model_Resource_Product_Indexer_Price_Interface */ + $indexer->reindexAll(); } - } - $this->syncData(); + $this->syncData(); + $this->commit(); + } catch (Exception $e) { + $this->rollBack(); + throw $e; + } return $this; } @@ -399,6 +404,16 @@ protected function _getTierPriceIndexTable() return $this->getTable('catalog/product_index_tier_price'); } + /** + * Retrieve table name for product group price index + * + * @return string + */ + protected function _getGroupPriceIndexTable() + { + return $this->getTable('catalog/product_index_group_price'); + } + /** * Prepare tier price index table * @@ -442,6 +457,49 @@ protected function _prepareTierPriceIndex($entityIds = null) return $this; } + /** + * Prepare group price index table + * + * @param int|array $entityIds the entity ids limitation + * @return Mage_Catalog_Model_Resource_Product_Indexer_Price + */ + protected function _prepareGroupPriceIndex($entityIds = null) + { + $write = $this->_getWriteAdapter(); + $table = $this->_getGroupPriceIndexTable(); + $write->delete($table); + + $websiteExpression = $write->getCheckSql('gp.website_id = 0', 'ROUND(gp.value * cwd.rate, 4)', 'gp.value'); + $select = $write->select() + ->from( + array('gp' => $this->getValueTable('catalog/product', 'group_price')), + array('entity_id')) + ->join( + array('cg' => $this->getTable('customer/customer_group')), + 'gp.all_groups = 1 OR (gp.all_groups = 0 AND gp.customer_group_id = cg.customer_group_id)', + array('customer_group_id')) + ->join( + array('cw' => $this->getTable('core/website')), + 'gp.website_id = 0 OR gp.website_id = cw.website_id', + array('website_id')) + ->join( + array('cwd' => $this->_getWebsiteDateTable()), + 'cw.website_id = cwd.website_id', + array()) + ->where('cw.website_id != 0') + ->columns(new Zend_Db_Expr("MIN({$websiteExpression})")) + ->group(array('gp.entity_id', 'cg.customer_group_id', 'cw.website_id')); + + if (!empty($entityIds)) { + $select->where('gp.entity_id IN(?)', $entityIds); + } + + $query = $select->insertFromSelect($table); + $write->query($query); + + return $this; + } + /** * Copy relations product index from primary index to temporary index table by parent entity * diff --git a/app/code/core/Mage/Catalog/Model/Resource/Product/Indexer/Price/Configurable.php b/app/code/core/Mage/Catalog/Model/Resource/Product/Indexer/Price/Configurable.php index b3f8208617..964b149865 100755 --- a/app/code/core/Mage/Catalog/Model/Resource/Product/Indexer/Price/Configurable.php +++ b/app/code/core/Mage/Catalog/Model/Resource/Product/Indexer/Price/Configurable.php @@ -43,11 +43,17 @@ class Mage_Catalog_Model_Resource_Product_Indexer_Price_Configurable public function reindexAll() { $this->useIdxTable(true); - $this->_prepareFinalPriceData(); - $this->_applyCustomOption(); - $this->_applyConfigurableOption(); - $this->_movePriceDataToIndexTable(); - + $this->beginTransaction(); + try { + $this->_prepareFinalPriceData(); + $this->_applyCustomOption(); + $this->_applyConfigurableOption(); + $this->_movePriceDataToIndexTable(); + $this->commit(); + } catch (Exception $e) { + $this->rollBack(); + throw $e; + } return $this; } @@ -164,20 +170,26 @@ protected function _applyConfigurableOption() ->group(array('l.parent_id', 'i.customer_group_id', 'i.website_id', 'l.product_id')); $priceExpression = $write->getCheckSql('apw.value_id IS NOT NULL', 'apw.pricing_value', 'apd.pricing_value'); - $percenExpr = $write->getCheckSql('apw.value_id IS NOT NULL', 'apw.is_percent', 'apd.is_percent'); + $percentExpr = $write->getCheckSql('apw.value_id IS NOT NULL', 'apw.is_percent', 'apd.is_percent'); $roundExpr = "ROUND(i.price * ({$priceExpression} / 100), 4)"; - $roundPriceExpr = $write->getCheckSql("{$percenExpr} = 1", $roundExpr, $priceExpression); + $roundPriceExpr = $write->getCheckSql("{$percentExpr} = 1", $roundExpr, $priceExpression); $priceColumn = $write->getCheckSql("{$priceExpression} IS NULL", '0', $roundPriceExpr); $priceColumn = new Zend_Db_Expr("SUM({$priceColumn})"); $tierPrice = $priceExpression; - $tierRoundPriceExp = $write->getCheckSql("{$percenExpr} = 1", $roundExpr, $tierPrice); + $tierRoundPriceExp = $write->getCheckSql("{$percentExpr} = 1", $roundExpr, $tierPrice); $tierPriceExp = $write->getCheckSql("{$tierPrice} IS NULL", '0', $tierRoundPriceExp); $tierPriceColumn = $write->getCheckSql("MIN(i.tier_price) IS NOT NULL", "SUM({$tierPriceExp})", 'NULL'); + $groupPrice = $priceExpression; + $groupRoundPriceExp = $write->getCheckSql("{$percentExpr} = 1", $roundExpr, $groupPrice); + $groupPriceExp = $write->getCheckSql("{$groupPrice} IS NULL", '0', $groupRoundPriceExp); + $groupPriceColumn = $write->getCheckSql("MIN(i.group_price) IS NOT NULL", "SUM({$groupPriceExp})", 'NULL'); + $select->columns(array( - 'price' => $priceColumn, - 'tier_price' => $tierPriceColumn + 'price' => $priceColumn, + 'tier_price' => $tierPriceColumn, + 'group_price' => $groupPriceColumn, )); $query = $select->insertFromSelect($coaTable); @@ -186,7 +198,10 @@ protected function _applyConfigurableOption() $select = $write->select() ->from( array($coaTable), - array('parent_id', 'customer_group_id', 'website_id', 'MIN(price)', 'MAX(price)', 'MIN(tier_price)')) + array( + 'parent_id', 'customer_group_id', 'website_id', + 'MIN(price)', 'MAX(price)', 'MIN(tier_price)', 'MIN(group_price)' + )) ->group(array('parent_id', 'customer_group_id', 'website_id')); $query = $select->insertFromSelect($copTable); @@ -200,21 +215,20 @@ protected function _applyConfigurableOption() .' AND i.website_id = io.website_id', array()); $select->columns(array( - 'min_price' => new Zend_Db_Expr('i.min_price + io.min_price'), - 'max_price' => new Zend_Db_Expr('i.max_price + io.max_price'), - 'tier_price' => $write->getCheckSql('i.tier_price IS NOT NULL', 'i.tier_price + io.tier_price', 'NULL'), + 'min_price' => new Zend_Db_Expr('i.min_price + io.min_price'), + 'max_price' => new Zend_Db_Expr('i.max_price + io.max_price'), + 'tier_price' => $write->getCheckSql('i.tier_price IS NOT NULL', 'i.tier_price + io.tier_price', 'NULL'), + 'group_price' => $write->getCheckSql( + 'i.group_price IS NOT NULL', + 'i.group_price + io.group_price', 'NULL' + ), )); $query = $select->crossUpdateFromSelect($table); $write->query($query); - if ($this->useIdxTable() && $this->_allowTableChanges) { - $write->truncateTable($coaTable); - $write->truncateTable($copTable); - } else { - $write->delete($coaTable); - $write->delete($copTable); - } + $write->delete($coaTable); + $write->delete($copTable); return $this; } diff --git a/app/code/core/Mage/Catalog/Model/Resource/Product/Indexer/Price/Default.php b/app/code/core/Mage/Catalog/Model/Resource/Product/Indexer/Price/Default.php index 542b0fa6b8..2684566b53 100755 --- a/app/code/core/Mage/Catalog/Model/Resource/Product/Indexer/Price/Default.php +++ b/app/code/core/Mage/Catalog/Model/Resource/Product/Indexer/Price/Default.php @@ -115,9 +115,16 @@ public function getIsComposite() public function reindexAll() { $this->useIdxTable(true); - $this->_prepareFinalPriceData(); - $this->_applyCustomOption(); - $this->_movePriceDataToIndexTable(); + $this->beginTransaction(); + try { + $this->_prepareFinalPriceData(); + $this->_applyCustomOption(); + $this->_movePriceDataToIndexTable(); + $this->commit(); + } catch (Exception $e) { + $this->rollBack(); + throw $e; + } return $this; } @@ -216,6 +223,11 @@ protected function _prepareFinalPriceData($entityIds = null) 'tp.entity_id = e.entity_id AND tp.website_id = cw.website_id' . ' AND tp.customer_group_id = cg.customer_group_id', array()) + ->joinLeft( + array('gp' => $this->_getGroupPriceIndexTable()), + 'gp.entity_id = e.entity_id AND gp.website_id = cw.website_id' + . ' AND gp.customer_group_id = cg.customer_group_id', + array()) ->where('e.type_id = ?', $this->getTypeId()); // add enable products limitation @@ -233,6 +245,7 @@ protected function _prepareFinalPriceData($entityIds = null) $specialFrom = $this->_addAttributeToSelect($select, 'special_from_date', 'e.entity_id', 'cs.store_id'); $specialTo = $this->_addAttributeToSelect($select, 'special_to_date', 'e.entity_id', 'cs.store_id'); $currentDate = $write->getDatePartSql('cwd.website_date'); + $groupPrice = $write->getCheckSql('gp.price IS NULL', "{$price}", 'gp.price'); $specialFromDate = $write->getDatePartSql($specialFrom); $specialToDate = $write->getDatePartSql($specialTo); @@ -243,14 +256,17 @@ protected function _prepareFinalPriceData($entityIds = null) $specialToHas = $write->getCheckSql("{$specialTo} IS NULL", '1', "{$specialToUse}"); $finalPrice = $write->getCheckSql("{$specialFromHas} > 0 AND {$specialToHas} > 0" . " AND {$specialPrice} < {$price}", $specialPrice, $price); + $finalPrice = $write->getCheckSql("{$groupPrice} < {$finalPrice}", $groupPrice, $finalPrice); $select->columns(array( - 'orig_price' => $price, - 'price' => $finalPrice, - 'min_price' => $finalPrice, - 'max_price' => $finalPrice, - 'tier_price' => new Zend_Db_Expr('tp.min_price'), - 'base_tier' => new Zend_Db_Expr('tp.min_price'), + 'orig_price' => $price, + 'price' => $finalPrice, + 'min_price' => $finalPrice, + 'max_price' => $finalPrice, + 'tier_price' => new Zend_Db_Expr('tp.min_price'), + 'base_tier' => new Zend_Db_Expr('tp.min_price'), + 'group_price' => new Zend_Db_Expr('gp.price'), + 'base_group_price' => new Zend_Db_Expr('gp.price'), )); if (!is_null($entityIds)) { @@ -300,7 +316,7 @@ protected function _getCustomOptionAggregateTable() if ($this->useIdxTable()) { return $this->getTable('catalog/product_price_indexer_option_aggregate_idx'); } - return $this->getTable('catalog/product_price_indexer_option_aggregate_idx'); + return $this->getTable('catalog/product_price_indexer_option_aggregate_tmp'); } /** @@ -399,16 +415,22 @@ protected function _applyCustomOption() $tierPriceValue = $write->getCheckSql("MIN(o.is_require) > 0", $tierPriceMin, 0); $tierPrice = $write->getCheckSql("MIN(i.base_tier) IS NOT NULL", $tierPriceValue, "NULL"); + $groupPriceRound = new Zend_Db_Expr("ROUND(i.base_group_price * ({$optPriceValue} / 100), 4)"); + $groupPriceExpr = $write->getCheckSql("{$optPriceType} = 'fixed'", $optPriceValue, $groupPriceRound); + $groupPriceMin = new Zend_Db_Expr("MIN($groupPriceExpr)"); + $groupPriceValue = $write->getCheckSql("MIN(o.is_require) > 0", $groupPriceMin, 0); + $groupPrice = $write->getCheckSql("MIN(i.base_group_price) IS NOT NULL", $groupPriceValue, "NULL"); + $maxPriceRound = new Zend_Db_Expr("ROUND(i.price * ({$optPriceValue} / 100), 4)"); $maxPriceExpr = $write->getCheckSql("{$optPriceType} = 'fixed'", $optPriceValue, $maxPriceRound); - //$tierPriceMin = new Zend_Db_Expr("MIN($tierPriceExpr)"); $maxPrice = $write->getCheckSql("(MIN(o.type)='radio' OR MIN(o.type)='drop_down')", "MAX($maxPriceExpr)", "SUM($maxPriceExpr)"); $select->columns(array( - 'min_price' => $minPrice, - 'max_price' => $maxPrice, - 'tier_price' => $tierPrice + 'min_price' => $minPrice, + 'max_price' => $maxPrice, + 'tier_price' => $tierPrice, + 'group_price' => $groupPrice, )); $query = $select->insertFromSelect($coaTable); @@ -457,10 +479,16 @@ protected function _applyCustomOption() $tierPriceValue = $write->getCheckSql("{$tierPriceExpr} > 0 AND o.is_require > 0", $tierPriceExpr, 0); $tierPrice = $write->getCheckSql("i.base_tier IS NOT NULL", $tierPriceValue, "NULL"); + $groupPriceRound = new Zend_Db_Expr("ROUND(i.base_group_price * ({$optPriceValue} / 100), 4)"); + $groupPriceExpr = $write->getCheckSql("{$optPriceType} = 'fixed'", $optPriceValue, $groupPriceRound); + $groupPriceValue = $write->getCheckSql("{$groupPriceExpr} > 0 AND o.is_require > 0", $groupPriceExpr, 0); + $groupPrice = $write->getCheckSql("i.base_group_price IS NOT NULL", $groupPriceValue, "NULL"); + $select->columns(array( - 'min_price' => $minPrice, - 'max_price' => $maxPrice, - 'tier_price' => $tierPrice + 'min_price' => $minPrice, + 'max_price' => $maxPrice, + 'tier_price' => $tierPrice, + 'group_price' => $groupPrice, )); $query = $select->insertFromSelect($coaTable); @@ -476,6 +504,7 @@ protected function _applyCustomOption() 'min_price' => 'SUM(min_price)', 'max_price' => 'SUM(max_price)', 'tier_price' => 'SUM(tier_price)', + 'group_price' => 'SUM(group_price)', )) ->group(array('entity_id', 'customer_group_id', 'website_id')); $query = $select->insertFromSelect($copTable); @@ -489,20 +518,19 @@ protected function _applyCustomOption() .' AND i.website_id = io.website_id', array()); $select->columns(array( - 'min_price' => new Zend_Db_Expr('i.min_price + io.min_price'), - 'max_price' => new Zend_Db_Expr('i.max_price + io.max_price'), - 'tier_price' => $write->getCheckSql('i.tier_price IS NOT NULL', 'i.tier_price + io.tier_price', 'NULL'), + 'min_price' => new Zend_Db_Expr('i.min_price + io.min_price'), + 'max_price' => new Zend_Db_Expr('i.max_price + io.max_price'), + 'tier_price' => $write->getCheckSql('i.tier_price IS NOT NULL', 'i.tier_price + io.tier_price', 'NULL'), + 'group_price' => $write->getCheckSql( + 'i.group_price IS NOT NULL', + 'i.group_price + io.group_price', 'NULL' + ), )); $query = $select->crossUpdateFromSelect($table); $write->query($query); - if ($this->useIdxTable() && $this->_allowTableChanges) { - $write->truncateTable($coaTable); - $write->truncateTable($copTable); - } else { - $write->delete($coaTable); - $write->delete($copTable); - } + $write->delete($coaTable); + $write->delete($copTable); return $this; } @@ -523,7 +551,8 @@ protected function _movePriceDataToIndexTable() 'final_price' => 'price', 'min_price' => 'min_price', 'max_price' => 'max_price', - 'tier_price' => 'tier_price' + 'tier_price' => 'tier_price', + 'group_price' => 'group_price', ); $write = $this->_getWriteAdapter(); @@ -534,11 +563,7 @@ protected function _movePriceDataToIndexTable() $query = $select->insertFromSelect($this->getIdxTable(), array(), false); $write->query($query); - if ($this->useIdxTable() && $this->_allowTableChanges) { - $write->truncateTable($table); - } else { - $write->delete($table); - } + $write->delete($table); return $this; } @@ -553,6 +578,16 @@ protected function _getTierPriceIndexTable() return $this->getTable('catalog/product_index_tier_price'); } + /** + * Retrieve table name for product group price index + * + * @return string + */ + protected function _getGroupPriceIndexTable() + { + return $this->getTable('catalog/product_index_group_price'); + } + /** * Register data required by product type process in event object * diff --git a/app/code/core/Mage/Catalog/Model/Resource/Product/Indexer/Price/Grouped.php b/app/code/core/Mage/Catalog/Model/Resource/Product/Indexer/Price/Grouped.php index 484947ec9a..a5f9754f81 100755 --- a/app/code/core/Mage/Catalog/Model/Resource/Product/Indexer/Price/Grouped.php +++ b/app/code/core/Mage/Catalog/Model/Resource/Product/Indexer/Price/Grouped.php @@ -43,7 +43,14 @@ class Mage_Catalog_Model_Resource_Product_Indexer_Price_Grouped public function reindexAll() { $this->useIdxTable(true); - $this->_prepareGroupedProductPriceData(); + $this->beginTransaction(); + try { + $this->_prepareGroupedProductPriceData(); + $this->commit(); + } catch (Exception $e) { + $this->rollBack(); + throw $e; + } return $this; } @@ -96,12 +103,14 @@ protected function _prepareGroupedProductPriceData($entityIds = null) 'i.entity_id = l.linked_product_id AND i.website_id = cw.website_id' . ' AND i.customer_group_id = cg.customer_group_id', array( - 'tax_class_id'=> $this->_getReadAdapter()->getCheckSql('MIN(i.tax_class_id) IS NULL', '0', 'MIN(i.tax_class_id)'), - 'price' => new Zend_Db_Expr('NULL'), - 'final_price' => new Zend_Db_Expr('NULL'), - 'min_price' => new Zend_Db_Expr('MIN(' . $minCheckSql . ')'), - 'max_price' => new Zend_Db_Expr('MAX(' . $maxCheckSql . ')'), - 'tier_price' => new Zend_Db_Expr('NULL') + 'tax_class_id' => $this->_getReadAdapter() + ->getCheckSql('MIN(i.tax_class_id) IS NULL', '0', 'MIN(i.tax_class_id)'), + 'price' => new Zend_Db_Expr('NULL'), + 'final_price' => new Zend_Db_Expr('NULL'), + 'min_price' => new Zend_Db_Expr('MIN(' . $minCheckSql . ')'), + 'max_price' => new Zend_Db_Expr('MAX(' . $maxCheckSql . ')'), + 'tier_price' => new Zend_Db_Expr('NULL'), + 'group_price' => new Zend_Db_Expr('NULL'), )) ->group(array('e.entity_id', 'cg.customer_group_id', 'cw.website_id')) ->where('e.type_id=?', $this->getTypeId()); diff --git a/app/code/core/Mage/Catalog/Model/Resource/Product/Option.php b/app/code/core/Mage/Catalog/Model/Resource/Product/Option.php index 1d1fc3e160..8d39f643e4 100755 --- a/app/code/core/Mage/Catalog/Model/Resource/Product/Option.php +++ b/app/code/core/Mage/Catalog/Model/Resource/Product/Option.php @@ -440,19 +440,19 @@ public function getSearchableData($productId, $storeId) $defaultOptionJoin = implode( ' AND ', - array('option_title_default.option_id=option.option_id', + array('option_title_default.option_id=product_option.option_id', $adapter->quoteInto('option_title_default.store_id = ?', Mage_Catalog_Model_Abstract::DEFAULT_STORE_ID)) ); $storeOptionJoin = implode( ' AND ', array( - 'option_title_store.option_id=option.option_id', + 'option_title_store.option_id=product_option.option_id', $adapter->quoteInto('option_title_store.store_id = ?', (int) $storeId)) ); $select = $adapter->select() - ->from(array('option' => $this->getMainTable()), null) + ->from(array('product_option' => $this->getMainTable()), null) ->join( array('option_title_default' => $this->getTable('catalog/product_option_title')), $defaultOptionJoin, @@ -463,7 +463,7 @@ public function getSearchableData($productId, $storeId) $storeOptionJoin, array('title' => $titleCheckSql) ) - ->where('option.product_id = ?', $productId); + ->where('product_option.product_id = ?', $productId); if ($titles = $adapter->fetchCol($select)) { $searchData = array_merge($searchData, $titles); @@ -484,10 +484,10 @@ public function getSearchableData($productId, $storeId) ); $select = $adapter->select() - ->from(array('option' => $this->getMainTable()), null) + ->from(array('product_option' => $this->getMainTable()), null) ->join( array('option_type' => $this->getTable('catalog/product_option_type_value')), - 'option_type.option_id=option.option_id', + 'option_type.option_id=product_option.option_id', array() ) ->join( @@ -500,7 +500,7 @@ public function getSearchableData($productId, $storeId) $storeOptionJoin, array('title' => $titleCheckSql) ) - ->where('option.product_id = ?', $productId); + ->where('product_option.product_id = ?', $productId); if ($titles = $adapter->fetchCol($select)) { $searchData = array_merge($searchData, $titles); diff --git a/app/code/core/Mage/Catalog/Model/Resource/Setup.php b/app/code/core/Mage/Catalog/Model/Resource/Setup.php index 450f8a2198..86938e6f6a 100755 --- a/app/code/core/Mage/Catalog/Model/Resource/Setup.php +++ b/app/code/core/Mage/Catalog/Model/Resource/Setup.php @@ -484,7 +484,7 @@ public function getDefaultEntities() 'weight' => array( 'type' => 'decimal', 'label' => 'Weight', - 'input' => 'text', + 'input' => 'weight', 'sort_order' => 5, 'apply_to' => Mage_Catalog_Model_Product_Type::TYPE_SIMPLE, ), @@ -576,6 +576,17 @@ public function getDefaultEntities() 'sort_order' => 6, 'visible' => false, ), + 'group_price' => array( + 'type' => 'decimal', + 'label' => 'Group Price', + 'input' => 'text', + 'backend' => 'catalog/product_attribute_backend_groupprice', + 'required' => false, + 'sort_order' => 6, + 'global' => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_WEBSITE, + 'apply_to' => 'simple,configurable,virtual', + 'group' => 'Prices', + ), 'tier_price' => array( 'type' => 'decimal', 'label' => 'Tier Price', diff --git a/app/code/core/Mage/Catalog/Model/Resource/Url.php b/app/code/core/Mage/Catalog/Model/Resource/Url.php index 89dbe8e639..0e81fa03dc 100755 --- a/app/code/core/Mage/Catalog/Model/Resource/Url.php +++ b/app/code/core/Mage/Catalog/Model/Resource/Url.php @@ -153,7 +153,7 @@ public function getRewriteByIdPath($idPath, $storeId) */ public function getRewriteByRequestPath($requestPath, $storeId) { - $adapter = $this->_getReadAdapter(); + $adapter = $this->_getWriteAdapter(); $select = $adapter->select() ->from($this->getMainTable()) ->where('store_id = :store_id') @@ -182,7 +182,7 @@ public function getRewriteByRequestPath($requestPath, $storeId) */ public function checkRequestPaths($paths, $storeId) { - $adapter = $this->_getReadAdapter(); + $adapter = $this->_getWriteAdapter(); $select = $adapter->select() ->from($this->getMainTable(), 'request_path') ->where('store_id = :store_id') @@ -1310,12 +1310,26 @@ public function findFinalTargetPath($requestPath, $storeId, &$_checkedPaths = ar */ public function deleteRewrite($requestPath, $storeId) { - $this->_getWriteAdapter()->delete( - $this->getMainTable(), - array( - 'store_id = ?' => $storeId, - 'request_path = ?' => $requestPath - ) + $this->deleteRewriteRecord($requestPath, $storeId); + } + + /** + * Delete rewrite path record from the database with RP checking. + * + * @param string $requestPath + * @param int $storeId + * @param bool $rp whether check rewrite option to be "Redirect = Permanent" + * @return void + */ + public function deleteRewriteRecord($requestPath, $storeId, $rp = false) + { + $conditions = array( + 'store_id = ?' => $storeId, + 'request_path = ?' => $requestPath, ); + if ($rp) { + $conditions['options = ?'] = 'RP'; + } + $this->_getWriteAdapter()->delete($this->getMainTable(), $conditions); } } diff --git a/app/code/core/Mage/Catalog/Model/Url.php b/app/code/core/Mage/Catalog/Model/Url.php index 179148716e..39cfc4b643 100644 --- a/app/code/core/Mage/Catalog/Model/Url.php +++ b/app/code/core/Mage/Catalog/Model/Url.php @@ -191,6 +191,7 @@ public function getProductModel() /** * Returns store root category, uses caching for it * + * @param int $storeId * @return Varien_Object */ public function getStoreRootCategory($storeId) { @@ -210,6 +211,7 @@ public function getStoreRootCategory($storeId) { * Setter for $_saveRewritesHistory * Force Rewrites History save bypass config settings * + * @param bool $flag * @return Mage_Catalog_Model_Url */ public function setShouldSaveRewritesHistory($flag) @@ -261,6 +263,7 @@ public function refreshRewrites($storeId = null) * * @param Varien_Object $category * @param string $parentPath + * @param bool $refreshProducts * @return Mage_Catalog_Model_Url */ protected function _refreshCategoryRewrites(Varien_Object $category, $parentPath = null, $refreshProducts = true) @@ -275,7 +278,7 @@ protected function _refreshCategoryRewrites(Varien_Object $category, $parentPath $idPath = $this->generatePath('id', null, $category); $targetPath = $this->generatePath('target', null, $category); - $requestPath = $this->generatePath('request', null, $category, $parentPath); + $requestPath = $this->getCategoryRequestPath($category, $parentPath); $rewriteData = array( 'store_id' => $category->getStoreId(), @@ -675,6 +678,74 @@ public function getCategoryUrlSuffix($storeId) return Mage::helper('catalog/category')->getCategoryUrlSuffix($storeId); } + /** + * Get unique category request path + * + * @param Varien_Object $category + * @param string $parentPath + * @return string + */ + public function getCategoryRequestPath($category, $parentPath) + { + $storeId = $category->getStoreId(); + $idPath = $this->generatePath('id', null, $category); + $suffix = $this->getCategoryUrlSuffix($storeId); + + if (isset($this->_rewrites[$idPath])) { + $this->_rewrite = $this->_rewrites[$idPath]; + $existingRequestPath = $this->_rewrites[$idPath]->getRequestPath(); + } + + if ($category->getUrlKey() == '') { + $urlKey = $this->getCategoryModel()->formatUrlKey($category->getName()); + } + else { + $urlKey = $this->getCategoryModel()->formatUrlKey($category->getUrlKey()); + } + + $categoryUrlSuffix = $this->getCategoryUrlSuffix($category->getStoreId()); + if (null === $parentPath) { + $parentPath = $this->getResource()->getCategoryParentPath($category); + } + elseif ($parentPath == '/') { + $parentPath = ''; + } + $parentPath = Mage::helper('catalog/category')->getCategoryUrlPath($parentPath, + true, $category->getStoreId()); + + $requestPath = $parentPath . $urlKey . $categoryUrlSuffix; + if (isset($existingRequestPath) && $existingRequestPath == $requestPath . $suffix) { + return $existingRequestPath; + } + + if ($this->_deleteOldTargetPath($requestPath, $idPath, $storeId)) { + return $requestPath; + } + + return $this->getUnusedPath($category->getStoreId(), $requestPath, + $this->generatePath('id', null, $category) + ); + } + + /** + * Check if current generated request path is one of the old paths + * + * @param string $requestPath + * @param string $idPath + * @param int $storeId + * @return bool + */ + protected function _deleteOldTargetPath($requestPath, $idPath, $storeId) + { + $finalOldTargetPath = $this->getResource()->findFinalTargetPath($requestPath, $storeId); + if ($finalOldTargetPath && $finalOldTargetPath == $idPath) { + $this->getResource()->deleteRewriteRecord($requestPath, $storeId, true); + return true; + } + + return false; + } + /** * Get unique product request path * @@ -716,29 +787,28 @@ public function getProductRequestPath($product, $category) if (isset($this->_rewrites[$idPath])) { $this->_rewrite = $this->_rewrites[$idPath]; $existingRequestPath = $this->_rewrites[$idPath]->getRequestPath(); - $existingRequestPath = str_replace($suffix, '', $existingRequestPath); - if ($existingRequestPath == $requestPath) { - return $requestPath.$suffix; + if ($existingRequestPath == $requestPath . $suffix) { + return $existingRequestPath; } + + $existingRequestPath = preg_replace('/' . preg_quote($suffix, '/') . '$/', '', $existingRequestPath); /** * Check if existing request past can be used */ if ($product->getUrlKey() == '' && !empty($requestPath) - && strpos($existingRequestPath, $requestPath) !== false + && strpos($existingRequestPath, $requestPath) === 0 ) { - $existingRequestPath = str_replace($requestPath, '', $existingRequestPath); + $existingRequestPath = preg_replace( + '/^' . preg_quote($requestPath, '/') . '/', '', $existingRequestPath + ); if (preg_match('#^-([0-9]+)$#i', $existingRequestPath)) { return $this->_rewrites[$idPath]->getRequestPath(); } } - /** - * check if current generated request path is one of the old paths - */ + $fullPath = $requestPath.$suffix; - $finalOldTargetPath = $this->getResource()->findFinalTargetPath($fullPath, $storeId); - if ($finalOldTargetPath && $finalOldTargetPath == $idPath) { - $this->getResource()->deleteRewrite($fullPath, $storeId); + if ($this->_deleteOldTargetPath($fullPath, $idPath, $storeId)) { return $fullPath; } } diff --git a/app/code/core/Mage/Catalog/data/catalog_setup/data-upgrade-1.6.0.0.4-1.6.0.0.5.php b/app/code/core/Mage/Catalog/data/catalog_setup/data-upgrade-1.6.0.0.4-1.6.0.0.5.php index 2c442be07f..9baaf4198c 100644 --- a/app/code/core/Mage/Catalog/data/catalog_setup/data-upgrade-1.6.0.0.4-1.6.0.0.5.php +++ b/app/code/core/Mage/Catalog/data/catalog_setup/data-upgrade-1.6.0.0.4-1.6.0.0.5.php @@ -34,26 +34,25 @@ foreach($multiSelectAttributeCodes as $attributeCode) { /** @var $attribute Mage_Catalog_Model_Resource_Eav_Attribute */ - if ($attribute = $installer->getAttribute('catalog_product', $attributeCode)) { + $attribute = $installer->getAttribute('catalog_product', $attributeCode); + if ($attribute) { $attributeTable = $installer->getAttributeTable('catalog_product', $attributeCode); $select = $installer->getConnection()->select() ->from(array('e' => $attributeTable)) - ->where("e.attribute_id=?", $attribute['attribute_id']); + ->where("e.attribute_id=?", $attribute['attribute_id']) + ->where('e.value LIKE "%,,%"'); + $result = $installer->getConnection()->fetchAll($select); - if ($result = $installer->getConnection()->fetchAll($select)) { + if ($result) { foreach ($result as $row) { - $value = explode(',', $row['value']); - if (is_array($value) && count($value) > 1) { - foreach ($value as $optionKey => $optionValue) { - if ($optionValue === '') { - unset($value[$optionKey]); - } + if (isset($row['value']) && !empty($row['value'])) { + // 1,2,,,3,5 --> 1,2,3,5 + $row['value'] = preg_replace('/,{2,}/', ',', $row['value'], -1, $replaceCnt); + + if ($replaceCnt) { + $installer->getConnection() + ->update($attributeTable, array('value' => $row['value']), "value_id=" . $row['value_id']); } - $value = implode(',', $value); - $installer->getConnection() - ->update($attributeTable, array('value'=>$value), "value_id=" . $row['value_id']); - } else { - unset($value); } } } diff --git a/app/code/core/Mage/Catalog/data/catalog_setup/data-upgrade-1.6.0.0.8-1.6.0.0.9.php b/app/code/core/Mage/Catalog/data/catalog_setup/data-upgrade-1.6.0.0.8-1.6.0.0.9.php new file mode 100644 index 0000000000..50802de0de --- /dev/null +++ b/app/code/core/Mage/Catalog/data/catalog_setup/data-upgrade-1.6.0.0.8-1.6.0.0.9.php @@ -0,0 +1,36 @@ +getAttribute('catalog_product', 'weight'); + +if ($attribute) { + $installer->updateAttribute($attribute['entity_type_id'], $attribute['attribute_id'], + 'frontend_input', $attribute['attribute_code']); +} diff --git a/app/code/core/Mage/Catalog/etc/config.xml b/app/code/core/Mage/Catalog/etc/config.xml index a14d2b2c02..d7cda0baa2 100644 --- a/app/code/core/Mage/Catalog/etc/config.xml +++ b/app/code/core/Mage/Catalog/etc/config.xml @@ -28,7 +28,7 @@ - 1.6.0.0.6 + 1.6.0.0.10 @@ -276,6 +276,9 @@ catalog_product_entity_tier_price
+ + catalog_product_entity_group_price
+
catalog_product_entity_media_gallery
@@ -324,6 +327,9 @@ catalog_product_index_tier_price
+ + catalog_product_index_group_price
+
catalog_product_index_website
@@ -767,6 +773,7 @@ auto 100 + 10 @@ -794,7 +801,7 @@ 0 2 * * * - catalog/product_indexer_price::reindexAll + catalog/observer::reindexProductPrices diff --git a/app/code/core/Mage/Catalog/etc/system.xml b/app/code/core/Mage/Catalog/etc/system.xml index 4cbacb4a7d..795301ec57 100644 --- a/app/code/core/Mage/Catalog/etc/system.xml +++ b/app/code/core/Mage/Catalog/etc/system.xml @@ -64,6 +64,7 @@ Comma-separated. text + validate-per-page-value-list 2 1 @@ -74,6 +75,7 @@ Must be in the allowed values list. text + validate-per-page-value 3 1 @@ -84,6 +86,7 @@ Comma-separated. text + validate-per-page-value-list 4 1 @@ -94,6 +97,7 @@ Must be in the allowed values list. text + validate-per-page-value 5 1 @@ -320,12 +324,24 @@ text + validate-number validate-number-range number-range-0.01-1000000000 2 1 1 1 manual + + + Maximum number of price intervals is 100 + text + validate-digits validate-digits-range digits-range-2-100 + 3 + 1 + 1 + 1 + manual + diff --git a/app/code/core/Mage/Catalog/sql/catalog_setup/install-1.6.0.0.php b/app/code/core/Mage/Catalog/sql/catalog_setup/install-1.6.0.0.php index bb63b86ec6..ab6cdbd098 100644 --- a/app/code/core/Mage/Catalog/sql/catalog_setup/install-1.6.0.0.php +++ b/app/code/core/Mage/Catalog/sql/catalog_setup/install-1.6.0.0.php @@ -470,8 +470,8 @@ 'default' => '0', ), 'Position') ->addColumn('value', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Value') ->addIndex( $installer->getIdxName( @@ -1074,8 +1074,8 @@ 'primary' => true, ), 'Link Type ID') ->addColumn('code', Varien_Db_Ddl_Table::TYPE_TEXT, 32, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Code') ->setComment('Catalog Product Link Type Table'); $installer->getConnection()->createTable($table); @@ -1152,12 +1152,12 @@ 'default' => '0', ), 'Link Type ID') ->addColumn('product_link_attribute_code', Varien_Db_Ddl_Table::TYPE_TEXT, 32, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Product Link Attribute Code') ->addColumn('data_type', Varien_Db_Ddl_Table::TYPE_TEXT, 32, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Data Type') ->addIndex($installer->getIdxName('catalog/product_link_attribute', array('link_type_id')), array('link_type_id')) @@ -1449,8 +1449,8 @@ 'default' => '0', ), 'Product Super Attribute ID') ->addColumn('value_index', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Value Index') ->addColumn('is_percent', Varien_Db_Ddl_Table::TYPE_SMALLINT, null, array( 'unsigned' => true, @@ -1720,8 +1720,8 @@ 'default' => '0', ), 'Product ID') ->addColumn('type', Varien_Db_Ddl_Table::TYPE_TEXT, 50, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Type') ->addColumn('is_require', Varien_Db_Ddl_Table::TYPE_SMALLINT, null, array( 'nullable' => false, @@ -1836,8 +1836,8 @@ 'default' => '0', ), 'Store ID') ->addColumn('title', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Title') ->addIndex( $installer->getIdxName( @@ -1991,8 +1991,8 @@ 'default' => '0', ), 'Store ID') ->addColumn('title', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Title') ->addIndex( $installer->getIdxName( @@ -3093,8 +3093,8 @@ 'default' => '0', ), 'Category ID') ->addColumn('path', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Path') ->addIndex($installer->getIdxName('catalog/category_anchor_indexer_idx', array('category_id')), array('category_id')) @@ -3112,8 +3112,8 @@ 'default' => '0', ), 'Category ID') ->addColumn('path', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Path') ->addIndex($installer->getIdxName('catalog/category_anchor_indexer_tmp', array('category_id')), array('category_id')) diff --git a/app/code/core/Mage/Catalog/sql/catalog_setup/mysql4-upgrade-1.6.0.0.8-1.6.0.0.9.php b/app/code/core/Mage/Catalog/sql/catalog_setup/mysql4-upgrade-1.6.0.0.8-1.6.0.0.9.php new file mode 100644 index 0000000000..3a89478cb0 --- /dev/null +++ b/app/code/core/Mage/Catalog/sql/catalog_setup/mysql4-upgrade-1.6.0.0.8-1.6.0.0.9.php @@ -0,0 +1,53 @@ +getConnection(); +$memoryTables = array( + 'catalog/category_anchor_indexer_tmp', + 'catalog/category_anchor_products_indexer_tmp', + 'catalog/category_product_enabled_indexer_tmp', + 'catalog/category_product_indexer_tmp', + 'catalog/product_eav_decimal_indexer_tmp', + 'catalog/product_eav_indexer_tmp', + 'catalog/product_price_indexer_cfg_option_aggregate_tmp', + 'catalog/product_price_indexer_cfg_option_tmp', + 'catalog/product_price_indexer_final_tmp', + 'catalog/product_price_indexer_option_aggregate_tmp', + 'catalog/product_price_indexer_option_tmp', + 'catalog/product_price_indexer_tmp', +); + +foreach ($memoryTables as $table) { + $connection->changeTableEngine($installer->getTable($table), Varien_Db_Adapter_Pdo_Mysql::ENGINE_MEMORY); +} diff --git a/app/code/core/Mage/Catalog/sql/catalog_setup/upgrade-1.6.0.0.6-1.6.0.0.7.php b/app/code/core/Mage/Catalog/sql/catalog_setup/upgrade-1.6.0.0.6-1.6.0.0.7.php new file mode 100644 index 0000000000..9410d26110 --- /dev/null +++ b/app/code/core/Mage/Catalog/sql/catalog_setup/upgrade-1.6.0.0.6-1.6.0.0.7.php @@ -0,0 +1,39 @@ +getConnection()->addColumn( + $installer->getTable('catalog/category_anchor_products_indexer_tmp'), + 'position', + array( + 'type' => Varien_Db_Ddl_Table::TYPE_INTEGER, + 'unsigned' => true, + 'nullable' => true, + 'comment' => 'Position' + ) +); diff --git a/app/code/core/Mage/Catalog/sql/catalog_setup/upgrade-1.6.0.0.7-1.6.0.0.8.php b/app/code/core/Mage/Catalog/sql/catalog_setup/upgrade-1.6.0.0.7-1.6.0.0.8.php new file mode 100644 index 0000000000..0cb3b8af9f --- /dev/null +++ b/app/code/core/Mage/Catalog/sql/catalog_setup/upgrade-1.6.0.0.7-1.6.0.0.8.php @@ -0,0 +1,97 @@ +getConnection(); + +$connection->addIndex( + $installer->getTable('catalog/category_product_indexer_tmp'), + $installer->getIdxName('catalog/category_product_indexer_tmp', array('product_id', 'category_id', 'store_id')), + array('product_id', 'category_id', 'store_id') +); + +$table = $installer->getTable('catalog/category_product_enabled_indexer_idx'); +$connection->dropIndex($table, 'IDX_CATALOG_CATEGORY_PRODUCT_INDEX_ENBL_IDX_PRODUCT_ID'); +$connection->addIndex( + $table, + $installer->getIdxName('catalog/category_product_enabled_indexer_idx', array('product_id', 'visibility')), + array('product_id', 'visibility') +); + + +$table = $installer->getTable('catalog/category_product_enabled_indexer_tmp'); +$connection->dropIndex($table, 'IDX_CATALOG_CATEGORY_PRODUCT_INDEX_ENBL_TMP_PRODUCT_ID'); +$connection->addIndex( + $table, + $installer->getIdxName('catalog/category_product_enabled_indexer_tmp', array('product_id', 'visibility')), + array('product_id', 'visibility') +); + +$connection->addIndex( + $installer->getTable('catalog/category_anchor_products_indexer_idx'), + $installer->getIdxName( + 'catalog/category_anchor_products_indexer_idx', + array('category_id', 'product_id', 'position') + ), + array('category_id', 'product_id', 'position') +); + +$connection->addIndex( + $installer->getTable('catalog/category_anchor_products_indexer_tmp'), + $installer->getIdxName( + 'catalog/category_anchor_products_indexer_tmp', + array('category_id', 'product_id', 'position') + ), + array('category_id', 'product_id', 'position') +); + +$connection->addIndex( + $installer->getTable('catalog/category_anchor_indexer_idx'), + $installer->getIdxName( + 'catalog/category_anchor_indexer_idx', + array('path', 'category_id') + ), + array('path', 'category_id') +); + +$connection->addIndex( + $installer->getTable('catalog/category_anchor_indexer_tmp'), + $installer->getIdxName( + 'catalog/category_anchor_indexer_tmp', + array('path', 'category_id') + ), + array('path', 'category_id') +); + +$connection->addIndex( + $installer->getTable('catalog/category'), + $installer->getIdxName( + 'catalog/category', + array('path', 'entity_id') + ), + array('path', 'entity_id') +); diff --git a/app/code/core/Mage/Catalog/sql/catalog_setup/upgrade-1.6.0.0.9-1.6.0.0.10.php b/app/code/core/Mage/Catalog/sql/catalog_setup/upgrade-1.6.0.0.9-1.6.0.0.10.php new file mode 100644 index 0000000000..3d9b6b45bd --- /dev/null +++ b/app/code/core/Mage/Catalog/sql/catalog_setup/upgrade-1.6.0.0.9-1.6.0.0.10.php @@ -0,0 +1,214 @@ +getConnection(); + +/** + * Create table 'catalog/product_attribute_group_price' + */ +$table = $installer->getConnection() + ->newTable($installer->getTable('catalog/product_attribute_group_price')) + ->addColumn('value_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array( + 'identity' => true, + 'nullable' => false, + 'primary' => true, + ), 'Value ID') + ->addColumn('entity_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array( + 'unsigned' => true, + 'nullable' => false, + 'default' => '0', + ), 'Entity ID') + ->addColumn('all_groups', Varien_Db_Ddl_Table::TYPE_SMALLINT, null, array( + 'unsigned' => true, + 'nullable' => false, + 'default' => '1', + ), 'Is Applicable To All Customer Groups') + ->addColumn('customer_group_id', Varien_Db_Ddl_Table::TYPE_SMALLINT, null, array( + 'unsigned' => true, + 'nullable' => false, + 'default' => '0', + ), 'Customer Group ID') + ->addColumn('value', Varien_Db_Ddl_Table::TYPE_DECIMAL, '12,4', array( + 'nullable' => false, + 'default' => '0.0000', + ), 'Value') + ->addColumn('website_id', Varien_Db_Ddl_Table::TYPE_SMALLINT, null, array( + 'unsigned' => true, + 'nullable' => false, + ), 'Website ID') + ->addIndex( + $installer->getIdxName( + 'catalog/product_attribute_group_price', + array('entity_id', 'all_groups', 'customer_group_id', 'website_id'), + Varien_Db_Adapter_Interface::INDEX_TYPE_UNIQUE + ), + array('entity_id', 'all_groups', 'customer_group_id', 'website_id'), + array('type' => Varien_Db_Adapter_Interface::INDEX_TYPE_UNIQUE)) + ->addIndex($installer->getIdxName('catalog/product_attribute_group_price', array('entity_id')), + array('entity_id')) + ->addIndex($installer->getIdxName('catalog/product_attribute_group_price', array('customer_group_id')), + array('customer_group_id')) + ->addIndex($installer->getIdxName('catalog/product_attribute_group_price', array('website_id')), + array('website_id')) + ->addForeignKey( + $installer->getFkName( + 'catalog/product_attribute_group_price', + 'customer_group_id', + 'customer/customer_group', + 'customer_group_id' + ), + 'customer_group_id', $installer->getTable('customer/customer_group'), 'customer_group_id', + Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE) + ->addForeignKey( + $installer->getFkName( + 'catalog/product_attribute_group_price', + 'entity_id', + 'catalog/product', + 'entity_id' + ), + 'entity_id', $installer->getTable('catalog/product'), 'entity_id', + Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE) + ->addForeignKey( + $installer->getFkName( + 'catalog/product_attribute_group_price', + 'website_id', + 'core/website', + 'website_id' + ), + 'website_id', $installer->getTable('core/website'), 'website_id', + Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE) + ->setComment('Catalog Product Group Price Attribute Backend Table'); +$installer->getConnection()->createTable($table); + +$installer->addAttribute('catalog_product', 'group_price', array( + 'type' => 'decimal', + 'label' => 'Group Price', + 'input' => 'text', + 'backend' => 'catalog/product_attribute_backend_groupprice', + 'required' => false, + 'sort_order' => 6, + 'global' => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_WEBSITE, + 'apply_to' => 'simple,configurable,virtual', + 'group' => 'Prices', +)); + +/** + * Create table 'catalog/product_index_group_price' + */ +$table = $connection + ->newTable($installer->getTable('catalog/product_index_group_price')) + ->addColumn('entity_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array( + 'unsigned' => true, + 'nullable' => false, + 'primary' => true, + ), 'Entity ID') + ->addColumn('customer_group_id', Varien_Db_Ddl_Table::TYPE_SMALLINT, null, array( + 'unsigned' => true, + 'nullable' => false, + 'primary' => true, + ), 'Customer Group ID') + ->addColumn('website_id', Varien_Db_Ddl_Table::TYPE_SMALLINT, null, array( + 'unsigned' => true, + 'nullable' => false, + 'primary' => true, + ), 'Website ID') + ->addColumn('price', Varien_Db_Ddl_Table::TYPE_DECIMAL, '12,4', array( + ), 'Min Price') + ->addIndex($installer->getIdxName('catalog/product_index_group_price', array('customer_group_id')), + array('customer_group_id')) + ->addIndex($installer->getIdxName('catalog/product_index_group_price', array('website_id')), + array('website_id')) + ->addForeignKey( + $installer->getFkName( + 'catalog/product_index_group_price', + 'customer_group_id', + 'customer/customer_group', + 'customer_group_id' + ), + 'customer_group_id', $installer->getTable('customer/customer_group'), 'customer_group_id', + Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE) + ->addForeignKey( + $installer->getFkName( + 'catalog/product_index_group_price', + 'entity_id', + 'catalog/product', + 'entity_id' + ), + 'entity_id', $installer->getTable('catalog/product'), 'entity_id', + Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE) + ->addForeignKey( + $installer->getFkName( + 'catalog/product_index_group_price', + 'website_id', + 'core/website', + 'website_id' + ), + 'website_id', $installer->getTable('core/website'), 'website_id', + Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE) + ->setComment('Catalog Product Group Price Index Table'); +$connection->createTable($table); + +$finalPriceIndexerTables = array( + 'catalog/product_price_indexer_final_idx', + 'catalog/product_price_indexer_final_tmp', +); + +$priceIndexerTables = array( + 'catalog/product_price_indexer_option_aggregate_idx', + 'catalog/product_price_indexer_option_aggregate_tmp', + 'catalog/product_price_indexer_option_idx', + 'catalog/product_price_indexer_option_tmp', + 'catalog/product_price_indexer_idx', + 'catalog/product_price_indexer_tmp', + 'catalog/product_price_indexer_cfg_option_aggregate_idx', + 'catalog/product_price_indexer_cfg_option_aggregate_tmp', + 'catalog/product_price_indexer_cfg_option_idx', + 'catalog/product_price_indexer_cfg_option_tmp', + 'catalog/product_index_price', +); + +foreach ($finalPriceIndexerTables as $table) { + $connection->addColumn($installer->getTable($table), 'group_price', array( + 'type' => Varien_Db_Ddl_Table::TYPE_DECIMAL, + 'length' => '12,4', + 'comment' => 'Group price', + )); + $connection->addColumn($installer->getTable($table), 'base_group_price', array( + 'type' => Varien_Db_Ddl_Table::TYPE_DECIMAL, + 'length' => '12,4', + 'comment' => 'Base Group Price', + )); +} + +foreach ($priceIndexerTables as $table) { + $connection->addColumn($installer->getTable($table), 'group_price', array( + 'type' => Varien_Db_Ddl_Table::TYPE_DECIMAL, + 'length' => '12,4', + 'comment' => 'Group price', + )); +} diff --git a/app/code/core/Mage/CatalogInventory/Model/Indexer/Stock.php b/app/code/core/Mage/CatalogInventory/Model/Indexer/Stock.php index b3774f91c3..67e69eeba0 100644 --- a/app/code/core/Mage/CatalogInventory/Model/Indexer/Stock.php +++ b/app/code/core/Mage/CatalogInventory/Model/Indexer/Stock.php @@ -47,6 +47,11 @@ */ class Mage_CatalogInventory_Model_Indexer_Stock extends Mage_Index_Model_Indexer_Abstract { + /** + * Data key for matching result to be saved in + */ + const EVENT_MATCH_RESULT_KEY = 'cataloginventory_stock_match_result'; + /** * @var array */ @@ -132,17 +137,15 @@ public function getDescription() public function matchEvent(Mage_Index_Model_Event $event) { $data = $event->getNewData(); - $resultKey = 'cataloginventory_stock_match_result'; - if (isset($data[$resultKey])) { - return $data[$resultKey]; + if (isset($data[self::EVENT_MATCH_RESULT_KEY])) { + return $data[self::EVENT_MATCH_RESULT_KEY]; } - $result = null; $entity = $event->getEntity(); if ($entity == Mage_Core_Model_Store::ENTITY) { /* @var $store Mage_Core_Model_Store */ $store = $event->getDataObject(); - if ($store->isObjectNew()) { + if ($store && $store->isObjectNew()) { $result = true; } else { $result = false; @@ -150,15 +153,14 @@ public function matchEvent(Mage_Index_Model_Event $event) } else if ($entity == Mage_Core_Model_Store_Group::ENTITY) { /* @var $storeGroup Mage_Core_Model_Store_Group */ $storeGroup = $event->getDataObject(); - if ($storeGroup->dataHasChangedFor('website_id')) { + if ($storeGroup && $storeGroup->dataHasChangedFor('website_id')) { $result = true; } else { $result = false; } } else if ($entity == Mage_Core_Model_Config_Data::ENTITY) { $configData = $event->getDataObject(); - $path = $configData->getPath(); - if (in_array($path, $this->_relatedConfigSettings)) { + if ($configData && in_array($configData->getPath(), $this->_relatedConfigSettings)) { $result = $configData->isValueChanged(); } else { $result = false; @@ -167,7 +169,7 @@ public function matchEvent(Mage_Index_Model_Event $event) $result = parent::matchEvent($event); } - $event->addNewData($resultKey, $result); + $event->addNewData(self::EVENT_MATCH_RESULT_KEY, $result); return $result; } @@ -179,6 +181,7 @@ public function matchEvent(Mage_Index_Model_Event $event) */ protected function _registerEvent(Mage_Index_Model_Event $event) { + $event->addNewData(self::EVENT_MATCH_RESULT_KEY, true); switch ($event->getEntity()) { case Mage_CatalogInventory_Model_Stock_Item::ENTITY: $this->_registerCatalogInventoryStockItemEvent($event); diff --git a/app/code/core/Mage/CatalogInventory/Model/Mysql4/Indexer/Stock/Interface.php b/app/code/core/Mage/CatalogInventory/Model/Mysql4/Indexer/Stock/Interface.php index cf91ab2b2d..fdd6ec2d85 100644 --- a/app/code/core/Mage/CatalogInventory/Model/Mysql4/Indexer/Stock/Interface.php +++ b/app/code/core/Mage/CatalogInventory/Model/Mysql4/Indexer/Stock/Interface.php @@ -32,7 +32,7 @@ * @package Mage_CatalogInventory * @author Magento Core Team */ -class Mage_CatalogInventory_Model_Mysql4_Indexer_Stock_Interface +interface Mage_CatalogInventory_Model_Mysql4_Indexer_Stock_Interface extends Mage_CatalogInventory_Model_Resource_Indexer_Stock_Interface { } diff --git a/app/code/core/Mage/CatalogInventory/Model/Observer.php b/app/code/core/Mage/CatalogInventory/Model/Observer.php index 028db33176..9e34f05ae1 100644 --- a/app/code/core/Mage/CatalogInventory/Model/Observer.php +++ b/app/code/core/Mage/CatalogInventory/Model/Observer.php @@ -700,9 +700,11 @@ public function reindexQuoteInventory($observer) */ public function refundOrderInventory($observer) { + /* @var $creditmemo Mage_Sales_Model_Order_Creditmemo */ $creditmemo = $observer->getEvent()->getCreditmemo(); $items = array(); foreach ($creditmemo->getAllItems() as $item) { + /* @var $item Mage_Sales_Model_Order_Creditmemo_Item */ $return = false; if ($item->hasBackToStock()) { if ($item->getBackToStock() && $item->getQty()) { @@ -712,11 +714,15 @@ public function refundOrderInventory($observer) $return = true; } if ($return) { + $parentOrderId = $item->getOrderItem()->getParentItemId(); + /* @var $parentItem Mage_Sales_Model_Order_Creditmemo_Item */ + $parentItem = $parentOrderId ? $creditmemo->getItemByOrderId($parentOrderId) : false; + $qty = $parentItem ? ($parentItem->getQty() * $item->getQty()) : $item->getQty(); if (isset($items[$item->getProductId()])) { - $items[$item->getProductId()]['qty'] += $item->getQty(); + $items[$item->getProductId()]['qty'] += $qty; } else { $items[$item->getProductId()] = array( - 'qty' => $item->getQty(), + 'qty' => $qty, 'item'=> null, ); } @@ -904,4 +910,15 @@ public function refundOrderItem($observer) return $this; } + /** + * Reindex all events of product-massAction type + * + * @param Varien_Event_Observer $observer + */ + public function reindexProductsMassAction($observer) + { + Mage::getSingleton('index/indexer')->indexEvents( + Mage_Catalog_Model_Product::ENTITY, Mage_Index_Model_Event::TYPE_MASS_ACTION + ); + } } diff --git a/app/code/core/Mage/CatalogInventory/Model/Resource/Indexer/Stock.php b/app/code/core/Mage/CatalogInventory/Model/Resource/Indexer/Stock.php index 548439ad73..2c57c16314 100755 --- a/app/code/core/Mage/CatalogInventory/Model/Resource/Indexer/Stock.php +++ b/app/code/core/Mage/CatalogInventory/Model/Resource/Indexer/Stock.php @@ -80,7 +80,7 @@ public function cataloginventoryStockItemSave(Mage_Index_Model_Event $event) 'force_reindex_required' => 1 )); $massObject->setProductIds(array($productId)); - Mage::getSingleton('index/indexer')->processEntityAction( + Mage::getSingleton('index/indexer')->logEvent( $massObject, Mage_Catalog_Model_Product::ENTITY, Mage_Index_Model_Event::TYPE_MASS_ACTION ); } @@ -251,13 +251,20 @@ public function catalogProductMassAction(Mage_Index_Model_Event $event) public function reindexAll() { $this->useIdxTable(true); - $this->clearTemporaryIndexTable(); + $this->beginTransaction(); + try { + $this->clearTemporaryIndexTable(); - foreach ($this->_getTypeIndexers() as $indexer) { - $indexer->reindexAll(); - } + foreach ($this->_getTypeIndexers() as $indexer) { + $indexer->reindexAll(); + } - $this->syncData(); + $this->syncData(); + $this->commit(); + } catch (Exception $e) { + $this->rollBack(); + throw $e; + } return $this; } diff --git a/app/code/core/Mage/CatalogInventory/Model/Resource/Indexer/Stock/Configurable.php b/app/code/core/Mage/CatalogInventory/Model/Resource/Indexer/Stock/Configurable.php index b1d282a283..8737af64d6 100755 --- a/app/code/core/Mage/CatalogInventory/Model/Resource/Indexer/Stock/Configurable.php +++ b/app/code/core/Mage/CatalogInventory/Model/Resource/Indexer/Stock/Configurable.php @@ -35,18 +35,6 @@ class Mage_CatalogInventory_Model_Resource_Indexer_Stock_Configurable extends Mage_CatalogInventory_Model_Resource_Indexer_Stock_Default { - /** - * Reindex all stock status data for configurable products - * - * @return Mage_CatalogInventory_Model_Resource_Indexer_Stock_Configurable - */ - public function reindexAll() - { - $this->useIdxTable(true); - $this->_prepareIndexTable(); - return $this; - } - /** * Reindex stock data for defined configurable product ids * diff --git a/app/code/core/Mage/CatalogInventory/Model/Resource/Indexer/Stock/Default.php b/app/code/core/Mage/CatalogInventory/Model/Resource/Indexer/Stock/Default.php index 6688e3f5dc..85a5c5f456 100755 --- a/app/code/core/Mage/CatalogInventory/Model/Resource/Indexer/Stock/Default.php +++ b/app/code/core/Mage/CatalogInventory/Model/Resource/Indexer/Stock/Default.php @@ -67,7 +67,14 @@ protected function _construct() public function reindexAll() { $this->useIdxTable(true); - $this->_prepareIndexTable(); + $this->beginTransaction(); + try { + $this->_prepareIndexTable(); + $this->commit(); + } catch (Exception $e) { + $this->rollBack(); + throw $e; + } return $this; } diff --git a/app/code/core/Mage/CatalogInventory/Model/Resource/Indexer/Stock/Grouped.php b/app/code/core/Mage/CatalogInventory/Model/Resource/Indexer/Stock/Grouped.php index 076fd280c3..b3f8c38008 100755 --- a/app/code/core/Mage/CatalogInventory/Model/Resource/Indexer/Stock/Grouped.php +++ b/app/code/core/Mage/CatalogInventory/Model/Resource/Indexer/Stock/Grouped.php @@ -35,18 +35,6 @@ class Mage_CatalogInventory_Model_Resource_Indexer_Stock_Grouped extends Mage_CatalogInventory_Model_Resource_Indexer_Stock_Default { - /** - * Reindex all stock status data for configurable products - * - * @return Mage_CatalogInventory_Model_Resource_Indexer_Stock_Grouped - */ - public function reindexAll() - { - $this->useIdxTable(true); - $this->_prepareIndexTable(); - return $this; - } - /** * Reindex stock data for defined configurable product ids * diff --git a/app/code/core/Mage/CatalogInventory/Model/Stock/Item.php b/app/code/core/Mage/CatalogInventory/Model/Stock/Item.php index 8088314f3a..0113ae920d 100644 --- a/app/code/core/Mage/CatalogInventory/Model/Stock/Item.php +++ b/app/code/core/Mage/CatalogInventory/Model/Stock/Item.php @@ -128,6 +128,13 @@ class Mage_CatalogInventory_Model_Stock_Item extends Mage_Core_Model_Abstract */ protected $_customerGroupId; + /** + * Whether index events should be processed immediately + * + * @var bool + */ + protected $_processIndexEvents = true; + /** * Initialize resource model * @@ -454,7 +461,8 @@ public function checkQty($qty) if (!$this->getManageStock() || Mage::app()->getStore()->isAdmin()) { return true; } - if ($this->getQty() - $qty < 0) { + + if ($this->getQty() - $this->getMinQty() - $qty < 0) { switch ($this->getBackorders()) { case Mage_CatalogInventory_Model_Stock::BACKORDERS_YES_NONOTIFY: case Mage_CatalogInventory_Model_Stock::BACKORDERS_YES_NOTIFY: @@ -529,9 +537,6 @@ public function checkQuoteItemQty($qty, $summaryQty, $origQty = 0) $result = new Varien_Object(); $result->setHasError(false); - /** @var $_helper Mage_CatalogInventory_Helper_Data */ - $_helper = Mage::helper('cataloginventory'); - if (!is_numeric($qty)) { $qty = Mage::app()->getLocale()->getNumber($qty); } @@ -560,9 +565,9 @@ public function checkQuoteItemQty($qty, $summaryQty, $origQty = 0) if ($this->getMinSaleQty() && ($qty) < $this->getMinSaleQty()) { $result->setHasError(true) ->setMessage( - $_helper->__('The minimum quantity allowed for purchase is %s.', $this->getMinSaleQty() * 1) + Mage::helper('cataloginventory')->__('The minimum quantity allowed for purchase is %s.', $this->getMinSaleQty() * 1) ) - ->setQuoteMessage($_helper->__('Some of the products cannot be ordered in requested quantity.')) + ->setQuoteMessage(Mage::helper('cataloginventory')->__('Some of the products cannot be ordered in requested quantity.')) ->setQuoteMessageIndex('qty'); return $result; } @@ -570,9 +575,9 @@ public function checkQuoteItemQty($qty, $summaryQty, $origQty = 0) if ($this->getMaxSaleQty() && ($qty) > $this->getMaxSaleQty()) { $result->setHasError(true) ->setMessage( - $_helper->__('The maximum quantity allowed for purchase is %s.', $this->getMaxSaleQty() * 1) + Mage::helper('cataloginventory')->__('The maximum quantity allowed for purchase is %s.', $this->getMaxSaleQty() * 1) ) - ->setQuoteMessage($_helper->__('Some of the products cannot be ordered in requested quantity.')) + ->setQuoteMessage(Mage::helper('cataloginventory')->__('Some of the products cannot be ordered in requested quantity.')) ->setQuoteMessageIndex('qty'); return $result; } @@ -589,15 +594,15 @@ public function checkQuoteItemQty($qty, $summaryQty, $origQty = 0) if (!$this->getIsInStock()) { $result->setHasError(true) - ->setMessage($_helper->__('This product is currently out of stock.')) - ->setQuoteMessage($_helper->__('Some of the products are currently out of stock')) + ->setMessage(Mage::helper('cataloginventory')->__('This product is currently out of stock.')) + ->setQuoteMessage(Mage::helper('cataloginventory')->__('Some of the products are currently out of stock')) ->setQuoteMessageIndex('stock'); $result->setItemUseOldQty(true); return $result; } if (!$this->checkQty($summaryQty) || !$this->checkQty($qty)) { - $message = $_helper->__('The requested quantity for "%s" is not available.', $this->getProductName()); + $message = Mage::helper('cataloginventory')->__('The requested quantity for "%s" is not available.', $this->getProductName()); $result->setHasError(true) ->setMessage($message) ->setQuoteMessage($message) @@ -627,16 +632,16 @@ public function checkQuoteItemQty($qty, $summaryQty, $origQty = 0) if ($this->getBackorders() == Mage_CatalogInventory_Model_Stock::BACKORDERS_YES_NOTIFY) { if (!$this->getIsChildItem()) { $result->setMessage( - $_helper->__('This product is not available in the requested quantity. %s of the items will be backordered.', ($backorderQty * 1)) + Mage::helper('cataloginventory')->__('This product is not available in the requested quantity. %s of the items will be backordered.', ($backorderQty * 1)) ); } else { $result->setMessage( - $_helper->__('"%s" is not available in the requested quantity. %s of the items will be backordered.', $this->getProductName(), ($backorderQty * 1)) + Mage::helper('cataloginventory')->__('"%s" is not available in the requested quantity. %s of the items will be backordered.', $this->getProductName(), ($backorderQty * 1)) ); } } elseif (Mage::app()->getStore()->isAdmin()) { $result->setMessage( - $_helper->__('The requested quantity for "%s" is not available.', $this->getProductName()) + Mage::helper('cataloginventory')->__('The requested quantity for "%s" is not available.', $this->getProductName()) ); } } @@ -792,16 +797,21 @@ public function verifyNotification($qty = null) } /** - * Process stock status index on item after commit + * Reindex CatalogInventory save event * * @return Mage_CatalogInventory_Model_Stock_Item */ - public function afterCommitCallback() + protected function _afterSave() { - parent::afterCommitCallback(); - Mage::getSingleton('index/indexer')->processEntityAction( - $this, self::ENTITY, Mage_Index_Model_Event::TYPE_SAVE - ); + parent::_afterSave(); + + /** @var $indexer Mage_Index_Model_Indexer */ + $indexer = Mage::getSingleton('index/indexer'); + if ($this->_processIndexEvents) { + $indexer->processEntityAction($this, self::ENTITY, Mage_Index_Model_Event::TYPE_SAVE); + } else { + $indexer->logEvent($this, self::ENTITY, Mage_Index_Model_Event::TYPE_SAVE); + } return $this; } @@ -899,4 +909,16 @@ public function reset() } return $this; } + + /** + * Set whether index events should be processed immediately + * + * @param bool $process + * @return Mage_CatalogInventory_Model_Stock_Item + */ + public function setProcessIndexEvents($process = true) + { + $this->_processIndexEvents = $process; + return $this; + } } diff --git a/app/code/core/Mage/CatalogInventory/Model/System/Config/Backend/Minqty.php b/app/code/core/Mage/CatalogInventory/Model/System/Config/Backend/Minqty.php new file mode 100644 index 0000000000..e4c5b0e3c2 --- /dev/null +++ b/app/code/core/Mage/CatalogInventory/Model/System/Config/Backend/Minqty.php @@ -0,0 +1,48 @@ + + */ +class Mage_CatalogInventory_Model_System_Config_Backend_Minqty extends Mage_Core_Model_Config_Data +{ + /** + * Validate minimum product qty value + * + * @return Mage_CatalogInventory_Model_System_Config_Backend_Minqty + */ + protected function _beforeSave() + { + parent::_beforeSave(); + $minQty = (int)$this->getValue() >= 0 ? (int)$this->getValue() : (int)$this->getOldValue(); + $this->setValue((string) $minQty); + return $this; + } +} diff --git a/app/code/core/Mage/CatalogInventory/etc/config.xml b/app/code/core/Mage/CatalogInventory/etc/config.xml index 60cd6ca9cf..1e7354899e 100644 --- a/app/code/core/Mage/CatalogInventory/etc/config.xml +++ b/app/code/core/Mage/CatalogInventory/etc/config.xml @@ -28,7 +28,7 @@ - 1.6.0.0 + 1.6.0.0.1 @@ -206,6 +206,22 @@ + + + + cataloginventory/observer + reindexProductsMassAction + + + + + + + cataloginventory/observer + reindexProductsMassAction + + + @@ -271,6 +287,16 @@ + + + + + cataloginventory/observer + reindexProductsMassAction + + + + diff --git a/app/code/core/Mage/CatalogInventory/etc/system.xml b/app/code/core/Mage/CatalogInventory/etc/system.xml index 80e11f659a..a2422844c8 100644 --- a/app/code/core/Mage/CatalogInventory/etc/system.xml +++ b/app/code/core/Mage/CatalogInventory/etc/system.xml @@ -62,7 +62,7 @@ 1 1 - + select adminhtml/system_config_source_yesno @@ -120,6 +120,7 @@ text + cataloginventory/system_config_backend_minqty 5 1 0 @@ -168,6 +169,7 @@ 1 0 0 + validate-per-page-value-list 1 diff --git a/app/code/core/Mage/CatalogInventory/sql/cataloginventory_setup/mysql4-upgrade-1.6.0.0-1.6.0.0.1.php b/app/code/core/Mage/CatalogInventory/sql/cataloginventory_setup/mysql4-upgrade-1.6.0.0-1.6.0.0.1.php new file mode 100644 index 0000000000..8c34a13788 --- /dev/null +++ b/app/code/core/Mage/CatalogInventory/sql/cataloginventory_setup/mysql4-upgrade-1.6.0.0-1.6.0.0.1.php @@ -0,0 +1,34 @@ +getConnection(); +$connection->changeTableEngine( + $installer->getTable('cataloginventory/stock_status_indexer_tmp'), + Varien_Db_Adapter_Pdo_Mysql::ENGINE_MEMORY +); diff --git a/app/code/core/Mage/CatalogRule/Model/Rule.php b/app/code/core/Mage/CatalogRule/Model/Rule.php index 0301f02702..99f2cb4073 100644 --- a/app/code/core/Mage/CatalogRule/Model/Rule.php +++ b/app/code/core/Mage/CatalogRule/Model/Rule.php @@ -343,7 +343,11 @@ public function applyAllRulesToProduct($product) } if ($productId) { - Mage::getResourceSingleton('catalog/product_indexer_price')->reindexProductIds(array($productId)); + Mage::getSingleton('index/indexer')->processEntityAction( + new Varien_Object(array('id' => $productId)), + Mage_Catalog_Model_Product::ENTITY, + Mage_Catalog_Model_Product_Indexer_Price::EVENT_TYPE_REINDEX_PRICE + ); } } diff --git a/app/code/core/Mage/CatalogSearch/Helper/Data.php b/app/code/core/Mage/CatalogSearch/Helper/Data.php index e4f0584ba6..3e0e962e0a 100644 --- a/app/code/core/Mage/CatalogSearch/Helper/Data.php +++ b/app/code/core/Mage/CatalogSearch/Helper/Data.php @@ -301,10 +301,7 @@ public function checkNotes($store = null) $wordsCut = array_map(array($this, 'htmlEscape'), $wordsCut); $this->addNoteMessage( - $this->__('Maximum words count is %1$s. In your search query was cut next part: %2$s.', - $this->getMaxQueryWords(), - join(' ', $wordsCut) - ) + $this->__('Maximum words count is %1$s. In your search query was cut next part: %2$s.', $this->getMaxQueryWords(), join(' ', $wordsCut)) ); } } diff --git a/app/code/core/Mage/CatalogSearch/Model/Fulltext.php b/app/code/core/Mage/CatalogSearch/Model/Fulltext.php index 12e3d7370c..34f79921c4 100644 --- a/app/code/core/Mage/CatalogSearch/Model/Fulltext.php +++ b/app/code/core/Mage/CatalogSearch/Model/Fulltext.php @@ -50,6 +50,7 @@ class Mage_CatalogSearch_Model_Fulltext extends Mage_Core_Model_Abstract /** * Whether table changes are allowed * + * @deprecated after 1.6.1.0 * @var bool */ protected $_allowTableChanges = true; @@ -79,14 +80,7 @@ public function rebuildIndex($storeId = null, $productIds = null) 'product_ids' => $productIds )); - $resourceModel = $this->getResource(); - if (!$this->_allowTableChanges && is_callable(array($resourceModel, 'setAllowTableChanges'))) { - $resourceModel->setAllowTableChanges(false); - } - $resourceModel->rebuildIndex($storeId, $productIds); - if (!$this->_allowTableChanges && is_callable(array($resourceModel, 'setAllowTableChanges'))) { - $resourceModel->setAllowTableChanges(true); - } + $this->getResource()->rebuildIndex($storeId, $productIds); Mage::dispatchEvent('catalogsearch_index_process_complete', array()); @@ -119,15 +113,7 @@ public function cleanIndex($storeId = null, $productId = null) */ public function resetSearchResults() { - $resourceModel = $this->getResource(); - if (!$this->_allowTableChanges && is_callable(array($resourceModel, 'setAllowTableChanges'))) { - $resourceModel->setAllowTableChanges(false); - } - $resourceModel->resetSearchResults(); - if (!$this->_allowTableChanges && is_callable(array($resourceModel, 'setAllowTableChanges'))) { - $resourceModel->setAllowTableChanges(true); - } - + $this->getResource()->resetSearchResults(); return $this; } @@ -177,6 +163,7 @@ public function updateCategoryIndex($productIds, $categoryIds) /** * Set whether table changes are allowed * + * @deprecated after 1.6.1.0 * @param bool $value * @return Mage_CatalogSearch_Model_Fulltext */ diff --git a/app/code/core/Mage/CatalogSearch/Model/Indexer/Fulltext.php b/app/code/core/Mage/CatalogSearch/Model/Indexer/Fulltext.php index f7c813257d..fc19b63bc1 100644 --- a/app/code/core/Mage/CatalogSearch/Model/Indexer/Fulltext.php +++ b/app/code/core/Mage/CatalogSearch/Model/Indexer/Fulltext.php @@ -34,6 +34,18 @@ */ class Mage_CatalogSearch_Model_Indexer_Fulltext extends Mage_Index_Model_Indexer_Abstract { + /** + * Data key for matching result to be saved in + */ + const EVENT_MATCH_RESULT_KEY = 'catalogsearch_fulltext_match_result'; + + /** + * List of searchable attributes + * + * @var null|array + */ + protected $_searchableAttributes = null; + /** * Retrieve resource instance * @@ -127,20 +139,20 @@ public function getDescription() public function matchEvent(Mage_Index_Model_Event $event) { $data = $event->getNewData(); - $resultKey = 'catalogsearch_fulltext_match_result'; - if (isset($data[$resultKey])) { - return $data[$resultKey]; + if (isset($data[self::EVENT_MATCH_RESULT_KEY])) { + return $data[self::EVENT_MATCH_RESULT_KEY]; } - $result = null; $entity = $event->getEntity(); if ($entity == Mage_Catalog_Model_Resource_Eav_Attribute::ENTITY) { /* @var $attribute Mage_Catalog_Model_Resource_Eav_Attribute */ $attribute = $event->getDataObject(); - if ($event->getType() == Mage_Index_Model_Event::TYPE_SAVE) { + if (!$attribute) { + $result = false; + } elseif ($event->getType() == Mage_Index_Model_Event::TYPE_SAVE) { $result = $attribute->dataHasChangedFor('is_searchable'); - } else if ($event->getType() == Mage_Index_Model_Event::TYPE_DELETE) { + } elseif ($event->getType() == Mage_Index_Model_Event::TYPE_DELETE) { $result = $attribute->getIsSearchable(); } else { $result = false; @@ -151,7 +163,7 @@ public function matchEvent(Mage_Index_Model_Event $event) } else { /* @var $store Mage_Core_Model_Store */ $store = $event->getDataObject(); - if ($store->isObjectNew()) { + if ($store && $store->isObjectNew()) { $result = true; } else { $result = false; @@ -160,14 +172,14 @@ public function matchEvent(Mage_Index_Model_Event $event) } else if ($entity == Mage_Core_Model_Store_Group::ENTITY) { /* @var $storeGroup Mage_Core_Model_Store_Group */ $storeGroup = $event->getDataObject(); - if ($storeGroup->dataHasChangedFor('website_id')) { + if ($storeGroup && $storeGroup->dataHasChangedFor('website_id')) { $result = true; } else { $result = false; } } else if ($entity == Mage_Core_Model_Config_Data::ENTITY) { $data = $event->getDataObject(); - if (in_array($data->getPath(), $this->_relatedConfigSettings)) { + if ($data && in_array($data->getPath(), $this->_relatedConfigSettings)) { $result = $data->isValueChanged(); } else { $result = false; @@ -176,7 +188,7 @@ public function matchEvent(Mage_Index_Model_Event $event) $result = parent::matchEvent($event); } - $event->addNewData($resultKey, $result); + $event->addNewData(self::EVENT_MATCH_RESULT_KEY, $result); return $result; } @@ -188,6 +200,7 @@ public function matchEvent(Mage_Index_Model_Event $event) */ protected function _registerEvent(Mage_Index_Model_Event $event) { + $event->addNewData(self::EVENT_MATCH_RESULT_KEY, true); switch ($event->getEntity()) { case Mage_Catalog_Model_Product::ENTITY: $this->_registerCatalogProductEvent($event); @@ -282,6 +295,16 @@ protected function _registerCatalogProductEvent(Mage_Index_Model_Event $event) $reindexData['catalogsearch_action_type'] = $actionObject->getActionType(); } + $searchableAttributes = array(); + if (is_array($attrData)) { + $searchableAttributes = array_intersect($this->_getSearchableAttributes(), array_keys($attrData)); + } + + if (count($searchableAttributes) > 0) { + $rebuildIndex = true; + $reindexData['catalogsearch_force_reindex'] = true; + } + // register affected products if ($rebuildIndex) { $reindexData['catalogsearch_product_ids'] = $actionObject->getProductIds(); @@ -295,6 +318,26 @@ protected function _registerCatalogProductEvent(Mage_Index_Model_Event $event) return $this; } + /** + * Retrieve searchable attributes list + * + * @return array + */ + protected function _getSearchableAttributes() + { + if (is_null($this->_searchableAttributes)) { + /** @var $attributeCollection Mage_Catalog_Model_Resource_Product_Attribute_Collection */ + $attributeCollection = Mage::getResourceModel('catalog/product_attribute_collection'); + $attributeCollection->addIsSearchableFilter(); + + foreach ($attributeCollection as $attribute) { + $this->_searchableAttributes[] = $attribute->getAttributeCode(); + } + } + + return $this->_searchableAttributes; + } + /** * Check if product is composite * @@ -314,9 +357,6 @@ protected function _isProductComposite($productId) */ protected function _processEvent(Mage_Index_Model_Event $event) { - if (!$this->_allowTableChanges && is_callable(array($this->_getIndexer(), 'setAllowTableChanges'))) { - $this->_getIndexer()->setAllowTableChanges(false); - } $data = $event->getNewData(); if (!empty($data['catalogsearch_fulltext_reindex_all'])) { @@ -380,6 +420,11 @@ protected function _processEvent(Mage_Index_Model_Event $event) ->resetSearchResults(); } } + if (isset($data['catalogsearch_force_reindex'])) { + $this->_getIndexer() + ->rebuildIndex(null, $productIds) + ->resetSearchResults(); + } } else if (isset($data['catalogsearch_category_update_product_ids'])) { $productIds = $data['catalogsearch_category_update_product_ids']; $categoryIds = $data['catalogsearch_category_update_category_ids']; @@ -387,9 +432,6 @@ protected function _processEvent(Mage_Index_Model_Event $event) $this->_getIndexer() ->updateCategoryIndex($productIds, $categoryIds); } - if (!$this->_allowTableChanges && is_callable(array($this->_getIndexer(), 'setAllowTableChanges'))) { - $this->_getIndexer()->setAllowTableChanges(true); - } } /** @@ -398,6 +440,14 @@ protected function _processEvent(Mage_Index_Model_Event $event) */ public function reindexAll() { - $this->_getIndexer()->rebuildIndex(); + $resourceModel = $this->_getIndexer()->getResource(); + $resourceModel->beginTransaction(); + try { + $this->_getIndexer()->rebuildIndex(); + $resourceModel->commit(); + } catch (Exception $e) { + $resourceModel->rollBack(); + throw $e; + } } } diff --git a/app/code/core/Mage/CatalogSearch/Model/Resource/Fulltext.php b/app/code/core/Mage/CatalogSearch/Model/Resource/Fulltext.php index 01f27a0c6d..80ce6cf8a4 100755 --- a/app/code/core/Mage/CatalogSearch/Model/Resource/Fulltext.php +++ b/app/code/core/Mage/CatalogSearch/Model/Resource/Fulltext.php @@ -72,6 +72,7 @@ class Mage_CatalogSearch_Model_Resource_Fulltext extends Mage_Core_Model_Resourc /** * Whether table changes are allowed * + * @deprecated after 1.6.1.0 * @var bool */ protected $_allowTableChanges = true; @@ -117,7 +118,7 @@ public function updateCategoryIndex($productIds, $categoryIds) * * @param int|null $storeId * @param int|array|null $productIds - * @return Mage_CatalogSearch_Model_Mysql4_Fulltext + * @return Mage_CatalogSearch_Model_Resource_Fulltext */ public function rebuildIndex($storeId = null, $productIds = null) { @@ -301,11 +302,7 @@ public function resetSearchResults() { $adapter = $this->_getWriteAdapter(); $adapter->update($this->getTable('catalogsearch/search_query'), array('is_processed' => 0)); - if ($this->_allowTableChanges) { - $adapter->truncateTable($this->getTable('catalogsearch/result')); - } else { - $adapter->delete($this->getTable('catalogsearch/result')); - } + $adapter->delete($this->getTable('catalogsearch/result')); Mage::dispatchEvent('catalogsearch_reset_search_result'); @@ -632,21 +629,16 @@ protected function _prepareProductIndex($indexData, $productData, $storeId) } } - foreach ($indexData as $attributeData) { + foreach ($indexData as $entityId => $attributeData) { foreach ($attributeData as $attributeId => $attributeValue) { $value = $this->_getAttributeValue($attributeId, $attributeValue, $storeId); if (!is_null($value) && $value !== false) { $code = $this->_getSearchableAttribute($attributeId)->getAttributeCode(); - //For grouped products + if (isset($index[$code])) { - if (!is_array($index[$code])) { - $index[$code] = array($index[$code]); - } - $index[$code][] = $value; - } - //For other types of products - else { - $index[$code] = $value; + $index[$code][$entityId] = $value; + } else { + $index[$code] = array($entityId => $value); } } } @@ -687,10 +679,12 @@ protected function _prepareProductIndex($indexData, $productData, $storeId) protected function _getAttributeValue($attributeId, $value, $storeId) { $attribute = $this->_getSearchableAttribute($attributeId); - if (!($attribute->getIsSearchable() || - $attribute->getIsVisibleInAdvancedSearch() || - $attribute->getIsFilterable() || - $attribute->getIsFilterableInSearch())) { + if (!($attribute->getIsSearchable() + || $attribute->getIsVisibleInAdvancedSearch() + || $attribute->getIsFilterable() + || $attribute->getIsFilterableInSearch() + || $attribute->getUsedForSortBy()) + ) { return null; } @@ -779,6 +773,7 @@ protected function _getStoreDate($storeId, $date = null) /** * Set whether table changes are allowed * + * @deprecated after 1.6.1.0 * @param bool $value * @return Mage_CatalogSearch_Model_Resource_Fulltext */ diff --git a/app/code/core/Mage/Centinel/Block/Adminhtml/Validation.php b/app/code/core/Mage/Centinel/Block/Adminhtml/Validation.php index cc90f6e083..15b009031b 100644 --- a/app/code/core/Mage/Centinel/Block/Adminhtml/Validation.php +++ b/app/code/core/Mage/Centinel/Block/Adminhtml/Validation.php @@ -66,7 +66,11 @@ public function getHeaderCssClass() protected function _toHtml() { $payment = $this->getQuote()->getPayment(); - if (!$payment->getMethod() || !$payment->getMethodInstance() || !$payment->getMethodInstance()->getIsCentinelValidationEnabled()) { + if (!$payment->getMethod() + || !$payment->getMethodInstance() + || $payment->getMethodInstance()->getIsDummy() + || !$payment->getMethodInstance()->getIsCentinelValidationEnabled()) + { return ''; } return parent::_toHtml(); diff --git a/app/code/core/Mage/Checkout/Block/Cart/Shipping.php b/app/code/core/Mage/Checkout/Block/Cart/Shipping.php index 26fac6ccd5..67c5ddc1d0 100644 --- a/app/code/core/Mage/Checkout/Block/Cart/Shipping.php +++ b/app/code/core/Mage/Checkout/Block/Cart/Shipping.php @@ -27,10 +27,30 @@ class Mage_Checkout_Block_Cart_Shipping extends Mage_Checkout_Block_Cart_Abstract { + /** + * Available Carriers Instances + * @var null|array + */ protected $_carriers = null; + + /** + * Estimate Rates + * @var array + */ protected $_rates = array(); + + /** + * Address Model + * + * @var array + */ protected $_address = array(); + /** + * Get Estimate Rates + * + * @return array + */ public function getEstimateRates() { if (empty($this->_rates)) { @@ -41,7 +61,7 @@ public function getEstimateRates() } /** - * Get address model + * Get Address Model * * @return Mage_Sales_Model_Quote_Address */ @@ -53,6 +73,12 @@ public function getAddress() return $this->_address; } + /** + * Get Carrier Name + * + * @param string $carrierCode + * @return mixed + */ public function getCarrierName($carrierCode) { if ($name = Mage::getStoreConfig('carriers/'.$carrierCode.'/title')) { @@ -61,51 +87,107 @@ public function getCarrierName($carrierCode) return $carrierCode; } + /** + * Get Shipping Method + * + * @return string + */ public function getAddressShippingMethod() { return $this->getAddress()->getShippingMethod(); } + /** + * Get Estimate Country Id + * + * @return string + */ public function getEstimateCountryId() { return $this->getAddress()->getCountryId(); } + /** + * Get Estimate Postcode + * + * @return string + */ public function getEstimatePostcode() { return $this->getAddress()->getPostcode(); } + /** + * Get Estimate City + * + * @return string + */ public function getEstimateCity() { return $this->getAddress()->getCity(); } + /** + * Get Estimate Region Id + * + * @return mixed + */ public function getEstimateRegionId() { return $this->getAddress()->getRegionId(); } + /** + * Get Estimate Region + * + * @return string + */ public function getEstimateRegion() { return $this->getAddress()->getRegion(); } + /** + * Show City in Shipping Estimation + * + * @return bool + */ public function getCityActive() { - return (bool)Mage::getStoreConfig('carriers/dhl/active'); + return (bool)Mage::getStoreConfig('carriers/dhl/active') + || (bool)Mage::getStoreConfig('carriers/dhlint/active'); } + /** + * Show State in Shipping Estimation + * + * @return bool + */ public function getStateActive() { - return (bool)Mage::getStoreConfig('carriers/dhl/active') || (bool)Mage::getStoreConfig('carriers/tablerate/active'); + return (bool)Mage::getStoreConfig('carriers/dhl/active') + || (bool)Mage::getStoreConfig('carriers/tablerate/active') + || (bool)Mage::getStoreConfig('carriers/dhlint/active'); } + /** + * Convert price from default currency to current currency + * + * @param float $price + * @return float + */ public function formatPrice($price) { return $this->getQuote()->getStore()->convertPrice($price, true); } + /** + * Get Shipping Price + * + * @param float $price + * @param bool $flag + * @return float + */ public function getShippingPrice($price, $flag) { return $this->formatPrice($this->helper('tax')->getShippingPrice( diff --git a/app/code/core/Mage/Checkout/Block/Onepage.php b/app/code/core/Mage/Checkout/Block/Onepage.php index 7b9abee9ee..40bc4c76a9 100644 --- a/app/code/core/Mage/Checkout/Block/Onepage.php +++ b/app/code/core/Mage/Checkout/Block/Onepage.php @@ -33,19 +33,24 @@ */ class Mage_Checkout_Block_Onepage extends Mage_Checkout_Block_Onepage_Abstract { + /** + * Get 'one step checkout' step data + * + * @return array + */ public function getSteps() { $steps = array(); + $stepCodes = $this->_getStepCodes(); - if (!$this->isCustomerLoggedIn()) { - $steps['login'] = $this->getCheckout()->getStepData('login'); + if ($this->isCustomerLoggedIn()) { + $stepCodes = array_diff($stepCodes, array('login')); } - $stepCodes = array('billing', 'shipping', 'shipping_method', 'payment', 'review'); - foreach ($stepCodes as $step) { $steps[$step] = $this->getCheckout()->getStepData($step); } + return $steps; } diff --git a/app/code/core/Mage/Checkout/Block/Onepage/Abstract.php b/app/code/core/Mage/Checkout/Block/Onepage/Abstract.php index 50bd5e871f..50b7018dd4 100644 --- a/app/code/core/Mage/Checkout/Block/Onepage/Abstract.php +++ b/app/code/core/Mage/Checkout/Block/Onepage/Abstract.php @@ -200,6 +200,17 @@ public function getCountryOptions() return $options; } + /** + * Get checkout steps codes + * + * @return array + */ + protected function _getStepCodes() + { + return array('login', 'billing', 'shipping', 'shipping_method', 'payment', 'review'); + } + + /** * Retrieve is allow and show block * diff --git a/app/code/core/Mage/Checkout/Block/Onepage/Progress.php b/app/code/core/Mage/Checkout/Block/Onepage/Progress.php index ebc9845adb..653359f3e7 100644 --- a/app/code/core/Mage/Checkout/Block/Onepage/Progress.php +++ b/app/code/core/Mage/Checkout/Block/Onepage/Progress.php @@ -69,6 +69,30 @@ public function getPaymentHtml() return $this->getChildHtml('payment_info'); } + /** + * Get is step completed. if is set 'toStep' then all steps after him is not completed. + * + * @param string $currentStep + * @see: Mage_Checkout_Block_Onepage_Abstract::_getStepCodes() for allowed values + * @return bool + */ + public function isStepComplete($currentStep) + { + $stepsRevertIndex = array_flip($this->_getStepCodes()); + + $toStep = $this->getRequest()->getParam('toStep'); + + if (empty($toStep) || !isset($stepsRevertIndex[$currentStep])) { + return $this->getCheckout()->getStepData($currentStep, 'complete'); + } + + if ($stepsRevertIndex[$currentStep] > $stepsRevertIndex[$toStep]) { + return false; + } + + return $this->getCheckout()->getStepData($currentStep, 'complete'); + } + /** * Get quote shipping price including tax * @return float diff --git a/app/code/core/Mage/Checkout/Helper/Data.php b/app/code/core/Mage/Checkout/Helper/Data.php index 6e18cbdfce..f4bf8344ff 100644 --- a/app/code/core/Mage/Checkout/Helper/Data.php +++ b/app/code/core/Mage/Checkout/Helper/Data.php @@ -31,7 +31,8 @@ */ class Mage_Checkout_Helper_Data extends Mage_Core_Helper_Abstract { - const XML_PATH_GUEST_CHECKOUT = 'checkout/options/guest_checkout'; + const XML_PATH_GUEST_CHECKOUT = 'checkout/options/guest_checkout'; + const XML_PATH_CUSTOMER_MUST_BE_LOGGED = 'checkout/options/customer_must_be_logged'; protected $_agreements = null; @@ -294,4 +295,14 @@ public function isContextCheckout() { return (Mage::app()->getRequest()->getParam('context') == 'checkout'); } + + /** + * Check if user must be logged during checkout process + * + * @return boolean + */ + public function isCustomerMustBeLogged() + { + return Mage::getStoreConfigFlag(self::XML_PATH_CUSTOMER_MUST_BE_LOGGED); + } } diff --git a/app/code/core/Mage/Checkout/Helper/Url.php b/app/code/core/Mage/Checkout/Helper/Url.php index 06dc5e8d22..a85d45fc1e 100644 --- a/app/code/core/Mage/Checkout/Helper/Url.php +++ b/app/code/core/Mage/Checkout/Helper/Url.php @@ -92,4 +92,14 @@ public function getOPCheckoutUrl() { return $this->_getUrl('checkout/onepage'); } + + /** + * Url to Registration Page + * + * @return string + */ + public function getRegistrationUrl() + { + return $this->_getUrl('customer/account/create'); + } } diff --git a/app/code/core/Mage/Checkout/Model/Cart.php b/app/code/core/Mage/Checkout/Model/Cart.php index eef86350c9..03730e3a62 100644 --- a/app/code/core/Mage/Checkout/Model/Cart.php +++ b/app/code/core/Mage/Checkout/Model/Cart.php @@ -436,11 +436,15 @@ public function save() return $this; } + /** + * Mark all quote items as deleted (empty shopping cart) + * + * @return Mage_Checkout_Model_Cart + */ public function truncate() { - foreach ($this->getQuote()->getItemsCollection() as $item) { - $item->isDeleted(true); - } + $this->getQuote()->removeAllItems(); + return $this; } public function getProductIds() diff --git a/app/code/core/Mage/Checkout/Model/Type/Multishipping.php b/app/code/core/Mage/Checkout/Model/Type/Multishipping.php index 93edf95e48..49b70b0a9c 100644 --- a/app/code/core/Mage/Checkout/Model/Type/Multishipping.php +++ b/app/code/core/Mage/Checkout/Model/Type/Multishipping.php @@ -245,7 +245,7 @@ public function setShippingItemsInformation($info) !$_item->getParentItem() && !$_item->getMultishippingQty() ) { - $_item->delete(); + $quote->removeItem($_item->getId()); } } @@ -268,7 +268,7 @@ public function setShippingItemsInformation($info) $quote->getBillingAddress()->addItem($_item); } else { $_item->setQty(0); - $_item->delete(); + $quote->removeItem($_item->getId()); } } @@ -296,13 +296,9 @@ protected function _addShippingItem($quoteItemId, $data) if ($addressId && $quoteItem) { /** - * Decrease quote item QTY if address item has QTY 0 and skip this item processing + * Skip item processing if qty 0 */ if ($qty === 0) { - $quoteItemQty = $quoteItem->getQty(); - if ($quoteItemQty > 0) { - $quoteItem->setQty($quoteItemQty-1); - } return $this; } $quoteItem->setMultishippingQty((int)$quoteItem->getMultishippingQty()+$qty); @@ -464,33 +460,32 @@ protected function _prepareOrder(Mage_Sales_Model_Quote_Address $address) */ protected function _validate() { - $helper = Mage::helper('checkout'); $quote = $this->getQuote(); if (!$quote->getIsMultiShipping()) { - Mage::throwException($helper->__('Invalid checkout type.')); + Mage::throwException(Mage::helper('checkout')->__('Invalid checkout type.')); } /** @var $paymentMethod Mage_Payment_Model_Method_Abstract */ $paymentMethod = $quote->getPayment()->getMethodInstance(); if (!empty($paymentMethod) && !$paymentMethod->isAvailable($quote)) { - Mage::throwException($helper->__('Please specify payment method.')); + Mage::throwException(Mage::helper('checkout')->__('Please specify payment method.')); } $addresses = $quote->getAllShippingAddresses(); foreach ($addresses as $address) { $addressValidation = $address->validate(); if ($addressValidation !== true) { - Mage::throwException($helper->__('Please check shipping addresses information.')); + Mage::throwException(Mage::helper('checkout')->__('Please check shipping addresses information.')); } $method= $address->getShippingMethod(); $rate = $address->getShippingRateByCode($method); if (!$method || !$rate) { - Mage::throwException($helper->__('Please specify shipping methods for all addresses.')); + Mage::throwException(Mage::helper('checkout')->__('Please specify shipping methods for all addresses.')); } } $addressValidation = $quote->getBillingAddress()->validate(); if ($addressValidation !== true) { - Mage::throwException($helper->__('Please check billing address information.')); + Mage::throwException(Mage::helper('checkout')->__('Please check billing address information.')); } return $this; } diff --git a/app/code/core/Mage/Checkout/Model/Type/Onepage.php b/app/code/core/Mage/Checkout/Model/Type/Onepage.php index 7ad0cca693..50be9c13b1 100644 --- a/app/code/core/Mage/Checkout/Model/Type/Onepage.php +++ b/app/code/core/Mage/Checkout/Model/Type/Onepage.php @@ -70,7 +70,7 @@ class Mage_Checkout_Model_Type_Onepage public function __construct() { $this->_helper = Mage::helper('checkout'); - $this->_customerEmailExistsMessage = $this->_helper->__('There is already a customer registered using this email address. Please login using this email address or enter a different email address to register your account.'); + $this->_customerEmailExistsMessage = Mage::helper('checkout')->__('There is already a customer registered using this email address. Please login using this email address or enter a different email address to register your account.'); $this->_checkoutSession = Mage::getSingleton('checkout/session'); $this->_customerSession = Mage::getSingleton('customer/session'); } @@ -196,7 +196,7 @@ public function getCheckoutMehod() public function saveCheckoutMethod($method) { if (empty($method)) { - return array('error' => -1, 'message' => $this->_helper->__('Invalid data.')); + return array('error' => -1, 'message' => Mage::helper('checkout')->__('Invalid data.')); } $this->getQuote()->setCheckoutMethod($method)->save(); @@ -231,7 +231,7 @@ public function getAddress($addressId) public function saveBilling($data, $customerAddressId) { if (empty($data)) { - return array('error' => -1, 'message' => $this->_helper->__('Invalid data.')); + return array('error' => -1, 'message' => Mage::helper('checkout')->__('Invalid data.')); } $address = $this->getQuote()->getBillingAddress(); @@ -246,7 +246,7 @@ public function saveBilling($data, $customerAddressId) if ($customerAddress->getId()) { if ($customerAddress->getCustomerId() != $this->getQuote()->getCustomerId()) { return array('error' => 1, - 'message' => $this->_helper->__('Customer Address is not valid.') + 'message' => Mage::helper('checkout')->__('Customer Address is not valid.') ); } @@ -272,7 +272,7 @@ public function saveBilling($data, $customerAddressId) $address->setData($attribute->getAttributeCode(), NULL); } } - + $address->setCustomerAddressId(null); // Additional form data, not fetched by extractData (as it fetches only attributes) $address->setSaveInAddressBook(empty($data['save_in_address_book']) ? 0 : 1); } @@ -479,7 +479,7 @@ protected function _processValidateCustomer(Mage_Sales_Model_Quote_Address $addr if (!Zend_Validate::is($email, 'EmailAddress')) { return array( 'error' => -1, - 'message' => $this->_helper->__('Invalid email address "%s"', $email) + 'message' => Mage::helper('checkout')->__('Invalid email address "%s"', $email) ); } } @@ -497,7 +497,7 @@ protected function _processValidateCustomer(Mage_Sales_Model_Quote_Address $addr public function saveShipping($data, $customerAddressId) { if (empty($data)) { - return array('error' => -1, 'message' => $this->_helper->__('Invalid data.')); + return array('error' => -1, 'message' => Mage::helper('checkout')->__('Invalid data.')); } $address = $this->getQuote()->getShippingAddress(); @@ -512,7 +512,7 @@ public function saveShipping($data, $customerAddressId) if ($customerAddress->getId()) { if ($customerAddress->getCustomerId() != $this->getQuote()->getCustomerId()) { return array('error' => 1, - 'message' => $this->_helper->__('Customer Address is not valid.') + 'message' => Mage::helper('checkout')->__('Customer Address is not valid.') ); } @@ -569,11 +569,11 @@ public function saveShipping($data, $customerAddressId) public function saveShippingMethod($shippingMethod) { if (empty($shippingMethod)) { - return array('error' => -1, 'message' => $this->_helper->__('Invalid shipping method.')); + return array('error' => -1, 'message' => Mage::helper('checkout')->__('Invalid shipping method.')); } $rate = $this->getQuote()->getShippingAddress()->getShippingRateByCode($shippingMethod); if (!$rate) { - return array('error' => -1, 'message' => $this->_helper->__('Invalid shipping method.')); + return array('error' => -1, 'message' => Mage::helper('checkout')->__('Invalid shipping method.')); } $this->getQuote()->getShippingAddress() ->setShippingMethod($shippingMethod); @@ -594,7 +594,7 @@ public function saveShippingMethod($shippingMethod) public function savePayment($data) { if (empty($data)) { - return array('error' => -1, 'message' => $this->_helper->__('Invalid data.')); + return array('error' => -1, 'message' => Mage::helper('checkout')->__('Invalid data.')); } $quote = $this->getQuote(); if ($quote->isVirtual()) { @@ -625,14 +625,13 @@ public function savePayment($data) */ public function validate() { - $helper = Mage::helper('checkout'); $quote = $this->getQuote(); if ($quote->getIsMultiShipping()) { - Mage::throwException($helper->__('Invalid checkout type.')); + Mage::throwException(Mage::helper('checkout')->__('Invalid checkout type.')); } if ($quote->getCheckoutMethod() == self::METHOD_GUEST && !$quote->isAllowedGuestCheckout()) { - Mage::throwException($this->_helper->__('Sorry, guest checkout is not enabled. Please try again or contact store owner.')); + Mage::throwException(Mage::helper('checkout')->__('Sorry, guest checkout is not enabled. Please try again or contact store owner.')); } } @@ -838,31 +837,30 @@ public function saveOrder() */ protected function validateOrder() { - $helper = Mage::helper('checkout'); if ($this->getQuote()->getIsMultiShipping()) { - Mage::throwException($helper->__('Invalid checkout type.')); + Mage::throwException(Mage::helper('checkout')->__('Invalid checkout type.')); } if (!$this->getQuote()->isVirtual()) { $address = $this->getQuote()->getShippingAddress(); $addressValidation = $address->validate(); if ($addressValidation !== true) { - Mage::throwException($helper->__('Please check shipping address information.')); + Mage::throwException(Mage::helper('checkout')->__('Please check shipping address information.')); } $method= $address->getShippingMethod(); $rate = $address->getShippingRateByCode($method); if (!$this->getQuote()->isVirtual() && (!$method || !$rate)) { - Mage::throwException($helper->__('Please specify shipping method.')); + Mage::throwException(Mage::helper('checkout')->__('Please specify shipping method.')); } } $addressValidation = $this->getQuote()->getBillingAddress()->validate(); if ($addressValidation !== true) { - Mage::throwException($helper->__('Please check billing address information.')); + Mage::throwException(Mage::helper('checkout')->__('Please check billing address information.')); } if (!($this->getQuote()->getPayment()->getMethod())) { - Mage::throwException($helper->__('Please select valid payment method.')); + Mage::throwException(Mage::helper('checkout')->__('Please select valid payment method.')); } } @@ -922,7 +920,7 @@ public function getLastOrderId() // * Related to issue with some browsers when checkout method was not saved during first step. // */ // if (!$this->getQuote()->getCheckoutMethod()) { -// if ($this->_helper->isAllowedGuestCheckout($this->getQuote(), $this->getQuote()->getStore())) { +// if (Mage::helper('checkout')->isAllowedGuestCheckout($this->getQuote(), $this->getQuote()->getStore())) { // $this->getQuote()->setCheckoutMethod(Mage_Sales_Model_Quote::CHECKOUT_METHOD_GUEST); // } else { // $this->getQuote()->setCheckoutMethod(Mage_Sales_Model_Quote::CHECKOUT_METHOD_REGISTER); @@ -932,7 +930,7 @@ public function getLastOrderId() // switch ($this->getQuote()->getCheckoutMethod()) { // case Mage_Sales_Model_Quote::CHECKOUT_METHOD_GUEST: // if (!$this->getQuote()->isAllowedGuestCheckout()) { -// Mage::throwException($this->_helper->__('Sorry, guest checkout is not enabled. Please try again or contact the store owner.')); +// Mage::throwException(Mage::helper('checkout')->__('Sorry, guest checkout is not enabled. Please try again or contact the store owner.')); // } // $this->getQuote()->setCustomerId(null) // ->setCustomerEmail($billing->getEmail()) diff --git a/app/code/core/Mage/Checkout/controllers/CartController.php b/app/code/core/Mage/Checkout/controllers/CartController.php index 35903de569..f925eb3e7d 100644 --- a/app/code/core/Mage/Checkout/controllers/CartController.php +++ b/app/code/core/Mage/Checkout/controllers/CartController.php @@ -368,9 +368,30 @@ public function updateItemOptionsAction() } /** - * Update shoping cart data action + * Update shopping cart data action */ public function updatePostAction() + { + $updateAction = (string)$this->getRequest()->getParam('update_cart_action'); + + switch ($updateAction) { + case 'empty_cart': + $this->_emptyShoppingCart(); + break; + case 'update_qty': + $this->_updateShoppingCart(); + break; + default: + $this->_updateShoppingCart(); + } + + $this->_goBack(); + } + + /** + * Update customer's shopping cart + */ + protected function _updateShoppingCart() { try { $cartData = $this->getRequest()->getParam('cart'); @@ -399,7 +420,21 @@ public function updatePostAction() $this->_getSession()->addException($e, $this->__('Cannot update shopping cart.')); Mage::logException($e); } - $this->_goBack(); + } + + /** + * Empty customer's shopping cart + */ + protected function _emptyShoppingCart() + { + try { + $this->_getCart()->truncate()->save(); + $this->_getSession()->setCartWasUpdated(true); + } catch (Mage_Core_Exception $exception) { + $this->_getSession()->addError($exception->getMessage()); + } catch (Exception $exception) { + $this->_getSession()->addException($exception, $this->__('Cannot update shopping cart.')); + } } /** @@ -481,7 +516,7 @@ public function couponPostAction() ->collectTotals() ->save(); - if ($couponCode) { + if (strlen($couponCode)) { if ($couponCode == $this->_getQuote()->getCouponCode()) { $this->_getSession()->addSuccess( $this->__('Coupon code "%s" was applied.', Mage::helper('core')->htmlEscape($couponCode)) diff --git a/app/code/core/Mage/Checkout/controllers/OnepageController.php b/app/code/core/Mage/Checkout/controllers/OnepageController.php index 536530122f..88ca7e9e50 100644 --- a/app/code/core/Mage/Checkout/controllers/OnepageController.php +++ b/app/code/core/Mage/Checkout/controllers/OnepageController.php @@ -47,6 +47,12 @@ public function preDispatch() $checkoutSessionQuote->removeAllAddresses(); } + if(!$this->_canShowForUnregisteredUsers()){ + $this->norouteAction(); + $this->setFlag('',self::FLAG_NO_DISPATCH,true); + return; + } + return $this; } @@ -580,4 +586,17 @@ protected function _filterPostData($data) $data = $this->_filterDates($data, array('dob')); return $data; } + + /** + * Check can page show for unregistered users + * + * @return boolean + */ + protected function _canShowForUnregisteredUsers() + { + return Mage::getSingleton('customer/session')->isLoggedIn() + || $this->getRequest()->getActionName() == 'index' + || Mage::helper('checkout')->isAllowedGuestCheckout($this->getOnepage()->getQuote()) + || !Mage::helper('checkout')->isCustomerMustBeLogged(); + } } diff --git a/app/code/core/Mage/Checkout/etc/config.xml b/app/code/core/Mage/Checkout/etc/config.xml index 6f1a381831..bc13fa2e1e 100644 --- a/app/code/core/Mage/Checkout/etc/config.xml +++ b/app/code/core/Mage/Checkout/etc/config.xml @@ -170,6 +170,7 @@ + /checkout/cart /checkout/onepage /checkout/multishipping diff --git a/app/code/core/Mage/Checkout/etc/system.xml b/app/code/core/Mage/Checkout/etc/system.xml index 499ddd004a..801c2ec288 100644 --- a/app/code/core/Mage/Checkout/etc/system.xml +++ b/app/code/core/Mage/Checkout/etc/system.xml @@ -62,6 +62,16 @@ 1 1 + + + select + adminhtml/system_config_source_yesno + 0 + 15 + 1 + 1 + 0 + select diff --git a/app/code/core/Mage/Cms/Helper/Page.php b/app/code/core/Mage/Cms/Helper/Page.php index af9d4ecb5d..86f95dfd1f 100644 --- a/app/code/core/Mage/Cms/Helper/Page.php +++ b/app/code/core/Mage/Cms/Helper/Page.php @@ -80,7 +80,8 @@ protected function _renderPage(Mage_Core_Controller_Varien_Action $action, $pag return false; } - $inRange = Mage::app()->getLocale()->isStoreDateInInterval(null, $page->getCustomThemeFrom(), $page->getCustomThemeTo()); + $inRange = Mage::app()->getLocale() + ->isStoreDateInInterval(null, $page->getCustomThemeFrom(), $page->getCustomThemeTo()); if ($page->getCustomTheme()) { if ($inRange) { @@ -105,15 +106,16 @@ protected function _renderPage(Mage_Core_Controller_Varien_Action $action, $pag Mage::dispatchEvent('cms_page_render', array('page' => $page, 'controller_action' => $action)); - $action->loadLayoutUpdates(); - $layoutUpdate = ($page->getCustomLayoutUpdateXml() && $inRange) ? $page->getCustomLayoutUpdateXml() : $page->getLayoutUpdateXml(); + $layoutUpdate = ($page->getCustomLayoutUpdateXml() && $inRange) + ? $page->getCustomLayoutUpdateXml() : $page->getLayoutUpdateXml(); $action->getLayout()->getUpdate()->addUpdate($layoutUpdate); $action->generateLayoutXml()->generateLayoutBlocks(); $contentHeadingBlock = $action->getLayout()->getBlock('page_content_heading'); if ($contentHeadingBlock) { - $contentHeadingBlock->setContentHeading($page->getContentHeading()); + $contentHeading = $this->escapeHtml($page->getContentHeading()); + $contentHeadingBlock->setContentHeading($contentHeading); } if ($page->getRootTemplate()) { diff --git a/app/code/core/Mage/Cms/sql/cms_setup/install-1.6.0.0.php b/app/code/core/Mage/Cms/sql/cms_setup/install-1.6.0.0.php index 3f80bfc8d9..b9829ca711 100644 --- a/app/code/core/Mage/Cms/sql/cms_setup/install-1.6.0.0.php +++ b/app/code/core/Mage/Cms/sql/cms_setup/install-1.6.0.0.php @@ -107,8 +107,8 @@ 'nullable' => true, ), 'Page Meta Description') ->addColumn('identifier', Varien_Db_Ddl_Table::TYPE_TEXT, 100, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Page String Identifier') ->addColumn('content_heading', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array( 'nullable' => true, diff --git a/app/code/core/Mage/Connect/Block/Adminhtml/Extension/Custom/Edit/Tab/Abstract.php b/app/code/core/Mage/Connect/Block/Adminhtml/Extension/Custom/Edit/Tab/Abstract.php index 02c5a84942..a87c0f4cdc 100644 --- a/app/code/core/Mage/Connect/Block/Adminhtml/Extension/Custom/Edit/Tab/Abstract.php +++ b/app/code/core/Mage/Connect/Block/Adminhtml/Extension/Custom/Edit/Tab/Abstract.php @@ -148,7 +148,7 @@ public function getAddFileDepsRowButtonHtml($selector='span', $filesClass='files */ public function getTabLabel() { - return Mage::helper('connect')->__(''); + return ''; } /** @@ -158,7 +158,7 @@ public function getTabLabel() */ public function getTabTitle() { - return Mage::helper('connect')->__(''); + return ''; } public function canShowTab() diff --git a/app/code/core/Mage/Connect/Model/Extension.php b/app/code/core/Mage/Connect/Model/Extension.php index 6ffb621e54..f5a8750771 100644 --- a/app/code/core/Mage/Connect/Model/Extension.php +++ b/app/code/core/Mage/Connect/Model/Extension.php @@ -24,7 +24,6 @@ * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) */ -require_once 'Mage/Connect/Package.php'; // TODO /** * Extension model @@ -158,8 +157,9 @@ protected function packageFilesToArray($filesString) */ protected function _setDependencies() { - $this->getPackage()->clearDependencies(); - $this->getPackage()->setDependencyPhpVersion($this->getData('depends_php_min'), $this->getData('depends_php_max')); + $this->getPackage() + ->clearDependencies() + ->setDependencyPhpVersion($this->getData('depends_php_min'), $this->getData('depends_php_max')); foreach ($this->getData('depends') as $deptype=>$deps) { foreach ($deps['name'] as $i=>$type) { @@ -174,7 +174,9 @@ protected function _setDependencies() $packageFiles = $this->packageFilesToArray($files); if ($deptype !== 'extension') { - $channel = !empty($deps['channel'][$i]) ? $deps['channel'][$i] : 'connect.magentocommerce.com/core'; + $channel = !empty($deps['channel'][$i]) + ? $deps['channel'][$i] + : 'connect.magentocommerce.com/core'; } switch ($deptype) { case 'package': diff --git a/app/code/core/Mage/Connect/controllers/Adminhtml/Extension/CustomController.php b/app/code/core/Mage/Connect/controllers/Adminhtml/Extension/CustomController.php index 3c9237cd21..897dc6b01b 100644 --- a/app/code/core/Mage/Connect/controllers/Adminhtml/Extension/CustomController.php +++ b/app/code/core/Mage/Connect/controllers/Adminhtml/Extension/CustomController.php @@ -43,7 +43,6 @@ public function indexAction() ->_title($this->__('Magento Connect')) ->_title($this->__('Package Extensions')); - Mage::app()->getStore()->setStoreId(1); $this->_forward('edit'); } @@ -131,7 +130,6 @@ public function saveAction() if (empty($create)) { $this->_redirect('*/*/edit'); } else { - Mage::app()->getStore()->setStoreId(1); $this->_forward('create'); } } catch (Mage_Core_Exception $e){ diff --git a/app/code/core/Mage/Core/Block/Html/Select.php b/app/code/core/Mage/Core/Block/Html/Select.php index 61248dc419..5e57f5a337 100644 --- a/app/code/core/Mage/Core/Block/Html/Select.php +++ b/app/code/core/Mage/Core/Block/Html/Select.php @@ -30,71 +30,128 @@ * * @category Mage * @package Mage_Core - * @author Magento Core Team + * @author Magento Core Team */ class Mage_Core_Block_Html_Select extends Mage_Core_Block_Abstract { protected $_options = array(); + /** + * Get options of the element + * + * @return array + */ public function getOptions() { return $this->_options; } + /** + * Set options for the HTML select + * + * @param array $options + * @return Mage_Core_Block_Html_Select + */ public function setOptions($options) { $this->_options = $options; return $this; } + /** + * Add an option to HTML select + * + * @param string $value HTML value + * @param string $label HTML label + * @param array $params HTML attributes + * @return Mage_Core_Block_Html_Select + */ public function addOption($value, $label, $params=array()) { $this->_options[] = array('value' => $value, 'label' => $label, 'params' => $params); return $this; } + /** + * Set element's HTML ID + * + * @param string $id ID + * @return Mage_Core_Block_Html_Select + */ public function setId($id) { $this->setData('id', $id); return $this; } + /** + * Set element's CSS class + * + * @param string $class Class + * @return Mage_Core_Block_Html_Select + */ public function setClass($class) { $this->setData('class', $class); return $this; } + /** + * Set element's HTML title + * + * @param string $title Title + * @return Mage_Core_Block_Html_Select + */ public function setTitle($title) { $this->setData('title', $title); return $this; } + /** + * HTML ID of the element + * + * @return string + */ public function getId() { return $this->getData('id'); } + /** + * CSS class of the element + * + * @return string + */ public function getClass() { return $this->getData('class'); } + /** + * Returns HTML title of the element + * + * @return string + */ public function getTitle() { return $this->getData('title'); } + /** + * Render HTML + * + * @return string + */ protected function _toHtml() { if (!$this->_beforeToHtml()) { return ''; } - $html = 'getExtraParams() . '>'; $values = $this->getValue(); if (!is_array($values)){ @@ -109,17 +166,17 @@ protected function _toHtml() foreach ($this->getOptions() as $key => $option) { if ($isArrayOption && is_array($option)) { $value = $option['value']; - $label = $option['label']; + $label = (string)$option['label']; $params = (!empty($option['params'])) ? $option['params'] : array(); } else { - $value = $key; - $label = $option; + $value = (string)$key; + $label = (string)$option; $isArrayOption = false; $params = array(); } if (is_array($value)) { - $html.= ''; + $html .= ''; foreach ($value as $keyGroup => $optionGroup) { if (!is_array($optionGroup)) { $optionGroup = array( @@ -127,23 +184,24 @@ protected function _toHtml() 'label' => $optionGroup ); } - $html.= $this->_optionToHtml( + $html .= $this->_optionToHtml( $optionGroup, in_array($optionGroup['value'], $values) ); } - $html.= ''; + $html .= ''; } else { - $html.= $this->_optionToHtml(array( - 'value' => $value, - 'label' => $label, - 'params' => $params - ), + $html .= $this->_optionToHtml( + array( + 'value' => $value, + 'label' => $label, + 'params' => $params + ), in_array($value, $values) ); } } - $html.= ''; + $html .= ''; return $html; } @@ -175,17 +233,28 @@ protected function _optionToHtml($option, $selected = false) } return sprintf('', - $this->htmlEscape($option['value']), + $this->escapeHtml($option['value']), $selectedHtml, $params, - $this->htmlEscape($option['label'])); + $this->escapeHtml($option['label'])); } + /** + * Alias for toHtml() + * + * @return string + */ public function getHtml() { return $this->toHtml(); } + /** + * Calculate CRC32 hash for option value + * + * @param string $optionValue Value of the option + * @return string + */ public function calcOptionHash($optionValue) { return sprintf('%u', crc32($this->getName() . $this->getId() . $optionValue)); diff --git a/app/code/core/Mage/Core/Controller/Varien/Action.php b/app/code/core/Mage/Core/Controller/Varien/Action.php index 805864ec54..ff21e8be0b 100644 --- a/app/code/core/Mage/Core/Controller/Varien/Action.php +++ b/app/code/core/Mage/Core/Controller/Varien/Action.php @@ -452,14 +452,21 @@ public function dispatch($action) } } + /** + * Retrieve action method name + * + * @param string $action + * @return string + */ public function getActionMethodName($action) { - $method = $action.'Action'; - return $method; + return $action . 'Action'; } /** - * Dispatches event before action + * Dispatch event before action + * + * @return null */ public function preDispatch() { @@ -493,7 +500,8 @@ public function preDispatch() Mage::app()->loadArea($this->getLayout()->getArea()); if ($this->getFlag('', self::FLAG_NO_COOKIES_REDIRECT) - && Mage::getStoreConfig('web/browser_capabilities/cookies')) { + && Mage::getStoreConfig('web/browser_capabilities/cookies') + ) { $this->_forward('noCookies', 'index', 'core'); return; } @@ -502,16 +510,13 @@ public function preDispatch() return; } - Mage::dispatchEvent('controller_action_predispatch', array('controller_action'=>$this)); - Mage::dispatchEvent( - 'controller_action_predispatch_'.$this->getRequest()->getRouteName(), - array('controller_action'=>$this) - ); Varien_Autoload::registerScope($this->getRequest()->getRouteName()); - Mage::dispatchEvent( - 'controller_action_predispatch_'.$this->getFullActionName(), - array('controller_action'=>$this) - ); + + Mage::dispatchEvent('controller_action_predispatch', array('controller_action' => $this)); + Mage::dispatchEvent('controller_action_predispatch_' . $this->getRequest()->getRouteName(), + array('controller_action' => $this)); + Mage::dispatchEvent('controller_action_predispatch_' . $this->getFullActionName(), + array('controller_action' => $this)); } /** @@ -745,6 +750,8 @@ protected function _getRefererUrl() $refererUrl = Mage::helper('core')->urlDecode($url); } + $refererUrl = Mage::helper('core')->escapeUrl($refererUrl); + if (!$this->_isUrlInternal($refererUrl)) { $refererUrl = Mage::app()->getStore()->getBaseUrl(); } diff --git a/app/code/core/Mage/Core/Helper/Data.php b/app/code/core/Mage/Core/Helper/Data.php index 6b602365df..2e907117cc 100644 --- a/app/code/core/Mage/Core/Helper/Data.php +++ b/app/code/core/Mage/Core/Helper/Data.php @@ -39,11 +39,35 @@ class Mage_Core_Helper_Data extends Mage_Core_Helper_Abstract const XML_PATH_CACHE_BETA_TYPES = 'global/cache/betatypes'; const XML_PATH_CONNECTION_TYPE = 'global/resources/default_setup/connection/type'; + const CHARS_LOWERS = 'abcdefghijklmnopqrstuvwxyz'; + const CHARS_UPPERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + const CHARS_DIGITS = '0123456789'; + const CHARS_SPECIALS = '!$*+-.=?@^_|~'; + const CHARS_PASSWORD_LOWERS = 'abcdefghjkmnpqrstuvwxyz'; + const CHARS_PASSWORD_UPPERS = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; + const CHARS_PASSWORD_DIGITS = '23456789'; + const CHARS_PASSWORD_SPECIALS = '!$*-.=?@_'; + + /** + * Config pathes to merchant country code and merchant VAT number + */ + const XML_PATH_MERCHANT_COUNTRY_CODE = 'general/store_information/merchant_country'; + const XML_PATH_MERCHANT_VAT_NUMBER = 'general/store_information/merchant_vat_number'; + const XML_PATH_EU_COUNTRIES_LIST = 'general/country/eu_countries'; + /** * @var Mage_Core_Model_Encryption */ protected $_encryptor = null; + protected $_allowedFormats = array( + Mage_Core_Model_Locale::FORMAT_TYPE_FULL, + Mage_Core_Model_Locale::FORMAT_TYPE_LONG, + Mage_Core_Model_Locale::FORMAT_TYPE_MEDIUM, + Mage_Core_Model_Locale::FORMAT_TYPE_SHORT + ); + + /** * @return Mage_Core_Model_Encryption */ @@ -125,19 +149,16 @@ public function formatPrice($price, $includeContainer = true) } /** - * Format date using current locale options + * Format date using current locale options and time zone. * - * @param date|Zend_Date|null $date in GMT timezone - * @param string $format - * @param bool $showTime + * @param date|Zend_Date|null $date + * @param string $format See Mage_Core_Model_Locale::FORMAT_TYPE_* constants + * @param bool $showTime Whether to include time * @return string */ - public function formatDate($date=null, $format='short', $showTime=false) + public function formatDate($date = null, $format = Mage_Core_Model_Locale::FORMAT_TYPE_SHORT, $showTime = false) { - if (Mage_Core_Model_Locale::FORMAT_TYPE_FULL !==$format && - Mage_Core_Model_Locale::FORMAT_TYPE_LONG !==$format && - Mage_Core_Model_Locale::FORMAT_TYPE_MEDIUM !==$format && - Mage_Core_Model_Locale::FORMAT_TYPE_SHORT !==$format) { + if (!in_array($format, $this->_allowedFormats, true)) { return $date; } if (!($date instanceof Zend_Date) && $date && !strtotime($date)) { @@ -145,15 +166,13 @@ public function formatDate($date=null, $format='short', $showTime=false) } if (is_null($date)) { $date = Mage::app()->getLocale()->date(Mage::getSingleton('core/date')->gmtTimestamp(), null, null); - } - elseif (!$date instanceof Zend_Date) { - $date = Mage::app()->getLocale()->date(strtotime($date), null, null, $showTime); + } else if (!$date instanceof Zend_Date) { + $date = Mage::app()->getLocale()->date(strtotime($date), null, null); } if ($showTime) { $format = Mage::app()->getLocale()->getDateTimeFormat($format); - } - else { + } else { $format = Mage::app()->getLocale()->getDateFormat($format); } @@ -164,33 +183,27 @@ public function formatDate($date=null, $format='short', $showTime=false) * Format time using current locale options * * @param date|Zend_Date|null $time - * @param string $format - * @param bool $showTime + * @param string $format + * @param bool $showDate * @return string */ - public function formatTime($time=null, $format='short', $showDate=false) + public function formatTime($time = null, $format = Mage_Core_Model_Locale::FORMAT_TYPE_SHORT, $showDate = false) { - if (Mage_Core_Model_Locale::FORMAT_TYPE_FULL !==$format && - Mage_Core_Model_Locale::FORMAT_TYPE_LONG !==$format && - Mage_Core_Model_Locale::FORMAT_TYPE_MEDIUM !==$format && - Mage_Core_Model_Locale::FORMAT_TYPE_SHORT !==$format) { + if (!in_array($format, $this->_allowedFormats, true)) { return $time; } if (is_null($time)) { $date = Mage::app()->getLocale()->date(time()); - } - elseif ($time instanceof Zend_Date) { + } else if ($time instanceof Zend_Date) { $date = $time; - } - else { + } else { $date = Mage::app()->getLocale()->date(strtotime($time)); } if ($showDate) { $format = Mage::app()->getLocale()->getDateTimeFormat($format); - } - else { + } else { $format = Mage::app()->getLocale()->getTimeFormat($format); } @@ -230,10 +243,10 @@ public function validateKey($key) return $this->getEncryptor()->validateKey($key); } - public function getRandomString($len, $chars=null) + public function getRandomString($len, $chars = null) { if (is_null($chars)) { - $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + $chars = self::CHARS_LOWERS . self::CHARS_UPPERS . self::CHARS_DIGITS; } mt_srand(10000000*(double)microtime()); for ($i = 0, $str = '', $lc = strlen($chars)-1; $i < $len; $i++) { @@ -805,4 +818,37 @@ public function useDbCompatibleMode() $value = (string) Mage::getConfig()->getNode($path); return (bool) $value; } + + /** + * Retrieve merchant country code + * + * @return string + */ + public function getMerchantCountryCode() + { + return (string) Mage::getStoreConfig(self::XML_PATH_MERCHANT_COUNTRY_CODE); + } + + /** + * Retrieve merchant VAT number + * + * @return string + */ + public function getMerchantVatNumber() + { + return (string) Mage::getStoreConfig(self::XML_PATH_MERCHANT_VAT_NUMBER); + } + + /** + * Check whether specified country is in EU countries list + * + * @param string $countryCode + * @param null|int $storeId + * @return bool + */ + public function isCountryInEU($countryCode, $storeId = null) + { + $euCountries = explode(',', Mage::getStoreConfig(self::XML_PATH_EU_COUNTRIES_LIST, $storeId)); + return in_array($countryCode, $euCountries); + } } diff --git a/app/code/core/Mage/Core/Helper/Translate.php b/app/code/core/Mage/Core/Helper/Translate.php index 9dfa1bd768..3ef098e98d 100644 --- a/app/code/core/Mage/Core/Helper/Translate.php +++ b/app/code/core/Mage/Core/Helper/Translate.php @@ -32,11 +32,11 @@ class Mage_Core_Helper_Translate extends Mage_Core_Helper_Abstract { /** - * Save transalation data to database for specific area - * - * @param array $translate + * Save translation data to database for specific area + * + * @param array $translate * @param string $area - * @param string $return_type + * @param string $returnType * @return string */ public function apply($translate, $area, $returnType = 'json') @@ -49,6 +49,30 @@ public function apply($translate, $area, $returnType = 'json') return $returnType == 'json' ? "{success:true}" : true; } catch (Exception $e) { return $returnType == 'json' ? "{error:true,message:'" . $e->getMessage() . "'}" : false; - } + } + } + + /** + * Sets escaping start marker which then processed by inline translation model + * + * @see Mage_Core_Model_Translate_Inline::_escapeInline() + * @param string $escapeChar Char to escape (default = ') + * @return string + */ + public function inlineEscapeStartMarker($escapeChar = "'") + { + $escapeChar = str_replace('"', '\"', $escapeChar); + return "{{escape={$escapeChar}}}"; + } + + /** + * Sets escaping end marker which then processed by inline translation model + * + * @see Mage_Core_Model_Translate_Inline::_escapeInline() + * @return string + */ + public function inlineEscapeEndMarker() + { + return '{{escape}}'; } } diff --git a/app/code/core/Mage/Core/Model/App.php b/app/code/core/Mage/Core/Model/App.php index 0c8556c68f..d80706d1b3 100644 --- a/app/code/core/Mage/Core/Model/App.php +++ b/app/code/core/Mage/Core/Model/App.php @@ -1213,6 +1213,18 @@ public function getRequest() return $this->_request; } + /** + * Request setter + * + * @param Mage_Core_Controller_Request_Http $request + * @return Mage_Core_Model_App + */ + public function setRequest(Mage_Core_Controller_Request_Http $request) + { + $this->_request = $request; + return $this; + } + /** * Retrieve response object * @@ -1228,6 +1240,18 @@ public function getResponse() return $this->_response; } + /** + * Response setter + * + * @param Mage_Core_Controller_Response_Http $response + * @return Mage_Core_Model_App + */ + public function setResponse(Mage_Core_Controller_Response_Http $response) + { + $this->_response = $response; + return $this; + } + public function addEventArea($area) { if (!isset($this->_events[$area])) { diff --git a/app/code/core/Mage/Core/Model/Calculator.php b/app/code/core/Mage/Core/Model/Calculator.php new file mode 100644 index 0000000000..119dfb8f0d --- /dev/null +++ b/app/code/core/Mage/Core/Model/Calculator.php @@ -0,0 +1,87 @@ + + */ +class Mage_Core_Model_Calculator +{ + /** + * Delta collected during rounding steps + * + * @var float + */ + protected $_delta = 0.0; + + /** + * Store instance + * + * @var Mage_Core_Model_Store|null + */ + protected $_store = null; + + /** + * Initialize calculator + * + * @param Mage_Core_Model_Store|int $store + */ + public function __construct($store) + { + if (!($store instanceof Mage_Core_Model_Store)) { + $store = Mage::app()->getStore($store); + } + $this->_store = $store; + } + + /** + * Round price considering delta + * + * @param float $price + * @param bool $negative Indicates if we perform addition (true) or subtraction (false) of rounded value + * @return float + */ + public function deltaRound($price, $negative = false) + { + $roundedPrice = $price; + if ($roundedPrice) { + if ($negative) { + $this->_delta = -$this->_delta; + } + $price += $this->_delta; + $roundedPrice = $this->_store->roundPrice($price); + $this->_delta = $price - $roundedPrice; + if ($negative) { + $this->_delta = -$this->_delta; + } + } + return $roundedPrice; + } +} diff --git a/app/code/core/Mage/Core/Model/Config.php b/app/code/core/Mage/Core/Model/Config.php index ee9fd31808..959250cdfe 100644 --- a/app/code/core/Mage/Core/Model/Config.php +++ b/app/code/core/Mage/Core/Model/Config.php @@ -593,8 +593,8 @@ protected function _getSectionConfig($path) if (!isset($this->_cacheSections[$section])) { return false; } - $sectioPath = array_slice($path, 0, $this->_cacheSections[$section]+1); - $sectionKey = implode('_', $sectioPath); + $sectionPath = array_slice($path, 0, $this->_cacheSections[$section]+1); + $sectionKey = implode('_', $sectionPath); if (!isset($this->_cacheLoadedSections[$sectionKey])) { Varien_Profiler::start('init_config_section:' . $sectionKey); @@ -618,7 +618,7 @@ public function getSectionNode($path) { $section = $path[0]; $config = $this->_getSectionConfig($path); - $path = array_slice($path, $this->_cacheSections[$section]+1); + $path = array_slice($path, $this->_cacheSections[$section] + 1); if ($config) { return $config->descend($path); } @@ -870,9 +870,7 @@ protected function _sortModuleDepends($modules) foreach ($moduleProp['depends'] as $dependModule => $true) { if (!isset($definedModules[$dependModule])) { Mage::throwException( - Mage::helper('core')->__( - 'Module "%1$s" cannot depend on "%2$s".', $moduleProp['module'], $dependModule - ) + Mage::helper('core')->__('Module "%1$s" cannot depend on "%2$s".', $moduleProp['module'], $dependModule) ); } } diff --git a/app/code/core/Mage/Core/Model/Cookie.php b/app/code/core/Mage/Core/Model/Cookie.php index e949ef9267..7577a3116b 100644 --- a/app/code/core/Mage/Core/Model/Cookie.php +++ b/app/code/core/Mage/Core/Model/Cookie.php @@ -198,6 +198,7 @@ public function isSecure() * @param string $path * @param string $domain * @param int|bool $secure + * @param bool $httponly * @return Mage_Core_Model_Cookie */ public function set($name, $value, $period = null, $path = null, $domain = null, $secure = null, $httponly = null) diff --git a/app/code/core/Mage/Core/Model/File/Validator/NotProtectedExtension.php b/app/code/core/Mage/Core/Model/File/Validator/NotProtectedExtension.php index fef7b2f252..5072451208 100644 --- a/app/code/core/Mage/Core/Model/File/Validator/NotProtectedExtension.php +++ b/app/code/core/Mage/Core/Model/File/Validator/NotProtectedExtension.php @@ -67,11 +67,8 @@ public function __construct() protected function _initMessageTemplates() { if (!$this->_messageTemplates) { - /** @var $helper Mage_Core_Helper_Data */ - $helper = Mage::helper('core'); $this->_messageTemplates = array( - self::PROTECTED_EXTENSION => - $helper->__('File with an extension "%value%" is protected and cannot be uploaded'), + self::PROTECTED_EXTENSION => Mage::helper('core')->__('File with an extension "%value%" is protected and cannot be uploaded'), ); } return $this; diff --git a/app/code/core/Mage/Core/Model/Locale.php b/app/code/core/Mage/Core/Model/Locale.php index eead5758a3..cb4d1cc09c 100644 --- a/app/code/core/Mage/Core/Model/Locale.php +++ b/app/code/core/Mage/Core/Model/Locale.php @@ -463,25 +463,28 @@ public function getTimeStrFormat($type) /** * Create Zend_Date object for current locale * - * @param mixed $date - * @param string $part - * @return Zend_Date - * @exception Zend_Date_Exception + * @param mixed $date + * @param string $part + * @param string|Zend_Locale $locale + * @param bool $useTimezone + * @return Zend_Date */ - public function date($date=null, $part=null, $locale=null, $useTimezone=true) + public function date($date = null, $part = null, $locale = null, $useTimezone = true) { if (is_null($locale)) { $locale = $this->getLocale(); } - // try-catch block was here + if (empty($date)) { + // $date may be false, but Zend_Date uses strict compare + $date = null; + } $date = new Zend_Date($date, $part, $locale); if ($useTimezone) { if ($timezone = Mage::app()->getStore()->getConfig(self::XML_PATH_DEFAULT_TIMEZONE)) { $date->setTimezone($timezone); } } - //$date->add(-(substr($date->get(Zend_Date::GMT_DIFF), 0,3)), Zend_Date::HOUR); return $date; } diff --git a/app/code/core/Mage/Core/Model/Resource/Abstract.php b/app/code/core/Mage/Core/Model/Resource/Abstract.php index 3ca2de8c29..21aacc295f 100644 --- a/app/code/core/Mage/Core/Model/Resource/Abstract.php +++ b/app/code/core/Mage/Core/Model/Resource/Abstract.php @@ -145,7 +145,7 @@ public function formatDate($date, $includeTime=true) */ public function mktime($str) { - return Varien_Date::formatDate($str); + return Varien_Date::toTimestamp($str); } /** diff --git a/app/code/core/Mage/Core/Model/Resource/Db/Collection/Abstract.php b/app/code/core/Mage/Core/Model/Resource/Db/Collection/Abstract.php index 8db14c1277..0660a708c9 100644 --- a/app/code/core/Mage/Core/Model/Resource/Db/Collection/Abstract.php +++ b/app/code/core/Mage/Core/Model/Resource/Db/Collection/Abstract.php @@ -244,7 +244,8 @@ protected function _initSelectFields() $column = $field; } - if (($alias !== null && in_array($alias, $columnsToSelect)) || // If field already joined from another table + if (($alias !== null && in_array($alias, $columnsToSelect)) || + // If field already joined from another table ($alias === null && isset($alias, $columnsToSelect))) { continue; } @@ -507,8 +508,8 @@ public function getAllIds() public function getData() { if ($this->_data === null) { - - + + $this->_renderFilters() ->_renderOrders() ->_renderLimit(); @@ -525,7 +526,7 @@ public function getData() /** * Prepare select for load - * + * * @return string */ protected function _prepareSelect(Varien_Db_Select $select) @@ -563,13 +564,13 @@ public function join($table, $cond, $cols = '*') $alias = $table; } - if (!isset($this->_joinedTables[$alias])) { + if (!isset($this->_joinedTables[$table])) { $this->getSelect()->join( array($alias => $this->getTable($table)), $cond, $cols ); - $this->_joinedTables[$table] = true; + $this->_joinedTables[$alias] = true; } return $this; } diff --git a/app/code/core/Mage/Core/Model/Resource/Design/Collection.php b/app/code/core/Mage/Core/Model/Resource/Design/Collection.php index e97cb40a7a..a243b61f50 100644 --- a/app/code/core/Mage/Core/Model/Resource/Design/Collection.php +++ b/app/code/core/Mage/Core/Model/Resource/Design/Collection.php @@ -51,7 +51,7 @@ protected function _construct() public function joinStore() { return $this->join( - array('cs' => $this->getTable('core/store')), + array('cs' => 'core/store'), 'cs.store_id = main_table.store_id', array('cs.name')); } diff --git a/app/code/core/Mage/Core/Model/Resource/Url/Rewrite.php b/app/code/core/Mage/Core/Model/Resource/Url/Rewrite.php index e6e22d8b3f..4fdef622aa 100644 --- a/app/code/core/Mage/Core/Model/Resource/Url/Rewrite.php +++ b/app/code/core/Mage/Core/Model/Resource/Url/Rewrite.php @@ -155,6 +155,9 @@ public function loadByRequestPath(Mage_Core_Model_Url_Rewrite $object, $path) $currentPenalty = null; $foundItem = null; foreach ($items as $item) { + if (!array_key_exists($item['request_path'], $mapPenalty)) { + continue; + } $penalty = $mapPenalty[$item['request_path']] << 1 + ($item['store_id'] ? 0 : 1); if (!$foundItem || $currentPenalty > $penalty) { $foundItem = $item; diff --git a/app/code/core/Mage/Core/Model/Resource/Variable.php b/app/code/core/Mage/Core/Model/Resource/Variable.php index 845d987e51..0826809cc7 100644 --- a/app/code/core/Mage/Core/Model/Resource/Variable.php +++ b/app/code/core/Mage/Core/Model/Resource/Variable.php @@ -136,10 +136,9 @@ protected function _getLoadSelect($field, $value, $object) */ protected function _addValueToSelect(Zend_Db_Select $select, $storeId = Mage_Core_Model_App::ADMIN_STORE_ID) { - $ifNullPlainValue = $this->_getReadAdapter() - ->getCheckSql('store.plain_value IS NULL', 'def.plain_value', 'store.plain_value'); - $ifNullHtmlValue = $this->_getReadAdapter() - ->getCheckSql('store.html_value IS NULL', 'def.html_value', 'store.html_value'); + $adapter = $this->_getReadAdapter(); + $ifNullPlainValue = $adapter->getCheckSql('store.plain_value IS NULL', 'def.plain_value', 'store.plain_value'); + $ifNullHtmlValue = $adapter->getCheckSql('store.html_value IS NULL', 'def.html_value', 'store.html_value'); $select->joinLeft( array('def' => $this->getTable('core/variable_value')), @@ -147,7 +146,7 @@ protected function _addValueToSelect(Zend_Db_Select $select, $storeId = Mage_Cor array()) ->joinLeft( array('store' => $this->getTable('core/variable_value')), - 'store.variable_id = def.variable_id AND store.store_id = ' . $storeId, + 'store.variable_id = def.variable_id AND store.store_id = ' . $adapter->quote($storeId), array()) ->columns(array( 'plain_value' => $ifNullPlainValue, diff --git a/app/code/core/Mage/Core/Model/Session/Abstract.php b/app/code/core/Mage/Core/Model/Session/Abstract.php index d705cb1aae..0f617b50b8 100644 --- a/app/code/core/Mage/Core/Model/Session/Abstract.php +++ b/app/code/core/Mage/Core/Model/Session/Abstract.php @@ -561,6 +561,17 @@ public function renewSession() $this->getCookie()->delete($this->getSessionName()); $this->regenerateSessionId(); + $sessionHosts = $this->getSessionHosts(); + $currentCookieDomain = $this->getCookie()->getDomain(); + if (is_array($sessionHosts)) { + foreach (array_keys($sessionHosts) as $host) { + // Delete cookies with the same name for parent domains + if (strpos($currentCookieDomain, $host) > 0) { + $this->getCookie()->delete($this->getSessionName(), null, $host); + } + } + } + return $this; } diff --git a/app/code/core/Mage/Core/Model/Source/Email/Variables.php b/app/code/core/Mage/Core/Model/Source/Email/Variables.php index de9b35cb59..65480ce5a5 100644 --- a/app/code/core/Mage/Core/Model/Source/Email/Variables.php +++ b/app/code/core/Mage/Core/Model/Source/Email/Variables.php @@ -47,59 +47,58 @@ class Mage_Core_Model_Source_Email_Variables */ public function __construct() { - $helper = Mage::helper('core'); $this->_configVariables = array( array( 'value' => Mage_Core_Model_Url::XML_PATH_UNSECURE_URL, - 'label' => $helper->__('Base Unsecure URL') + 'label' => Mage::helper('core')->__('Base Unsecure URL') ), array( 'value' => Mage_Core_Model_Url::XML_PATH_SECURE_URL, - 'label' => $helper->__('Base Secure URL') + 'label' => Mage::helper('core')->__('Base Secure URL') ), array( 'value' => 'trans_email/ident_general/name', - 'label' => $helper->__('General Contact Name') + 'label' => Mage::helper('core')->__('General Contact Name') ), array( 'value' => 'trans_email/ident_general/email', - 'label' => $helper->__('General Contact Email') + 'label' => Mage::helper('core')->__('General Contact Email') ), array( 'value' => 'trans_email/ident_sales/name', - 'label' => $helper->__('Sales Representative Contact Name') + 'label' => Mage::helper('core')->__('Sales Representative Contact Name') ), array( 'value' => 'trans_email/ident_sales/email', - 'label' => $helper->__('Sales Representative Contact Email') + 'label' => Mage::helper('core')->__('Sales Representative Contact Email') ), array( 'value' => 'trans_email/ident_custom1/name', - 'label' => $helper->__('Custom1 Contact Name') + 'label' => Mage::helper('core')->__('Custom1 Contact Name') ), array( 'value' => 'trans_email/ident_custom1/email', - 'label' => $helper->__('Custom1 Contact Email') + 'label' => Mage::helper('core')->__('Custom1 Contact Email') ), array( 'value' => 'trans_email/ident_custom2/name', - 'label' => $helper->__('Custom2 Contact Name') + 'label' => Mage::helper('core')->__('Custom2 Contact Name') ), array( 'value' => 'trans_email/ident_custom2/email', - 'label' => $helper->__('Custom2 Contact Email') + 'label' => Mage::helper('core')->__('Custom2 Contact Email') ), array( 'value' => 'general/store_information/name', - 'label' => $helper->__('Store Name') + 'label' => Mage::helper('core')->__('Store Name') ), array( 'value' => 'general/store_information/phone', - 'label' => $helper->__('Store Contact Telephone') + 'label' => Mage::helper('core')->__('Store Contact Telephone') ), array( 'value' => 'general/store_information/address', - 'label' => $helper->__('Store Contact Address') + 'label' => Mage::helper('core')->__('Store Contact Address') ) ); } diff --git a/app/code/core/Mage/Core/Model/Store.php b/app/code/core/Mage/Core/Model/Store.php index 4c1279123b..b939d56153 100644 --- a/app/code/core/Mage/Core/Model/Store.php +++ b/app/code/core/Mage/Core/Model/Store.php @@ -445,7 +445,7 @@ protected function _processConfigValue($fullPath, $path, $node) } elseif (strpos($sValue, '{{secure_base_url}}') !== false) { $secureBaseUrl = $this->getConfig(self::XML_PATH_SECURE_BASE_URL); $sValue = str_replace('{{secure_base_url}}', $secureBaseUrl, $sValue); - } elseif (strpos($sValue, '{{base_url}}') === false) { + } elseif (strpos($sValue, '{{base_url}}') !== false) { $sValue = Mage::getConfig()->substDistroServerVars($sValue); } } @@ -697,13 +697,7 @@ public function isCurrentlySecure() } if (Mage::isInstalled()) { - $secureBaseUrl = ''; - if (!$this->isAdmin()) { - $secureBaseUrl = Mage::getStoreConfig(Mage_Core_Model_Url::XML_PATH_SECURE_URL); - } else { - $secureBaseUrl = (string) Mage::getConfig() - ->getNode(Mage_Core_Model_Url::XML_PATH_SECURE_URL, 'default'); - } + $secureBaseUrl = Mage::getStoreConfig(Mage_Core_Model_Url::XML_PATH_SECURE_URL); if (!$secureBaseUrl) { return false; diff --git a/app/code/core/Mage/Core/Model/Translate/Inline.php b/app/code/core/Mage/Core/Model/Translate/Inline.php index b3869df656..6db6a05f52 100644 --- a/app/code/core/Mage/Core/Model/Translate/Inline.php +++ b/app/code/core/Mage/Core/Model/Translate/Inline.php @@ -202,6 +202,7 @@ public function stripInlineTranslations(&$body) } } else if (is_string($body)) { $body = preg_replace('#'.$this->_tokenRegex.'#', '$1', $body); + $body = preg_replace('/{{escape.*?}}/', '', $body); } return $this; } @@ -232,7 +233,7 @@ public function processResponseBody(&$body) $this->_specialTags(); $this->_otherText(); $this->_insertInlineScriptsHtml(); - + $this->_escapeInline(); $body = $this->_content; } @@ -247,7 +248,8 @@ protected function _insertInlineScriptsHtml() $baseJsUrl = Mage::getBaseUrl('js'); $url_prefix = Mage::app()->getStore()->isAdmin() ? 'adminhtml' : 'core'; - $ajaxUrl = Mage::getUrl($url_prefix.'/ajax/translate', array('_secure'=>Mage::app()->getStore()->isCurrentlySecure())); + $ajaxUrl = Mage::getUrl($url_prefix . '/ajax/translate', + array('_secure'=>Mage::app()->getStore()->isCurrentlySecure())); $trigImg = Mage::getDesign()->getSkinUrl('images/fam_book_open.png'); ob_start(); @@ -262,7 +264,8 @@ protected function _insertInlineScriptsHtml()
[TR]
_content, $matches)) { + // escape double quote character to make it possible to use it inside "" + $charToEscape = str_replace('"', '\\"', $matches[1]); + // preg_replace() used to avoid escaping already escaped quotes + $part = preg_replace("/[^\\\\]{$charToEscape}/", "\\{$charToEscape}", $matches[2]); + // Replace markers+string with the string itself + $this->_content = str_replace($matches[0], $part, $this->_content); + } + return $this; + } + /** * Prepare tags inline translates * @@ -292,27 +315,29 @@ protected function _tagAttributes() if ($this->getIsJson()) { $quotePatern = '\\\\"'; $quoteHtml = '\"'; + $tagEndRegexp = '(\\\\/)?' . '>$'; } else { $quotePatern = '"'; $quoteHtml = '"'; + $tagEndRegexp = '/?>$'; } $tagMatch = array(); $nextTag = 0; - $tagRegExp = '#<([a-z]+)\s*?[^>]+?(('.$this->_tokenRegex.')[^/>]*?)+(/?(>))#i'; + $tagRegExp = '#<([a-z]+)\s*?[^>]+?((' . $this->_tokenRegex . ')[^/>]*?)+(/?(>))#i'; while (preg_match($tagRegExp, $this->_content, $tagMatch, PREG_OFFSET_CAPTURE, $nextTag)) { $next = 0; $tagHtml = $tagMatch[0][0]; $trArr = array(); $m = array(); - $attrRegExp = '#'.$this->_tokenRegex.'#'; + $attrRegExp = '#' . $this->_tokenRegex . '#'; while (preg_match($attrRegExp, $tagHtml, $m, PREG_OFFSET_CAPTURE, $next)) { - $trArr[] = '{shown:\''.$this->_escape($m[1][0]).'\',' - . 'translated:\''.$this->_escape($m[2][0]).'\',' - . 'original:\''.$this->_escape($m[3][0]).'\',' + $trArr[] = '{shown:\''.$this->_escape($m[1][0]) . '\',' + . 'translated:\''.$this->_escape($m[2][0]) . '\',' + . 'original:\''.$this->_escape($m[3][0]) . '\',' . 'location:\'Tag attribute (ALT, TITLE, etc.)\',' - . 'scope:\''.$this->_escape($m[4][0]).'\'}'; + . 'scope:\''.$this->_escape($m[4][0]) . '\'}'; $tagHtml = substr_replace($tagHtml, $m[1][0], $m[0][1], strlen($m[0][0])); $next = $m[0][1]; } @@ -327,9 +352,8 @@ protected function _tagAttributes() array_unshift($trArr, $m[1][0]); $tagHtml = substr_replace($tagHtml, '', $m[0][1], strlen($m[0][0])); } - - $trAttr = ' translate='.$quoteHtml.'['.join(',', $trArr).']'.$quoteHtml; - $tagHtml = preg_replace('#/?>$#', $trAttr . '$0', $tagHtml); + $trAttr = ' translate=' . $quoteHtml . '[' . join(',', $trArr) . ']' . $quoteHtml; + $tagHtml = preg_replace('#' . $tagEndRegexp . '#', $trAttr . '$0', $tagHtml); $this->_content = substr_replace($this->_content, $tagHtml, $tagMatch[0][1], $tagMatch[9][1]+1-$tagMatch[0][1]); @@ -354,7 +378,8 @@ protected function _specialTags() $nextTag = 0; $location = array_merge($this->_allowedTagsGlobal, $this->_allowedTagsSimple); - $tags = implode('|', array_merge(array_keys($this->_allowedTagsGlobal), array_keys($this->_allowedTagsSimple))); + $tags = implode('|', array_merge(array_keys($this->_allowedTagsGlobal), + array_keys($this->_allowedTagsSimple))); $tagRegExp = '#<(' . $tags . ')(\s+[^>]*|)(>)#i'; $tagMatch = array(); @@ -392,7 +417,9 @@ protected function _specialTags() $this->_content = substr_replace($this->_content, $tagHtml, $tagMatch[0][1], $tagLength); if (in_array($tag, array_keys($this->_allowedTagsSimple))) { - if (preg_match('# translate='.$quotePatern.'\[(.+?)\]'.$quotePatern.'#i', $tagMatch[0][0], $m, PREG_OFFSET_CAPTURE)) { + if (preg_match('# translate='.$quotePatern.'\[(.+?)\]'.$quotePatern.'#i', + $tagMatch[0][0], $m, PREG_OFFSET_CAPTURE) + ) { foreach ($trArr as $i=>$tr) { if (strpos($m[1][0], $tr)!==false) { unset($trArr[$i]); diff --git a/app/code/core/Mage/Core/Model/Url.php b/app/code/core/Mage/Core/Model/Url.php index 63bd855343..c51874fe9f 100644 --- a/app/code/core/Mage/Core/Model/Url.php +++ b/app/code/core/Mage/Core/Model/Url.php @@ -1035,6 +1035,38 @@ protected function _prepareSessionUrl($url) return $this; } + /** + * Rebuild URL to handle the case when session ID was changed + * + * @param string $url + * @return string + */ + public function getRebuiltUrl($url) + { + $this->parseUrl($url); + $port = $this->getPort(); + if ($port) { + $port = ':' . $port; + } else { + $port = ''; + } + $url = $this->getScheme() . '://' . $this->getHost() . $port . $this->getPath(); + + $this->_prepareSessionUrl($url); + + $query = $this->getQuery(); + if ($query) { + $url .= '?' . $query; + } + + $fragment = $this->getFragment(); + if ($fragment) { + $url .= '#' . $fragment; + } + + return $this->escape($url); + } + /** * Escape (enclosure) URL string * diff --git a/app/code/core/Mage/Core/controllers/AjaxController.php b/app/code/core/Mage/Core/controllers/AjaxController.php index 8d3bce5afa..368ed11769 100644 --- a/app/code/core/Mage/Core/controllers/AjaxController.php +++ b/app/code/core/Mage/Core/controllers/AjaxController.php @@ -49,7 +49,8 @@ public function translateAction () $item['custom'] = $filter->filter($item['custom']); } - echo Mage::helper('core/translate')->apply($translation, $area); - exit(); + $response = Mage::helper('core/translate')->apply($translation, $area); + $this->getResponse()->setBody($response); + $this->setFlag('', self::FLAG_NO_POST_DISPATCH, true); } } diff --git a/app/code/core/Mage/Core/etc/config.xml b/app/code/core/Mage/Core/etc/config.xml index 68e241fb98..af1cf91ab5 100644 --- a/app/code/core/Mage/Core/etc/config.xml +++ b/app/code/core/Mage/Core/etc/config.xml @@ -364,6 +364,9 @@
+ + AT,BE,BG,CY,CZ,DK,EE,FI,FR,DE,GR,HU,IE,IT,LV,LT,LU,MT,NL,PL,PT,RO,SK,SI,ES,SE,GB + 0 0,6 diff --git a/app/code/core/Mage/Core/etc/system.xml b/app/code/core/Mage/Core/etc/system.xml index 685194aab3..9675f9e97d 100644 --- a/app/code/core/Mage/Core/etc/system.xml +++ b/app/code/core/Mage/Core/etc/system.xml @@ -571,6 +571,7 @@ text + adminhtml/system_config_backend_filename 2 1 1 @@ -580,6 +581,7 @@ text + adminhtml/system_config_backend_filename 3 1 1 @@ -664,6 +666,15 @@ 1 1 + + + multiselect + adminhtml/system_config_source_country + 30 + 1 + 0 + 0 + @@ -738,6 +749,25 @@ 1 1 + + + select + adminhtml/system_config_source_country + 25 + 1 + 1 + 1 + 1 + + + + text + 27 + 1 + 1 + 1 + 1 +
textarea @@ -932,6 +962,7 @@ + This value must be greater than 0. text adminhtml/system_config_backend_admin_password_link_expirationperiod 30 diff --git a/app/code/core/Mage/Customer/Block/Widget/Name.php b/app/code/core/Mage/Customer/Block/Widget/Name.php index bd951eeb99..189c0ac97e 100644 --- a/app/code/core/Mage/Customer/Block/Widget/Name.php +++ b/app/code/core/Mage/Customer/Block/Widget/Name.php @@ -169,7 +169,16 @@ protected function _getAttribute($attributeCode) return parent::_getAttribute($attributeCode); } - return Mage::getSingleton('eav/config')->getAttribute('customer_address', $attributeCode); + $_attribute = Mage::getSingleton('eav/config')->getAttribute('customer_address', $attributeCode); + + if ($this->getForceUseCustomerRequiredAttributes() && !$_attribute->getIsRequired()) { + $customerAttribute = parent::_getAttribute($attributeCode); + if ($customerAttribute && $customerAttribute->getIsRequired()) { + $_attribute = $customerAttribute; + } + } + + return $_attribute; } /** diff --git a/app/code/core/Mage/Customer/Helper/Address.php b/app/code/core/Mage/Customer/Helper/Address.php index 9ff7d337e4..9ada743ed8 100644 --- a/app/code/core/Mage/Customer/Helper/Address.php +++ b/app/code/core/Mage/Customer/Helper/Address.php @@ -31,6 +31,13 @@ */ class Mage_Customer_Helper_Address extends Mage_Core_Helper_Abstract { + /** + * VAT Validation parameters XML paths + */ + const XML_PATH_VIV_DISABLE_AUTO_ASSIGN_DEFAULT = 'customer/create_account/viv_disable_auto_group_assign_default'; + const XML_PATH_VIV_ON_EACH_TRANSACTION = 'customer/create_account/viv_on_each_transaction'; + const XML_PATH_VAT_VALIDATION_ENABLED = 'customer/create_account/auto_group_assign'; + /** * Array of Customer Address Attributes * @@ -200,4 +207,36 @@ public function convertStreetLines($origStreets, $toCount) return $lines; } + + /** + * Check whether VAT ID validation is enabled + * + * @param Mage_Core_Model_Store|string|int $store + * @return bool + */ + public function isVatValidationEnabled($store = null) + { + return (bool)Mage::getStoreConfig(self::XML_PATH_VAT_VALIDATION_ENABLED, $store); + } + + /** + * Retrieve disable auto group assign default value + * + * @return bool + */ + public function getDisableAutoGroupAssignDefaultValue() + { + return (bool)Mage::getStoreConfig(self::XML_PATH_VIV_DISABLE_AUTO_ASSIGN_DEFAULT); + } + + /** + * Retrieve 'validate on each transaction' value + * + * @param Mage_Core_Model_Store|string|int $store + * @return bool + */ + public function getValidateOnEachTransaction($store = null) + { + return (bool)Mage::getStoreConfig(self::XML_PATH_VIV_ON_EACH_TRANSACTION, $store); + } } diff --git a/app/code/core/Mage/Customer/Helper/Data.php b/app/code/core/Mage/Customer/Helper/Data.php index 541e726a60..6dd82f6d37 100644 --- a/app/code/core/Mage/Customer/Helper/Data.php +++ b/app/code/core/Mage/Customer/Helper/Data.php @@ -44,12 +44,44 @@ class Mage_Customer_Helper_Data extends Mage_Core_Helper_Abstract */ const XML_PATH_CUSTOMER_STARTUP_REDIRECT_TO_DASHBOARD = 'customer/startup/redirect_dashboard'; + /** + * Config pathes to VAT related customer groups + */ + const XML_PATH_CUSTOMER_VIV_INTRA_UNION_GROUP = 'customer/create_account/viv_intra_union_group'; + const XML_PATH_CUSTOMER_VIV_DOMESTIC_GROUP = 'customer/create_account/viv_domestic_group'; + const XML_PATH_CUSTOMER_VIV_INVALID_GROUP = 'customer/create_account/viv_invalid_group'; + const XML_PATH_CUSTOMER_VIV_ERROR_GROUP = 'customer/create_account/viv_error_group'; + + /** + * Config path to option that enables/disables automatic group assignment based on VAT + */ + const XML_PATH_CUSTOMER_VIV_GROUP_AUTO_ASSIGN = 'customer/create_account/viv_disable_auto_group_assign_default'; + + /** + * Config path to support email + */ + const XML_PATH_SUPPORT_EMAIL = 'trans_email/ident_support/email'; + + /** + * WSDL of VAT validation service + * + */ + const VAT_VALIDATION_WSDL_URL = 'http://ec.europa.eu/taxation_customs/vies/services/checkVatService?wsdl'; + /** * Configuration path to expiration period of reset password link */ const XML_PATH_CUSTOMER_RESET_PASSWORD_LINK_EXPIRATION_PERIOD = 'default/customer/password/reset_link_expiration_period'; + /** + * VAT class constants + */ + const VAT_CLASS_DOMESTIC = 'domestic'; + const VAT_CLASS_INTRA_UNION = 'intra_union'; + const VAT_CLASS_INVALID = 'invalid'; + const VAT_CLASS_ERROR = 'error'; + /** * Customer groups collection * @@ -346,4 +378,228 @@ public function getResetPasswordLinkExpirationPeriod() { return (int) Mage::getConfig()->getNode(self::XML_PATH_CUSTOMER_RESET_PASSWORD_LINK_EXPIRATION_PERIOD); } + + /** + * Get default customer group id + * + * @param Mage_Core_Model_Store|string|int $store + * @return int + */ + public function getDefaultCustomerGroupId($store = null) + { + return (int)Mage::getStoreConfig(Mage_Customer_Model_Group::XML_PATH_DEFAULT_ID, $store); + } + + /** + * Retrieve customer group ID based on his VAT number + * + * @param string $customerCountryCode + * @param Varien_Object $vatValidationResult + * @param Mage_Core_Model_Store|string|int $store + * + * @return null|int + */ + public function getCustomerGroupIdBasedOnVatNumber($customerCountryCode, $vatValidationResult, $store = null) + { + $groupId = null; + + $vatClass = $this->getCustomerVatClass($customerCountryCode, $vatValidationResult); + + $vatClassToGroupXmlPathMap = array( + self::VAT_CLASS_DOMESTIC => self::XML_PATH_CUSTOMER_VIV_DOMESTIC_GROUP, + self::VAT_CLASS_INTRA_UNION => self::XML_PATH_CUSTOMER_VIV_INTRA_UNION_GROUP, + self::VAT_CLASS_INVALID => self::XML_PATH_CUSTOMER_VIV_INVALID_GROUP, + self::VAT_CLASS_ERROR => self::XML_PATH_CUSTOMER_VIV_ERROR_GROUP + ); + + if (isset($vatClassToGroupXmlPathMap[$vatClass])) { + $groupId = (int)Mage::getStoreConfig($vatClassToGroupXmlPathMap[$vatClass], $store); + } + + return $groupId; + } + + /** + * Send request to VAT validation service and return validation result + * + * @param string $countryCode + * @param string $vatNumber + * @param string $requesterCountryCode + * @param string $requesterVatNumber + * + * @return Varien_Object + */ + public function checkVatNumber($countryCode, $vatNumber, $requesterCountryCode = '', $requesterVatNumber = '') + { + // Default response + $gatewayResponse = new Varien_Object(array( + 'is_valid' => false, + 'request_date' => '', + 'request_identifier' => '', + 'request_success' => false + )); + + if (!extension_loaded('soap')) { + Mage::logException(Mage::exception('Mage_Core', + Mage::helper('core')->__('PHP SOAP extension is required.'))); + return $gatewayResponse; + } + + if (!$this->canCheckVatNumber($countryCode, $vatNumber, $requesterCountryCode, $requesterVatNumber)) { + return $gatewayResponse; + } + + try { + $soapClient = $this->_createVatNumberValidationSoapClient(); + + $requestParams = array(); + $requestParams['countryCode'] = $countryCode; + $requestParams['vatNumber'] = $vatNumber; + $requestParams['requesterCountryCode'] = $requesterCountryCode; + $requestParams['requesterVatNumber'] = $requesterVatNumber; + + // Send request to service + $result = $soapClient->checkVatApprox($requestParams); + + $gatewayResponse->setIsValid((boolean) $result->valid); + $gatewayResponse->setRequestDate((string) $result->requestDate); + $gatewayResponse->setRequestIdentifier((string) $result->requestIdentifier); + $gatewayResponse->setRequestSuccess(true); + } catch (Exception $exception) { + $gatewayResponse->setIsValid(false); + $gatewayResponse->setRequestDate(''); + $gatewayResponse->setRequestIdentifier(''); + } + + return $gatewayResponse; + } + + /** + * Check if parameters are valid to send to VAT validation service + * + * @param string $countryCode + * @param string $vatNumber + * @param string $requesterCountryCode + * @param string $requesterVatNumber + * + * @return boolean + */ + public function canCheckVatNumber($countryCode, $vatNumber, $requesterCountryCode, $requesterVatNumber) + { + $result = true; + /** @var $coreHelper Mage_Core_Helper_Data */ + $coreHelper = Mage::helper('core'); + + if (!is_string($countryCode) + || !is_string($vatNumber) + || !is_string($requesterCountryCode) + || !is_string($requesterVatNumber) + || empty($countryCode) + || !$coreHelper->isCountryInEU($countryCode) + || empty($vatNumber) + || (empty($requesterCountryCode) && !empty($requesterVatNumber)) + || (!empty($requesterCountryCode) && empty($requesterVatNumber)) + || (!empty($requesterCountryCode) && !$coreHelper->isCountryInEU($requesterCountryCode)) + ) { + $result = false; + } + + return $result; + } + + /** + * Get VAT class + * + * @param string $customerCountryCode + * @param Varien_Object $vatValidationResult + * @return null|string + */ + public function getCustomerVatClass($customerCountryCode, $vatValidationResult) + { + $vatClass = null; + + $isVatNumberValid = $vatValidationResult->getIsValid(); + + if (is_string($customerCountryCode) + && !empty($customerCountryCode) + && $customerCountryCode === Mage::helper('core')->getMerchantCountryCode() + && $isVatNumberValid + ) { + $vatClass = self::VAT_CLASS_DOMESTIC; + } elseif ($isVatNumberValid) { + $vatClass = self::VAT_CLASS_INTRA_UNION; + } else { + $vatClass = self::VAT_CLASS_INVALID; + } + + if (!$vatValidationResult->getRequestSuccess()) { + $vatClass = self::VAT_CLASS_ERROR; + } + + return $vatClass; + } + + /** + * Get validation message that will be displayed to user by VAT validation result object + * + * @param Mage_Customer_Model_Address $customerAddress + * @param bool $customerGroupAutoAssignDisabled + * @param Varien_Object $validationResult + * @return Varien_Object + */ + public function getVatValidationUserMessage($customerAddress, $customerGroupAutoAssignDisabled, $validationResult) + { + $message = ''; + $isError = true; + $customerVatClass = $this->getCustomerVatClass($customerAddress->getCountryId(), $validationResult); + $groupAutoAssignDisabled = Mage::getStoreConfigFlag(self::XML_PATH_CUSTOMER_VIV_GROUP_AUTO_ASSIGN); + + $willChargeTaxMessage = $this->__('You will be charged tax.'); + $willNotChargeTaxMessage = $this->__('You will not be charged tax.'); + + if ($validationResult->getIsValid()) { + $message = $this->__('Your VAT ID was successfully validated.'); + $isError = false; + + if (!$groupAutoAssignDisabled && !$customerGroupAutoAssignDisabled) { + $message .= ' ' . ($customerVatClass == self::VAT_CLASS_DOMESTIC + ? $willChargeTaxMessage + : $willNotChargeTaxMessage); + } + } else if ($validationResult->getRequestSuccess()) { + $message = sprintf( + $this->__('The VAT ID entered (%s) is not a valid VAT ID.') . ' ', + $this->escapeHtml($customerAddress->getVatId()) + ); + if (!$groupAutoAssignDisabled && !$customerGroupAutoAssignDisabled) { + $message .= $willChargeTaxMessage; + } + } + else { + $contactUsMessage = sprintf($this->__('If you believe this is an error, please contact us at %s'), + Mage::getStoreConfig(self::XML_PATH_SUPPORT_EMAIL)); + + $message = $this->__('Your Tax ID cannot be validated.') . ' ' + . (!$groupAutoAssignDisabled && !$customerGroupAutoAssignDisabled + ? $willChargeTaxMessage . ' ' : '') + . $contactUsMessage; + } + + $validationMessageEnvelope = new Varien_Object(); + $validationMessageEnvelope->setMessage($message); + $validationMessageEnvelope->setIsError($isError); + + return $validationMessageEnvelope; + } + + /** + * Create SOAP client based on VAT validation service WSDL + * + * @param boolean $trace + * @return SoapClient + */ + protected function _createVatNumberValidationSoapClient($trace = false) + { + return new SoapClient(self::VAT_VALIDATION_WSDL_URL, array('trace' => $trace)); + } } diff --git a/app/code/core/Mage/Customer/Model/Address.php b/app/code/core/Mage/Customer/Model/Address.php index 20697c933a..0576f82731 100644 --- a/app/code/core/Mage/Customer/Model/Address.php +++ b/app/code/core/Mage/Customer/Model/Address.php @@ -150,7 +150,7 @@ public function getEntityTypeId() } return $entityTypeId; } - + /** * Return Region ID * @@ -160,7 +160,7 @@ public function getRegionId() { return (int)$this->getData('region_id'); } - + /** * Set Region ID. $regionId is automatically converted to integer * diff --git a/app/code/core/Mage/Customer/Model/Address/Abstract.php b/app/code/core/Mage/Customer/Model/Address/Abstract.php index 4c85d49281..426fcbf428 100644 --- a/app/code/core/Mage/Customer/Model/Address/Abstract.php +++ b/app/code/core/Mage/Customer/Model/Address/Abstract.php @@ -231,7 +231,10 @@ public function getRegionId() $this->setData('region_id', $region); $this->unsRegion(); } else { - $regionModel = Mage::getModel('directory/region')->loadByCode($this->getRegionCode(), $this->getCountryId()); + $regionModel = Mage::getModel('directory/region')->loadByCode( + $this->getRegionCode(), + $this->getCountryId() + ); $this->setData('region_id', $regionModel->getId()); } } @@ -240,10 +243,6 @@ public function getRegionId() public function getCountry() { - /*if ($this->getData('country_id') && !$this->getData('country')) { - $this->setData('country', Mage::getModel('directory/country')->load($this->getData('country_id'))->getIso2Code()); - } - return $this->getData('country');*/ $country = $this->getCountryId(); return $country ? $country : $this->getData('country'); } @@ -256,7 +255,9 @@ public function getCountry() public function getCountryModel() { if(!isset(self::$_countryModels[$this->getCountryId()])) { - self::$_countryModels[$this->getCountryId()] = Mage::getModel('directory/country')->load($this->getCountryId()); + self::$_countryModels[$this->getCountryId()] = Mage::getModel('directory/country')->load( + $this->getCountryId() + ); } return self::$_countryModels[$this->getCountryId()]; @@ -293,7 +294,7 @@ public function getHtmlFormat() */ public function getFormated($html=false) { - return $this->format($html ? 'html' : 'text');//Mage::getModel('directory/country')->load($this->getCountryId())->formatAddress($this, $html); + return $this->format($html ? 'html' : 'text'); } public function format($type) @@ -331,40 +332,41 @@ protected function _beforeSave() public function validate() { $errors = array(); - $helper = Mage::helper('customer'); $this->implodeStreetAddress(); if (!Zend_Validate::is($this->getFirstname(), 'NotEmpty')) { - $errors[] = $helper->__('Please enter the first name.'); + $errors[] = Mage::helper('customer')->__('Please enter the first name.'); } if (!Zend_Validate::is($this->getLastname(), 'NotEmpty')) { - $errors[] = $helper->__('Please enter the last name.'); + $errors[] = Mage::helper('customer')->__('Please enter the last name.'); } if (!Zend_Validate::is($this->getStreet(1), 'NotEmpty')) { - $errors[] = $helper->__('Please enter the street.'); + $errors[] = Mage::helper('customer')->__('Please enter the street.'); } if (!Zend_Validate::is($this->getCity(), 'NotEmpty')) { - $errors[] = $helper->__('Please enter the city.'); + $errors[] = Mage::helper('customer')->__('Please enter the city.'); } if (!Zend_Validate::is($this->getTelephone(), 'NotEmpty')) { - $errors[] = $helper->__('Please enter the telephone number.'); + $errors[] = Mage::helper('customer')->__('Please enter the telephone number.'); } $_havingOptionalZip = Mage::helper('directory')->getCountriesWithOptionalZip(); - if (!in_array($this->getCountryId(), $_havingOptionalZip) && !Zend_Validate::is($this->getPostcode(), 'NotEmpty')) { - $errors[] = $helper->__('Please enter the zip/postal code.'); + if (!in_array($this->getCountryId(), $_havingOptionalZip) + && !Zend_Validate::is($this->getPostcode(), 'NotEmpty') + ) { + $errors[] = Mage::helper('customer')->__('Please enter the zip/postal code.'); } if (!Zend_Validate::is($this->getCountryId(), 'NotEmpty')) { - $errors[] = $helper->__('Please enter the country.'); + $errors[] = Mage::helper('customer')->__('Please enter the country.'); } if ($this->getCountryModel()->getRegionCollection()->getSize() && !Zend_Validate::is($this->getRegionId(), 'NotEmpty')) { - $errors[] = $helper->__('Please enter the state/province.'); + $errors[] = Mage::helper('customer')->__('Please enter the state/province.'); } if (empty($errors) || $this->getShouldIgnoreValidation()) { diff --git a/app/code/core/Mage/Customer/Model/Attribute/Backend/Data/Boolean.php b/app/code/core/Mage/Customer/Model/Attribute/Backend/Data/Boolean.php new file mode 100644 index 0000000000..fe439b8d7d --- /dev/null +++ b/app/code/core/Mage/Customer/Model/Attribute/Backend/Data/Boolean.php @@ -0,0 +1,52 @@ + + */ +class Mage_Customer_Model_Attribute_Backend_Data_Boolean + extends Mage_Eav_Model_Entity_Attribute_Backend_Abstract +{ + /** + * Prepare data before attribute save + * + * @param Mage_Customer_Model_Customer $customer + * @return Mage_Customer_Model_Attribute_Backend_Data_Boolean + */ + public function beforeSave($customer) + { + $attributeName = $this->getAttribute()->getName(); + $inputValue = $customer->getData($attributeName); + $sanitizedValue = (!empty($inputValue)) ? '1' : '0'; + $customer->setData($attributeName, $sanitizedValue); + return $this; + } +} diff --git a/app/code/core/Mage/Customer/Model/Customer.php b/app/code/core/Mage/Customer/Model/Customer.php index ba2ac0ec1c..e0017c0596 100644 --- a/app/code/core/Mage/Customer/Model/Customer.php +++ b/app/code/core/Mage/Customer/Model/Customer.php @@ -375,9 +375,13 @@ public function hashPassword($password, $salt = null) * @param int $length * @return string */ - public function generatePassword($length = 6) + public function generatePassword($length = 8) { - return Mage::helper('core')->getRandomString($length); + $chars = Mage_Core_Helper_Data::CHARS_PASSWORD_LOWERS + . Mage_Core_Helper_Data::CHARS_PASSWORD_UPPERS + . Mage_Core_Helper_Data::CHARS_PASSWORD_DIGITS + . Mage_Core_Helper_Data::CHARS_PASSWORD_SPECIALS; + return Mage::helper('core')->getRandomString($length, $chars); } /** @@ -788,43 +792,42 @@ public function setStore(Mage_Core_Model_Store $store) public function validate() { $errors = array(); - $customerHelper = Mage::helper('customer'); if (!Zend_Validate::is( trim($this->getFirstname()) , 'NotEmpty')) { - $errors[] = $customerHelper->__('The first name cannot be empty.'); + $errors[] = Mage::helper('customer')->__('The first name cannot be empty.'); } if (!Zend_Validate::is( trim($this->getLastname()) , 'NotEmpty')) { - $errors[] = $customerHelper->__('The last name cannot be empty.'); + $errors[] = Mage::helper('customer')->__('The last name cannot be empty.'); } if (!Zend_Validate::is($this->getEmail(), 'EmailAddress')) { - $errors[] = $customerHelper->__('Invalid email address "%s".', $this->getEmail()); + $errors[] = Mage::helper('customer')->__('Invalid email address "%s".', $this->getEmail()); } $password = $this->getPassword(); if (!$this->getId() && !Zend_Validate::is($password , 'NotEmpty')) { - $errors[] = $customerHelper->__('The password cannot be empty.'); + $errors[] = Mage::helper('customer')->__('The password cannot be empty.'); } if (strlen($password) && !Zend_Validate::is($password, 'StringLength', array(6))) { - $errors[] = $customerHelper->__('The minimum password length is %s', 6); + $errors[] = Mage::helper('customer')->__('The minimum password length is %s', 6); } $confirmation = $this->getConfirmation(); if ($password != $confirmation) { - $errors[] = $customerHelper->__('Please make sure your passwords match.'); + $errors[] = Mage::helper('customer')->__('Please make sure your passwords match.'); } $entityType = Mage::getSingleton('eav/config')->getEntityType('customer'); $attribute = Mage::getModel('customer/attribute')->loadByCode($entityType, 'dob'); if ($attribute->getIsRequired() && '' == trim($this->getDob())) { - $errors[] = $customerHelper->__('The Date of Birth is required.'); + $errors[] = Mage::helper('customer')->__('The Date of Birth is required.'); } $attribute = Mage::getModel('customer/attribute')->loadByCode($entityType, 'taxvat'); if ($attribute->getIsRequired() && '' == trim($this->getTaxvat())) { - $errors[] = $customerHelper->__('The TAX/VAT number is required.'); + $errors[] = Mage::helper('customer')->__('The TAX/VAT number is required.'); } $attribute = Mage::getModel('customer/attribute')->loadByCode($entityType, 'gender'); if ($attribute->getIsRequired() && '' == trim($this->getGender())) { - $errors[] = $customerHelper->__('Gender is required.'); + $errors[] = Mage::helper('customer')->__('Gender is required.'); } if (empty($errors)) { @@ -842,7 +845,6 @@ public function validate() public function importFromTextArray(array $row) { $this->resetErrors(); - $hlp = Mage::helper('customer'); $line = $row['i']; $row = $row['row']; @@ -851,7 +853,7 @@ public function importFromTextArray(array $row) $website = Mage::getModel('core/website')->load($row['website_code'], 'code'); if (!$website->getId()) { - $this->addError($hlp->__('Invalid website, skipping the record, line: %s', $line)); + $this->addError(Mage::helper('customer')->__('Invalid website, skipping the record, line: %s', $line)); } else { $row['website_id'] = $website->getWebsiteId(); @@ -860,18 +862,18 @@ public function importFromTextArray(array $row) // Validate Email if (empty($row['email'])) { - $this->addError($hlp->__('Missing email, skipping the record, line: %s', $line)); + $this->addError(Mage::helper('customer')->__('Missing email, skipping the record, line: %s', $line)); } else { $this->loadByEmail($row['email']); } if (empty($row['entity_id'])) { if ($this->getData('entity_id')) { - $this->addError($hlp->__('The customer email (%s) already exists, skipping the record, line: %s', $row['email'], $line)); + $this->addError(Mage::helper('customer')->__('The customer email (%s) already exists, skipping the record, line: %s', $row['email'], $line)); } } else { if ($row['entity_id'] != $this->getData('entity_id')) { - $this->addError($hlp->__('The customer ID and email did not match, skipping the record, line: %s', $line)); + $this->addError(Mage::helper('customer')->__('The customer ID and email did not match, skipping the record, line: %s', $line)); } else { $this->unsetData(); $this->load($row['entity_id']); @@ -883,7 +885,7 @@ public function importFromTextArray(array $row) } if (empty($row['website_code'])) { - $this->addError($hlp->__('Missing website, skipping the record, line: %s', $line)); + $this->addError(Mage::helper('customer')->__('Missing website, skipping the record, line: %s', $line)); } if (empty($row['group'])) { @@ -891,10 +893,10 @@ public function importFromTextArray(array $row) } if (empty($row['firstname'])) { - $this->addError($hlp->__('Missing first name, skipping the record, line: %s', $line)); + $this->addError(Mage::helper('customer')->__('Missing first name, skipping the record, line: %s', $line)); } if (empty($row['lastname'])) { - $this->addError($hlp->__('Missing last name, skipping the record, line: %s', $line)); + $this->addError(Mage::helper('customer')->__('Missing last name, skipping the record, line: %s', $line)); } if (!empty($row['password_new'])) { @@ -915,7 +917,7 @@ public function importFromTextArray(array $row) } if (!$this->validateAddress($row, 'billing')) { - $this->printError($hlp->__('Invalid billing address for (%s)', $row['email']), $line); + $this->printError(Mage::helper('customer')->__('Invalid billing address for (%s)', $row['email']), $line); } else { // Handling billing address $billingAddress = $this->getPrimaryBillingAddress(); @@ -956,7 +958,7 @@ public function importFromTextArray(array $row) } if (!$this->validateAddress($row, 'shipping')) { - $this->printError($hlp->__('Invalid shipping address for (%s)', $row['email']), $line); + $this->printError(Mage::helper('customer')->__('Invalid shipping address for (%s)', $row['email']), $line); } else { // Handling shipping address $shippingAddress = $this->getPrimaryShippingAddress(); @@ -1300,7 +1302,7 @@ public function isResetPasswordLinkTokenExpired() $tokenExpirationPeriod = Mage::helper('customer')->getResetPasswordLinkExpirationPeriod(); - $currentDate = Varien_Date::now(true); + $currentDate = Varien_Date::now(); $currentTimestamp = Varien_Date::toTimestamp($currentDate); $tokenTimestamp = Varien_Date::toTimestamp($resetPasswordLinkTokenCreatedAt); if ($tokenTimestamp > $currentTimestamp) { diff --git a/app/code/core/Mage/Customer/Model/Form.php b/app/code/core/Mage/Customer/Model/Form.php index 4670d7a1d2..71d8fd332a 100644 --- a/app/code/core/Mage/Customer/Model/Form.php +++ b/app/code/core/Mage/Customer/Model/Form.php @@ -48,4 +48,15 @@ class Mage_Customer_Model_Form extends Mage_Eav_Model_Form */ protected $_entityTypeCode = 'customer'; + /** + * Get EAV Entity Form Attribute Collection for Customer + * exclude 'created_at' + * + * @return Mage_Customer_Model_Resource_Form_Attribute_Collection + */ + protected function _getFormAttributeCollection() + { + return parent::_getFormAttributeCollection() + ->addFieldToFilter('attribute_code', array('neq' => 'created_at')); + } } diff --git a/app/code/core/Mage/Customer/Model/Observer.php b/app/code/core/Mage/Customer/Model/Observer.php index 65cadcab54..d5c56f333f 100644 --- a/app/code/core/Mage/Customer/Model/Observer.php +++ b/app/code/core/Mage/Customer/Model/Observer.php @@ -31,11 +31,181 @@ */ class Mage_Customer_Model_Observer { + /** + * VAT ID validation processed flag code + */ + const VIV_PROCESSED_FLAG = 'viv_after_address_save_processed'; + + /** + * VAT ID validation currently saved address flag + */ + const VIV_CURRENTLY_SAVED_ADDRESS = 'currently_saved_address'; + + /** + * Check whether specified billing address is default for its customer + * + * @param Mage_Customer_Model_Address $address + * @return bool + */ + protected function _isDefaultBilling($address) + { + return $address->getId() && $address->getId() == $address->getCustomer()->getDefaultBilling(); + } + + /** + * Check whether specified address should be processed in after_save event handler + * + * @param Mage_Customer_Model_Address $address + * @return bool + */ + protected function _canProcessAddress($address) + { + if ($address->getForceProcess()) { + return true; + } + + if (Mage::registry(self::VIV_CURRENTLY_SAVED_ADDRESS) != $address->getId()) { + return false; + } + + return $this->_isDefaultBilling($address); + } + + /** + * Before load layout event handler + * + * @param Varien_Event_Observer $observer + */ public function beforeLoadLayout($observer) { $loggedIn = Mage::getSingleton('customer/session')->isLoggedIn(); $observer->getEvent()->getLayout()->getUpdate() - ->addHandle('customer_logged_'.($loggedIn?'in':'out')); + ->addHandle('customer_logged_' . ($loggedIn ? 'in' : 'out')); + } + + /** + * Address before save event handler + * + * @param Varien_Event_Observer $observer + */ + public function beforeAddressSave($observer) + { + if (Mage::registry(self::VIV_CURRENTLY_SAVED_ADDRESS)) { + Mage::unregister(self::VIV_CURRENTLY_SAVED_ADDRESS); + } + + /** @var $customerAddress Mage_Customer_Model_Address */ + $customerAddress = $observer->getCustomerAddress(); + if ($customerAddress->getId()) { + Mage::register(self::VIV_CURRENTLY_SAVED_ADDRESS, $customerAddress->getId()); + } elseif ($customerAddress->getIsDefaultBilling()) { + $customerAddress->setForceProcess(true); + } else { + Mage::register(self::VIV_CURRENTLY_SAVED_ADDRESS, 'new_address'); + } + } + + /** + * Address after save event handler + * + * @param Varien_Event_Observer $observer + */ + public function afterAddressSave($observer) + { + /** @var $customerAddress Mage_Customer_Model_Address */ + $customerAddress = $observer->getCustomerAddress(); + $customer = $customerAddress->getCustomer(); + + if (!Mage::helper('customer/address')->isVatValidationEnabled($customer->getStore()) + || Mage::registry(self::VIV_PROCESSED_FLAG) + || !$this->_canProcessAddress($customerAddress) + ) { + return; + } + + try { + Mage::register(self::VIV_PROCESSED_FLAG, true); + + /** @var $customerHelper Mage_Customer_Helper_Data */ + $customerHelper = Mage::helper('customer'); + + if ($customerAddress->getVatId() == '' + || !Mage::helper('core')->isCountryInEU($customerAddress->getCountry())) + { + $defaultGroupId = $customerHelper->getDefaultCustomerGroupId($customer->getStore()); + + if (!$customer->getDisableAutoGroupChange() && $customer->getGroupId() != $defaultGroupId) { + $customer->setGroupId($defaultGroupId); + $customer->save(); + } + } else { + + $result = $customerHelper->checkVatNumber( + $customerAddress->getCountryId(), + $customerAddress->getVatId() + ); + + $newGroupId = $customerHelper->getCustomerGroupIdBasedOnVatNumber( + $customerAddress->getCountryId(), $result, $customer->getStore() + ); + + if (!$customer->getDisableAutoGroupChange() && $customer->getGroupId() != $newGroupId) { + $customer->setGroupId($newGroupId); + $customer->save(); + } + + if (!Mage::app()->getStore()->isAdmin()) { + $validationMessage = Mage::helper('customer')->getVatValidationUserMessage($customerAddress, + $customer->getDisableAutoGroupChange(), $result); + + if (!$validationMessage->getIsError()) { + Mage::getSingleton('customer/session')->addSuccess($validationMessage->getMessage()); + } else { + Mage::getSingleton('customer/session')->addError($validationMessage->getMessage()); + } + } + } + } catch (Exception $e) { + Mage::register(self::VIV_PROCESSED_FLAG, false, true); + } + } + + /** + * Assign custom renderer for VAT ID field in billing address form + * + * @param Varien_Event_Observer $observer + */ + public function prepareFormAfter($observer) + { + /** @var $formBlock Mage_Adminhtml_Block_Sales_Order_Create_Billing_Address */ + $formBlock = $observer->getForm(); + $formBlock->getForm()->getElement('vat_id')->setRenderer( + $formBlock->getLayout()->createBlock('adminhtml/customer_sales_order_address_form_billing_renderer_vat') + ); + } + + /** + * Revert emulated customer group_id + * + * @param Varien_Event_Observer $observer + */ + public function quoteSubmitAfter($observer) + { + /** @var $customer Mage_Customer_Model_Customer */ + $customer = $observer->getQuote()->getCustomer(); + + if (!Mage::helper('customer/address')->isVatValidationEnabled($customer->getStore())) { + return; + } + + if (!$customer->getId()) { + return; + } + + $customer->setGroupId( + $customer->getOrigData('group_id') + ); + $customer->save(); } } diff --git a/app/code/core/Mage/Customer/Model/Resource/Customer.php b/app/code/core/Mage/Customer/Model/Resource/Customer.php index c4e0622b67..34ef426497 100755 --- a/app/code/core/Mage/Customer/Model/Resource/Customer.php +++ b/app/code/core/Mage/Customer/Model/Resource/Customer.php @@ -325,7 +325,7 @@ public function setNewIncrementId(Varien_Object $object) public function changeResetPasswordLinkToken(Mage_Customer_Model_Customer $customer, $newResetPasswordLinkToken) { if (is_string($newResetPasswordLinkToken) && !empty($newResetPasswordLinkToken)) { $customer->setRpToken($newResetPasswordLinkToken); - $currentDate = Varien_Date::now(true); + $currentDate = Varien_Date::now(); $customer->setRpTokenCreatedAt($currentDate); $this->saveAttribute($customer, 'rp_token'); $this->saveAttribute($customer, 'rp_token_created_at'); diff --git a/app/code/core/Mage/Customer/Model/Resource/Customer/Collection.php b/app/code/core/Mage/Customer/Model/Resource/Customer/Collection.php index ae35c9481f..ddf1e9d9d2 100755 --- a/app/code/core/Mage/Customer/Model/Resource/Customer/Collection.php +++ b/app/code/core/Mage/Customer/Model/Resource/Customer/Collection.php @@ -80,26 +80,29 @@ public function addNameToSelect() if (isset($fields['prefix'])) { $concatenate[] = $adapter->getCheckSql( '{{prefix}} IS NOT NULL AND {{prefix}} != \'\'', - 'LTRIM(RTRIM({{prefix}}))', + $adapter->getConcatSql(array('LTRIM(RTRIM({{prefix}}))', '\' \'')), '\'\''); } $concatenate[] = 'LTRIM(RTRIM({{firstname}}))'; + $concatenate[] = '\' \''; if (isset($fields['middlename'])) { $concatenate[] = $adapter->getCheckSql( '{{middlename}} IS NOT NULL AND {{middlename}} != \'\'', - 'LTRIM(RTRIM({{middlename}}))', + $adapter->getConcatSql(array('LTRIM(RTRIM({{middlename}}))', '\' \'')), '\'\''); } $concatenate[] = 'LTRIM(RTRIM({{lastname}}))'; if (isset($fields['suffix'])) { $concatenate[] = $adapter - ->getCheckSql('{{suffix}} IS NOT NULL AND {{suffix}} != \'\'', "LTRIM(RTRIM({{suffix}}))", "''"); + ->getCheckSql('{{suffix}} IS NOT NULL AND {{suffix}} != \'\'', + $adapter->getConcatSql(array('\' \'', 'LTRIM(RTRIM({{suffix}}))')), + '\'\''); } - $nameExpr = $adapter->getConcatSql($concatenate, ' '); + $nameExpr = $adapter->getConcatSql($concatenate); $this->addExpressionAttributeToSelect('name', $nameExpr, $fields); - + return $this; } diff --git a/app/code/core/Mage/Customer/Model/Resource/Group.php b/app/code/core/Mage/Customer/Model/Resource/Group.php index 1c624157fd..0764df6847 100755 --- a/app/code/core/Mage/Customer/Model/Resource/Group.php +++ b/app/code/core/Mage/Customer/Model/Resource/Group.php @@ -85,8 +85,7 @@ protected function _afterDelete(Mage_Core_Model_Abstract $group) ->addAttributeToFilter('group_id', $group->getId()) ->load(); foreach ($customerCollection as $customer) { - $defaultGroupId = Mage::getStoreConfig(Mage_Customer_Model_Group::XML_PATH_DEFAULT_ID, - $customer->getStoreId()); + $defaultGroupId = Mage::helper('customer')->getDefaultCustomerGroupId($customer->getStoreId()); $customer->setGroupId($defaultGroupId); $customer->save(); } diff --git a/app/code/core/Mage/Customer/Model/Session.php b/app/code/core/Mage/Customer/Model/Session.php index 07e81b47d9..9903e42f15 100644 --- a/app/code/core/Mage/Customer/Model/Session.php +++ b/app/code/core/Mage/Customer/Model/Session.php @@ -86,9 +86,7 @@ public function setCustomer(Mage_Customer_Model_Customer $customer) // check if customer is not confirmed if ($customer->isConfirmationRequired()) { if ($customer->getConfirmation()) { - throw new Exception('This customer is not confirmed and cannot log in.', - Mage_Customer_Model_Customer::EXCEPTION_EMAIL_NOT_CONFIRMED - ); + return $this->_logout(); } } $this->_customer = $customer; @@ -170,7 +168,10 @@ public function getCustomerGroupId() if ($this->getData('customer_group_id')) { return $this->getData('customer_group_id'); } - return ($this->isLoggedIn()) ? $this->getCustomer()->getGroupId() : Mage_Customer_Model_Group::NOT_LOGGED_IN_ID; + if ($this->isLoggedIn() && $this->getCustomer()) { + return $this->getCustomer()->getGroupId(); + } + return Mage_Customer_Model_Group::NOT_LOGGED_IN_ID; } /** @@ -250,8 +251,7 @@ public function logout() { if ($this->isLoggedIn()) { Mage::dispatchEvent('customer_logout', array('customer' => $this->getCustomer()) ); - $this->setId(null); - $this->getCookie()->delete($this->getSessionName()); + $this->_logout(); } return $this; } @@ -286,9 +286,24 @@ protected function _setAuthUrl($key, $url) { $url = Mage::helper('core/url') ->removeRequestParam($url, Mage::getSingleton('core/session')->getSessionIdQueryParam()); + // Add correct session ID to URL if needed + $url = Mage::getModel('core/url')->getRebuiltUrl($url); return $this->setData($key, $url); } + /** + * Logout without dispatching event + * + * @return Mage_Customer_Model_Session + */ + protected function _logout() + { + $this->setId(null); + $this->setCustomerGroupId(Mage_Customer_Model_Group::NOT_LOGGED_IN_ID); + $this->getCookie()->delete($this->getSessionName()); + return $this; + } + /** * Set Before auth url * @@ -310,4 +325,17 @@ public function setAfterAuthUrl($url) { return $this->_setAuthUrl('after_auth_url', $url); } + + /** + * Reset core session hosts after reseting session ID + * + * @return Mage_Customer_Model_Session + */ + public function renewSession() + { + parent::renewSession(); + Mage::getSingleton('core/session')->unsSessionHosts(); + + return $this; + } } diff --git a/app/code/core/Mage/Customer/controllers/AccountController.php b/app/code/core/Mage/Customer/controllers/AccountController.php index f3a69f39f0..473774680f 100644 --- a/app/code/core/Mage/Customer/controllers/AccountController.php +++ b/app/code/core/Mage/Customer/controllers/AccountController.php @@ -193,7 +193,9 @@ protected function _loginPostRedirect() )) { $referer = $this->getRequest()->getParam(Mage_Customer_Helper_Data::REFERER_QUERY_PARAM_NAME); if ($referer) { - $referer = Mage::helper('core')->urlDecode($referer); + // Rebuild referer URL to handle the case when SID was changed + $referer = Mage::getModel('core/url') + ->getRebuiltUrl(Mage::helper('core')->urlDecode($referer)); if ($this->_isUrlInternal($referer)) { $session->setBeforeAuthUrl($referer); } @@ -392,6 +394,11 @@ protected function _welcomeCustomer(Mage_Customer_Model_Customer $customer, $isJ $this->_getSession()->addSuccess( $this->__('Thank you for registering with %s.', Mage::app()->getStore()->getFrontendName()) ); + if ($this->_isVatValidationEnabled()) { + $this->_getSession()->addSuccess( + $this->__('If you are a registered VAT customer, please click here to enter you billing address to see proper VAT calculated', Mage::getUrl('customer/address/edit')) + ); + } $customer->sendNewAccountEmail( $isJustConfirmed ? 'confirmed' : 'registered', @@ -589,7 +596,7 @@ public function resetPasswordAction() $this->renderLayout(); } catch (Exception $exception) { $this->_getSession()->addError(Mage::helper('customer')->__('Your password reset link has expired.')); - $this->_redirect('*/*/'); + $this->_redirect('*/*/forgotpassword'); } } @@ -819,4 +826,15 @@ protected function _filterPostData($data) $data = $this->_filterDates($data, array('dob')); return $data; } + + /** + * Check whether VAT ID validation is enabled + * + * @param Mage_Core_Model_Store|string|int $store + * @return bool + */ + protected function _isVatValidationEnabled($store = null) + { + return Mage::helper('customer/address')->isVatValidationEnabled($store); + } } diff --git a/app/code/core/Mage/Customer/etc/config.xml b/app/code/core/Mage/Customer/etc/config.xml index 603e424b96..5f6d020ecc 100644 --- a/app/code/core/Mage/Customer/etc/config.xml +++ b/app/code/core/Mage/Customer/etc/config.xml @@ -28,7 +28,7 @@ - 1.6.1.0 + 1.6.2.0.1 @@ -247,6 +247,79 @@ 1 + + + * + + + * + + + * + + + * + + + * + + + + + * + + + * + + + * + + + * + + + * + + + + + * + * + + + * + * + + + * + * + + + * + * + + + * + * + + + + + * + + + * + + + * + + + * + + + * + +
@@ -345,6 +418,24 @@ + + + + + customer/observer + beforeAddressSave + + + + + + + customer/observer + afterAddressSave + + + + @@ -356,6 +447,16 @@ + + + + + customer/observer + prepareFormAfter + + + + @@ -370,6 +471,14 @@ + + + + customer/observer + quoteSubmitAfter + + + @@ -418,7 +527,7 @@ support customer_password_forgot_email_template customer_password_remind_email_template - 3 + 1
2 @@ -444,7 +553,8 @@ {{if city}}{{var city}}, {{/if}}{{if region}}{{var region}}, {{/if}}{{if postcode}}{{var postcode}}{{/if}} {{var country}} T: {{var telephone}} -{{depend fax}}F: {{var fax}}{{/depend}} +{{depend fax}}F: {{var fax}}{{/depend}} +{{depend vat_id}}VAT: {{var vat_id}}{{/depend}} {{depend prefix}}{{var prefix}} {{/depend}}{{var firstname}} {{depend middlename}}{{var middlename}} {{/depend}}{{var lastname}}{{depend suffix}} {{var suffix}}{{/depend}}, {{var street}}, {{var city}}, {{var region}} {{var postcode}}, {{var country}} {{depend prefix}}{{var prefix}} {{/depend}}{{var firstname}} {{depend middlename}}{{var middlename}} {{/depend}}{{var lastname}}{{depend suffix}} {{var suffix}}{{/depend}}<br/> {{depend company}}{{var company}}<br />{{/depend}} @@ -455,7 +565,8 @@ T: {{var telephone}} {{if city}}{{var city}}, {{/if}}{{if region}}{{var region}}, {{/if}}{{if postcode}}{{var postcode}}{{/if}}<br/> {{var country}}<br/> {{depend telephone}}T: {{var telephone}}{{/depend}} -{{depend fax}}<br/>F: {{var fax}}{{/depend}} +{{depend fax}}<br/>F: {{var fax}}{{/depend}} +{{depend vat_id}}<br/>VAT: {{var vat_id}}{{/depend}} {{depend prefix}}{{var prefix}} {{/depend}}{{var firstname}} {{depend middlename}}{{var middlename}} {{/depend}}{{var lastname}}{{depend suffix}} {{var suffix}}{{/depend}}| {{depend company}}{{var company}}|{{/depend}} {{if street1}}{{var street1}} @@ -467,8 +578,9 @@ T: {{var telephone}} {{if region}}{{var region}}, {{/if}}{{if postcode}}{{var postcode}}{{/if}}| {{var country}}| {{depend telephone}}T: {{var telephone}}{{/depend}}| -{{depend fax}}<br/>F: {{var fax}}{{/depend}}| - #{prefix} #{firstname} #{middlename} #{lastname} #{suffix}<br/>#{company}<br/>#{street0}<br/>#{street1}<br/>#{street2}<br/>#{street3}<br/>#{city}, #{region}, #{postcode}<br/>#{country_id}<br/>T: #{telephone}<br/>F: #{fax} +{{depend fax}}<br/>F: {{var fax}}{{/depend}}| +{{depend vat_id}}<br/>VAT: {{var vat_id}}{{/depend}}| + #{prefix} #{firstname} #{middlename} #{lastname} #{suffix}<br/>#{company}<br/>#{street0}<br/>#{street1}<br/>#{street2}<br/>#{street3}<br/>#{city}, #{region}, #{postcode}<br/>#{country_id}<br/>T: #{telephone}<br/>F: #{fax}<br/>VAT: #{vat_id} diff --git a/app/code/core/Mage/Customer/etc/system.xml b/app/code/core/Mage/Customer/etc/system.xml index 66746b7e46..65987d4176 100644 --- a/app/code/core/Mage/Customer/etc/system.xml +++ b/app/code/core/Mage/Customer/etc/system.xml @@ -88,19 +88,88 @@ 1 1 + + + select + adminhtml/system_config_source_yesno + 10 + 1 + 1 + 1 + select adminhtml/system_config_source_customer_group - 1 + 20 1 1 1 + + + select + adminhtml/system_config_source_customer_group + 30 + 1 + 1 + 1 + 1 + + + + select + adminhtml/system_config_source_customer_group + 40 + 1 + 1 + 1 + 1 + + + + select + adminhtml/system_config_source_customer_group + 50 + 1 + 1 + 1 + 1 + + + + select + adminhtml/system_config_source_customer_group + 55 + 1 + 1 + 1 + 1 + + + + select + adminhtml/system_config_source_yesno + 56 + 1 + 1 + 1 + 1 + + + + select + adminhtml/system_config_source_yesno + 57 + 1 + 0 + 0 + 1 + text - 2 + 60 1 1 1 @@ -109,7 +178,7 @@ select adminhtml/system_config_source_email_template - 3 + 70 1 1 1 @@ -118,7 +187,7 @@ select adminhtml/system_config_source_email_identity - 4 + 80 1 1 1 @@ -127,7 +196,7 @@ select adminhtml/system_config_source_yesno - 5 + 90 1 1 0 @@ -136,7 +205,7 @@ select adminhtml/system_config_source_email_template - 6 + 100 1 1 1 @@ -146,7 +215,7 @@ This email will be sent instead of default welcome email, after account confirmation. select adminhtml/system_config_source_email_template - 7 + 110 1 1 1 @@ -155,7 +224,7 @@ select adminhtml/system_config_source_yesno - 8 + 120 1 0 0 @@ -199,6 +268,7 @@ + This value must be greater than 0. text adminhtml/system_config_backend_customer_password_link_expirationperiod 40 @@ -376,5 +446,32 @@ + + + + + + adminhtml/system_config_backend_customer_groupAutoAssign + + + + + + + adminhtml/system_config_backend_customer_groupAutoAssign + + + + Validate VAT Number + adminhtml/customer_system_config_validatevat + 28 + 1 + 1 + 1 + + + + + diff --git a/app/code/core/Mage/Customer/sql/customer_setup/upgrade-1.6.1.0-1.6.2.0.php b/app/code/core/Mage/Customer/sql/customer_setup/upgrade-1.6.1.0-1.6.2.0.php new file mode 100644 index 0000000000..0c39c37e37 --- /dev/null +++ b/app/code/core/Mage/Customer/sql/customer_setup/upgrade-1.6.1.0-1.6.2.0.php @@ -0,0 +1,94 @@ +addAttribute('customer', $disableAGCAttributeCode, array( + 'type' => 'static', + 'label' => 'Disable automatic group change', + 'input' => 'boolean', + 'backend' => 'customer/attribute_backend_data_boolean', + 'position' => 28, + 'required' => false +)); + +$disableAGCAttribute = Mage::getSingleton('eav/config') + ->getAttribute('customer', $disableAGCAttributeCode); +$disableAGCAttribute->setData('used_in_forms', array( + 'adminhtml_customer' +)); +$disableAGCAttribute->save(); + + +$attributesInfo = array( + 'vat_id' => array( + 'label' => 'VAT number', + 'type' => 'varchar', + 'input' => 'text', + 'position' => 140, + 'visible' => true, + 'required' => false + ), + 'vat_is_valid' => array( + 'label' => 'VAT number validity', + 'visible' => false, + 'required' => false, + 'type' => 'int' + ), + 'vat_request_id' => array( + 'label' => 'VAT number validation request ID', + 'type' => 'varchar', + 'visible' => false, + 'required' => false + ), + 'vat_request_date' => array( + 'label' => 'VAT number validation request date', + 'type' => 'varchar', + 'visible' => false, + 'required' => false + ), + 'vat_request_success' => array( + 'label' => 'VAT number validation request success', + 'visible' => false, + 'required' => false, + 'type' => 'int' + ) +); + +foreach ($attributesInfo as $attributeCode => $attributeParams) { + $installer->addAttribute('customer_address', $attributeCode, $attributeParams); +} + +$vatAttribute = Mage::getSingleton('eav/config')->getAttribute('customer_address', 'vat_id'); +$vatAttribute->setData('used_in_forms', array( + 'adminhtml_customer_address', + 'customer_address_edit', + 'customer_register_address' +)); +$vatAttribute->save(); diff --git a/app/code/core/Mage/Customer/sql/customer_setup/upgrade-1.6.2.0-1.6.2.0.1.php b/app/code/core/Mage/Customer/sql/customer_setup/upgrade-1.6.2.0-1.6.2.0.1.php new file mode 100644 index 0000000000..0d655bf475 --- /dev/null +++ b/app/code/core/Mage/Customer/sql/customer_setup/upgrade-1.6.2.0-1.6.2.0.1.php @@ -0,0 +1,36 @@ +getConnection()->addColumn($installer->getTable('customer/entity'), 'disable_auto_group_change', array( + 'type' => Varien_Db_Ddl_Table::TYPE_SMALLINT, + 'unsigned' => true, + 'nullable' => false, + 'default' => '0', + 'comment' => 'Disable automatic group change' +)); diff --git a/app/code/core/Mage/Dataflow/Model/Convert/Adapter/Http/Curl.php b/app/code/core/Mage/Dataflow/Model/Convert/Adapter/Http/Curl.php index da6787c284..a9475f2efc 100644 --- a/app/code/core/Mage/Dataflow/Model/Convert/Adapter/Http/Curl.php +++ b/app/code/core/Mage/Dataflow/Model/Convert/Adapter/Http/Curl.php @@ -47,7 +47,7 @@ public function load() } // use Varien curl adapter - $http = new Varien_Http_Adapter_Curl; + $http = new Varien_Http_Adapter_Curl(); // send GET request $http->write('GET', $uri); @@ -55,6 +55,8 @@ public function load() // read the remote file $data = $http->read(); + $http->close(); + $data = preg_split('/^\r?$/m', $data, 2); $data = trim($data[1]); diff --git a/app/code/core/Mage/Dataflow/Model/Convert/Validator/Column.php b/app/code/core/Mage/Dataflow/Model/Convert/Validator/Column.php index 1078be75fc..8ae9b7aba7 100644 --- a/app/code/core/Mage/Dataflow/Model/Convert/Validator/Column.php +++ b/app/code/core/Mage/Dataflow/Model/Convert/Validator/Column.php @@ -34,5 +34,7 @@ */ class Mage_Dataflow_Model_Convert_Validator_Column extends Mage_Dataflow_Model_Convert_Validator_Abstract { - + public function validate() + { + } } diff --git a/app/code/core/Mage/Dataflow/Model/Mysql4/Batch/Abstract.php b/app/code/core/Mage/Dataflow/Model/Mysql4/Batch/Abstract.php index e2664a1031..9b26e509d5 100644 --- a/app/code/core/Mage/Dataflow/Model/Mysql4/Batch/Abstract.php +++ b/app/code/core/Mage/Dataflow/Model/Mysql4/Batch/Abstract.php @@ -32,6 +32,6 @@ * @package Mage_Dataflow * @author Magento Core Team */ -class Mage_Dataflow_Model_Mysql4_Batch_Abstract extends Mage_Dataflow_Model_Resource_Batch_Abstract +abstract class Mage_Dataflow_Model_Mysql4_Batch_Abstract extends Mage_Dataflow_Model_Resource_Batch_Abstract { } diff --git a/app/code/core/Mage/Dataflow/Model/Profile.php b/app/code/core/Mage/Dataflow/Model/Profile.php index 4a13a6d47a..e37b1cb894 100644 --- a/app/code/core/Mage/Dataflow/Model/Profile.php +++ b/app/code/core/Mage/Dataflow/Model/Profile.php @@ -80,7 +80,7 @@ protected function _beforeSave() $actionsXML = $this->getData('actions_xml'); if (strlen($actionsXML) < 0 && @simplexml_load_string('' . $actionsXML . '', null, LIBXML_NOERROR) === false) { - Mage::throwException(Mage::helper("dataflow")->__("Actions XML is not valid.")); + Mage::throwException(Mage::helper('dataflow')->__("Actions XML is not valid.")); } if (is_array($this->getGuiData())) { @@ -120,7 +120,7 @@ protected function _beforeSave() } if ($this->_getResource()->isProfileExists($this->getName(), $this->getId())) { - Mage::throwException(Mage::helper("dataflow")->__("Profile with the same name already exists.")); + Mage::throwException(Mage::helper('dataflow')->__("Profile with the same name already exists.")); } } diff --git a/app/code/core/Mage/Directory/sql/directory_setup/install-1.6.0.0.php b/app/code/core/Mage/Directory/sql/directory_setup/install-1.6.0.0.php index 8c0730a9c6..cc623d2772 100644 --- a/app/code/core/Mage/Directory/sql/directory_setup/install-1.6.0.0.php +++ b/app/code/core/Mage/Directory/sql/directory_setup/install-1.6.0.0.php @@ -40,12 +40,12 @@ 'default' => '', ), 'Country Id in ISO-2') ->addColumn('iso2_code', Varien_Db_Ddl_Table::TYPE_TEXT, 2, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Country ISO-2 format') ->addColumn('iso3_code', Varien_Db_Ddl_Table::TYPE_TEXT, 3, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Country ISO-3') ->setComment('Directory Country'); $installer->getConnection()->createTable($table); @@ -62,17 +62,22 @@ 'primary' => true, ), 'Country Format Id') ->addColumn('country_id', Varien_Db_Ddl_Table::TYPE_TEXT, 2, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Country Id in ISO-2') ->addColumn('type', Varien_Db_Ddl_Table::TYPE_TEXT, 30, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Country Format Type') ->addColumn('format', Varien_Db_Ddl_Table::TYPE_TEXT, '64k', array( 'nullable' => false, ), 'Country Format') - ->addIndex($installer->getIdxName('directory/country_format', array('country_id', 'type'), Varien_Db_Adapter_Interface::INDEX_TYPE_UNIQUE), + ->addIndex( + $installer->getIdxName( + 'directory/country_format', + array('country_id', 'type'), + Varien_Db_Adapter_Interface::INDEX_TYPE_UNIQUE + ), array('country_id', 'type'), array('type' => Varien_Db_Adapter_Interface::INDEX_TYPE_UNIQUE)) ->setComment('Directory Country Format'); $installer->getConnection()->createTable($table); @@ -93,8 +98,8 @@ 'default' => '0', ), 'Country Id in ISO-2') ->addColumn('code', Varien_Db_Ddl_Table::TYPE_TEXT, 32, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Region code') ->addColumn('default_name', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array( ), 'Region Name') @@ -120,12 +125,13 @@ 'default' => '0', ), 'Region Id') ->addColumn('name', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Region Name') ->addIndex($installer->getIdxName('directory/country_region_name', array('region_id')), array('region_id')) - ->addForeignKey($installer->getFkName('directory/country_region_name', 'region_id', 'directory/country_region', 'region_id'), + ->addForeignKey( + $installer->getFkName('directory/country_region_name', 'region_id', 'directory/country_region', 'region_id'), 'region_id', $installer->getTable('directory/country_region'), 'region_id', Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE) ->setComment('Directory Country Region Name'); diff --git a/app/code/core/Mage/Downloadable/Model/Observer.php b/app/code/core/Mage/Downloadable/Model/Observer.php index caa86f0e52..de7d35f396 100644 --- a/app/code/core/Mage/Downloadable/Model/Observer.php +++ b/app/code/core/Mage/Downloadable/Model/Observer.php @@ -215,6 +215,12 @@ public function setLinkStatus($observer) if ($item->getProductType() == Mage_Downloadable_Model_Product_Type::TYPE_DOWNLOADABLE || $item->getRealProductType() == Mage_Downloadable_Model_Product_Type::TYPE_DOWNLOADABLE ) { + if ($item->getStatusId() == Mage_Sales_Model_Order_Item::STATUS_BACKORDERED && + $orderItemStatusToEnable == Mage_Sales_Model_Order_Item::STATUS_PENDING && + !in_array(Mage_Sales_Model_Order_Item::STATUS_BACKORDERED, $availableStatuses, true) ) { + $availableStatuses[] = Mage_Sales_Model_Order_Item::STATUS_BACKORDERED; + } + if (in_array($item->getStatusId(), $availableStatuses)) { $downloadableItemsStatuses[$item->getId()] = $linkStatuses['avail']; } diff --git a/app/code/core/Mage/Downloadable/Model/Resource/Indexer/Price.php b/app/code/core/Mage/Downloadable/Model/Resource/Indexer/Price.php index 53cbc0ef08..a040fdaff8 100755 --- a/app/code/core/Mage/Downloadable/Model/Resource/Indexer/Price.php +++ b/app/code/core/Mage/Downloadable/Model/Resource/Indexer/Price.php @@ -42,11 +42,17 @@ class Mage_Downloadable_Model_Resource_Indexer_Price extends Mage_Catalog_Model_ public function reindexAll() { $this->useIdxTable(true); - $this->_prepareFinalPriceData(); - $this->_applyCustomOption(); - $this->_applyDownloadableLink(); - $this->_movePriceDataToIndexTable(); - + $this->beginTransaction(); + try { + $this->_prepareFinalPriceData(); + $this->_applyCustomOption(); + $this->_applyDownloadableLink(); + $this->_movePriceDataToIndexTable(); + $this->commit(); + } catch (Exception $e) { + $this->rollBack(); + throw $e; + } return $this; } @@ -140,6 +146,7 @@ protected function _applyDownloadableLink() $write->query($query); $ifTierPrice = $write->getCheckSql('i.tier_price IS NOT NULL', '(i.tier_price + id.min_price)', 'NULL'); + $ifGroupPrice = $write->getCheckSql('i.group_price IS NOT NULL', '(i.group_price + id.min_price)', 'NULL'); $select = $write->select() ->join( @@ -148,19 +155,16 @@ protected function _applyDownloadableLink() .' AND i.website_id = id.website_id', array()) ->columns(array( - 'min_price' => new Zend_Db_Expr('i.min_price + id.min_price'), - 'max_price' => new Zend_Db_Expr('i.max_price + id.max_price'), - 'tier_price' => new Zend_Db_Expr($ifTierPrice) + 'min_price' => new Zend_Db_Expr('i.min_price + id.min_price'), + 'max_price' => new Zend_Db_Expr('i.max_price + id.max_price'), + 'tier_price' => new Zend_Db_Expr($ifTierPrice), + 'group_price' => new Zend_Db_Expr($ifGroupPrice), )); $query = $select->crossUpdateFromSelect(array('i' => $this->_getDefaultFinalPriceTable())); $write->query($query); - if ($this->useIdxTable() && $this->_allowTableChanges) { - $write->truncateTable($table); - } else { - $write->delete($table); - } + $write->delete($table); return $this; } diff --git a/app/code/core/Mage/Downloadable/controllers/Adminhtml/Downloadable/FileController.php b/app/code/core/Mage/Downloadable/controllers/Adminhtml/Downloadable/FileController.php index 6e4f8bd96f..1d87939d8f 100644 --- a/app/code/core/Mage/Downloadable/controllers/Adminhtml/Downloadable/FileController.php +++ b/app/code/core/Mage/Downloadable/controllers/Adminhtml/Downloadable/FileController.php @@ -55,6 +55,12 @@ public function uploadAction() $uploader->setFilesDispersion(true); $result = $uploader->save($tmpPath); + /** + * Workaround for prototype 1.7 methods "isJSON", "evalJSON" on Windows OS + */ + $result['tmp_name'] = str_replace(DS, "/", $result['tmp_name']); + $result['path'] = str_replace(DS, "/", $result['path']); + if (isset($result['file'])) { $fullPath = rtrim($tmpPath, DS) . DS . ltrim($result['file'], DS); Mage::helper('core/file_storage_database')->saveFile($fullPath); diff --git a/app/code/core/Mage/Downloadable/controllers/DownloadController.php b/app/code/core/Mage/Downloadable/controllers/DownloadController.php index 0a5b905854..57fc8776e1 100644 --- a/app/code/core/Mage/Downloadable/controllers/DownloadController.php +++ b/app/code/core/Mage/Downloadable/controllers/DownloadController.php @@ -164,10 +164,7 @@ public function linkAction() if (!$customerId) { $product = Mage::getModel('catalog/product')->load($linkPurchasedItem->getProductId()); if ($product->getId()) { - $notice = Mage::helper('downloadable')->__( - 'Please log in to download your product or purchase %s.', - $product->getProductUrl(), $product->getName() - ); + $notice = Mage::helper('downloadable')->__('Please log in to download your product or purchase %s.', $product->getProductUrl(), $product->getName()); } else { $notice = Mage::helper('downloadable')->__('Please log in to download your product.'); } diff --git a/app/code/core/Mage/Downloadable/etc/config.xml b/app/code/core/Mage/Downloadable/etc/config.xml index 735b0a600b..22e7dc29f6 100644 --- a/app/code/core/Mage/Downloadable/etc/config.xml +++ b/app/code/core/Mage/Downloadable/etc/config.xml @@ -28,7 +28,7 @@ - 1.6.0.0.1 + 1.6.0.0.2 diff --git a/app/code/core/Mage/Downloadable/sql/downloadable_setup/mysql4-upgrade-1.6.0.0.1-1.6.0.0.2.php b/app/code/core/Mage/Downloadable/sql/downloadable_setup/mysql4-upgrade-1.6.0.0.1-1.6.0.0.2.php new file mode 100644 index 0000000000..a24f7f3ec3 --- /dev/null +++ b/app/code/core/Mage/Downloadable/sql/downloadable_setup/mysql4-upgrade-1.6.0.0.1-1.6.0.0.2.php @@ -0,0 +1,39 @@ +getConnection(); +$connection->changeTableEngine( + $installer->getTable('downloadable/product_price_indexer_tmp'), + Varien_Db_Adapter_Pdo_Mysql::ENGINE_MEMORY +); diff --git a/app/code/core/Mage/Downloadable/sql/downloadable_setup/upgrade-1.6.0.0.1-1.6.0.0.2.php b/app/code/core/Mage/Downloadable/sql/downloadable_setup/upgrade-1.6.0.0.1-1.6.0.0.2.php new file mode 100644 index 0000000000..5d7e8f5384 --- /dev/null +++ b/app/code/core/Mage/Downloadable/sql/downloadable_setup/upgrade-1.6.0.0.1-1.6.0.0.2.php @@ -0,0 +1,34 @@ +getAttribute(Mage_Catalog_Model_Product::ENTITY, 'group_price', 'apply_to')); +if (!in_array(Mage_Downloadable_Model_Product_Type::TYPE_DOWNLOADABLE, $applyTo)) { + $applyTo[] = Mage_Downloadable_Model_Product_Type::TYPE_DOWNLOADABLE; + $installer->updateAttribute(Mage_Catalog_Model_Product::ENTITY, 'group_price', 'apply_to', implode(',', $applyTo)); +} diff --git a/app/code/core/Mage/Eav/Block/Adminhtml/Attribute/Edit/Main/Abstract.php b/app/code/core/Mage/Eav/Block/Adminhtml/Attribute/Edit/Main/Abstract.php index b2b77455c6..c3dedc3c71 100644 --- a/app/code/core/Mage/Eav/Block/Adminhtml/Attribute/Edit/Main/Abstract.php +++ b/app/code/core/Mage/Eav/Block/Adminhtml/Attribute/Edit/Main/Abstract.php @@ -84,8 +84,7 @@ protected function _prepareForm() 'name' => 'attribute_code', 'label' => Mage::helper('eav')->__('Attribute Code'), 'title' => Mage::helper('eav')->__('Attribute Code'), - 'note' => Mage::helper('eav')->__('For internal use. Must be unique with no spaces. Maximum length of attribute code must be less then %s symbols', - Mage_Eav_Model_Entity_Attribute::ATTRIBUTE_CODE_MAX_LENGTH), + 'note' => Mage::helper('eav')->__('For internal use. Must be unique with no spaces. Maximum length of attribute code must be less then %s symbols', Mage_Eav_Model_Entity_Attribute::ATTRIBUTE_CODE_MAX_LENGTH), 'class' => $validateClass, 'required' => true, )); diff --git a/app/code/core/Mage/Eav/Model/Attribute/Data/Date.php b/app/code/core/Mage/Eav/Model/Attribute/Data/Date.php index 1041f410ea..309c4aca04 100644 --- a/app/code/core/Mage/Eav/Model/Attribute/Data/Date.php +++ b/app/code/core/Mage/Eav/Model/Attribute/Data/Date.php @@ -83,21 +83,11 @@ public function validateValue($value) || (!empty($validateRules['date_range_max']) && (strtotime($value) > $validateRules['date_range_max'])) ) { if (!empty($validateRules['date_range_min']) && !empty($validateRules['date_range_max'])) { - $errors[] = Mage::helper('customer')->__('Please enter a valid date between %s and %s at %s.', - date('d/m/Y', $validateRules['date_range_min']), - date('d/m/Y', $validateRules['date_range_max']), - $label - ); + $errors[] = Mage::helper('customer')->__('Please enter a valid date between %s and %s at %s.', date('d/m/Y', $validateRules['date_range_min']), date('d/m/Y', $validateRules['date_range_max']), $label); } elseif (!empty($validateRules['date_range_min'])) { - $errors[] = Mage::helper('customer')->__('Please enter a valid date equal to or greater than %s at %s.', - date('d/m/Y', $validateRules['date_range_min']), - $label - ); + $errors[] = Mage::helper('customer')->__('Please enter a valid date equal to or greater than %s at %s.', date('d/m/Y', $validateRules['date_range_min']), $label); } elseif (!empty($validateRules['date_range_max'])) { - $errors[] = Mage::helper('customer')->__('Please enter a valid date less than or equal to %s at %s.', - date('d/m/Y', $validateRules['date_range_max']), - $label - ); + $errors[] = Mage::helper('customer')->__('Please enter a valid date less than or equal to %s at %s.', date('d/m/Y', $validateRules['date_range_max']), $label); } } diff --git a/app/code/core/Mage/Eav/Model/Entity/Abstract.php b/app/code/core/Mage/Eav/Model/Entity/Abstract.php index 1a1d7babd8..267ca194f5 100644 --- a/app/code/core/Mage/Eav/Model/Entity/Abstract.php +++ b/app/code/core/Mage/Eav/Model/Entity/Abstract.php @@ -1091,7 +1091,7 @@ protected function _setAttributeValue($object, $valueRow) if ($attribute) { $attributeCode = $attribute->getAttributeCode(); $object->setData($attributeCode, $valueRow['value']); - $attribute->getBackend()->setValueId($valueRow['value_id']); + $attribute->getBackend()->setEntityValueId($object, $valueRow['value_id']); } return $this; @@ -1226,11 +1226,11 @@ protected function _collectSaveData($newObject) if ($this->_isAttributeValueEmpty($attribute, $v)) { $delete[$attribute->getBackend()->getTable()][] = array( 'attribute_id' => $attrId, - 'value_id' => $attribute->getBackend()->getValueId() + 'value_id' => $attribute->getBackend()->getEntityValueId($newObject) ); } elseif ($v !== $origData[$k]) { $update[$attrId] = array( - 'value_id' => $attribute->getBackend()->getValueId(), + 'value_id' => $attribute->getBackend()->getEntityValueId($newObject), 'value' => $v, ); } diff --git a/app/code/core/Mage/Eav/Model/Entity/Attribute/Backend/Abstract.php b/app/code/core/Mage/Eav/Model/Entity/Attribute/Backend/Abstract.php index ffd89913a8..162c036c5c 100644 --- a/app/code/core/Mage/Eav/Model/Entity/Attribute/Backend/Abstract.php +++ b/app/code/core/Mage/Eav/Model/Entity/Attribute/Backend/Abstract.php @@ -32,7 +32,8 @@ * @package Mage_Eav * @author Magento Core Team */ -abstract class Mage_Eav_Model_Entity_Attribute_Backend_Abstract implements Mage_Eav_Model_Entity_Attribute_Backend_Interface +abstract class Mage_Eav_Model_Entity_Attribute_Backend_Abstract + implements Mage_Eav_Model_Entity_Attribute_Backend_Interface { /** * Reference to the attribute instance @@ -48,6 +49,13 @@ abstract class Mage_Eav_Model_Entity_Attribute_Backend_Abstract implements Mage_ */ protected $_valueId; + /** + * PK value_ids for each loaded entity + * + * @var array + */ + protected $_valueIds = array(); + /** * Table name for this attribute * @@ -164,7 +172,24 @@ public function setValueId($valueId) } /** - * Retreive value id + * Set entity value id + * + * @param Varien_Object $entity + * @param int $valueId + * @return Mage_Eav_Model_Entity_Attribute_Backend_Abstract + */ + public function setEntityValueId($entity, $valueId) + { + if (!$entity || !$entity->getId()) { + return $this->setValueId($valueId); + } + + $this->_valueIds[$entity->getId()] = $valueId; + return $this; + } + + /** + * Retrieve value id * * @return int */ @@ -174,7 +199,22 @@ public function getValueId() } /** - * Retreive default value + * Get entity value id + * + * @param Varien_Object $entity + * @return int + */ + public function getEntityValueId($entity) + { + if (!$entity || !$entity->getId() || !array_key_exists($entity->getId(), $this->_valueIds)) { + return $this->getValueId(); + } + + return $this->_valueIds[$entity->getId()]; + } + + /** + * Retrieve default value * * @return mixed */ diff --git a/app/code/core/Mage/Eav/Model/Entity/Attribute/Backend/Interface.php b/app/code/core/Mage/Eav/Model/Entity/Attribute/Backend/Interface.php index 32f2863c18..d0a063dfb6 100644 --- a/app/code/core/Mage/Eav/Model/Entity/Attribute/Backend/Interface.php +++ b/app/code/core/Mage/Eav/Model/Entity/Attribute/Backend/Interface.php @@ -45,4 +45,19 @@ public function beforeSave($object); public function afterSave($object); public function beforeDelete($object); public function afterDelete($object); + + /** + * Get entity value id + * + * @param Varien_Object $entity + */ + public function getEntityValueId($entity); + + /** + * Set entity value id + * + * @param Varien_Object $entity + * @param int $valueId + */ + public function setEntityValueId($entity, $valueId); } diff --git a/app/code/core/Mage/Eav/Model/Entity/Setup.php b/app/code/core/Mage/Eav/Model/Entity/Setup.php index 7740fe0a1b..cae1249026 100644 --- a/app/code/core/Mage/Eav/Model/Entity/Setup.php +++ b/app/code/core/Mage/Eav/Model/Entity/Setup.php @@ -1296,8 +1296,8 @@ public function createEntityTables($baseTableName, array $options = array()) foreach ($types as $type => $fieldType) { $eavTableName = array($baseTableName, $type); - $eavTable = $connection - ->newTable($this->getTable($eavTableName)) + $eavTable = $connection->newTable($this->getTable($eavTableName)); + $eavTable ->addColumn('value_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array( 'identity' => true, 'nullable' => false, @@ -1333,13 +1333,17 @@ public function createEntityTables($baseTableName, array $options = array()) ->addIndex($this->getIdxName($eavTableName, array('store_id')), array('store_id')) ->addIndex($this->getIdxName($eavTableName, array('entity_id')), - array('entity_id')) - ->addIndex($this->getIdxName($eavTableName, array('attribute_id', 'value')), - array('attribute_id', 'value')) - ->addIndex($this->getIdxName($eavTableName, array('entity_type_id', 'value')), - array('entity_type_id', 'value')) - ->addForeignKey($this->getFkName($eavTableName, 'entity_id', 'eav/entity', 'entity_id'), - 'entity_id', $this->getTable('eav/entity'), 'entity_id', + array('entity_id')); + if ($type !== 'text') { + $eavTable->addIndex($this->getIdxName($eavTableName, array('attribute_id', 'value')), + array('attribute_id', 'value')); + $eavTable->addIndex($this->getIdxName($eavTableName, array('entity_type_id', 'value')), + array('entity_type_id', 'value')); + } + + $eavTable + ->addForeignKey($this->getFkName($eavTableName, 'entity_id', $baseTableName, 'entity_id'), + 'entity_id', $this->getTable($baseTableName), 'entity_id', Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE) ->addForeignKey($this->getFkName($eavTableName, 'entity_type_id', 'eav/entity_type', 'entity_type_id'), 'entity_type_id', $this->getTable('eav/entity_type'), 'entity_type_id', diff --git a/app/code/core/Mage/Eav/sql/eav_setup/install-1.6.0.0.php b/app/code/core/Mage/Eav/sql/eav_setup/install-1.6.0.0.php index 92965fb496..d31768d937 100644 --- a/app/code/core/Mage/Eav/sql/eav_setup/install-1.6.0.0.php +++ b/app/code/core/Mage/Eav/sql/eav_setup/install-1.6.0.0.php @@ -92,7 +92,7 @@ ), 'Additional Attribute Table') ->addColumn('entity_attribute_collection', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array( 'nullable' => true, - 'default' => '', + 'default' => null, ), 'Entity Attribute Collection') ->addIndex($installer->getIdxName('eav/entity_type', array('entity_type_code')), array('entity_type_code')) @@ -121,8 +121,8 @@ 'default' => '0', ), 'Attribute Set Id') ->addColumn('increment_id', Varien_Db_Ddl_Table::TYPE_TEXT, 50, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Increment Id') ->addColumn('parent_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array( 'unsigned' => true, @@ -520,8 +520,8 @@ 'default' => '0', ), 'Entity Id') ->addColumn('value', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Attribute Value') ->addIndex($installer->getIdxName(array('eav/entity_value_prefix', 'varchar'), array('entity_type_id')), array('entity_type_id')) @@ -589,8 +589,8 @@ 'default' => '0', ), 'Entity Type Id') ->addColumn('attribute_code', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Attribute Code') ->addColumn('attribute_model', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array( ), 'Attribute Model') @@ -706,8 +706,8 @@ 'default' => '0', ), 'Entity Type Id') ->addColumn('attribute_set_name', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Attribute Set Name') ->addColumn('sort_order', Varien_Db_Ddl_Table::TYPE_SMALLINT, null, array( 'nullable' => false, @@ -746,8 +746,8 @@ 'default' => '0', ), 'Attribute Set Id') ->addColumn('attribute_group_name', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Attribute Group Name') ->addColumn('sort_order', Varien_Db_Ddl_Table::TYPE_SMALLINT, null, array( 'nullable' => false, @@ -900,8 +900,8 @@ 'default' => '0', ), 'Store Id') ->addColumn('value', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Value') ->addIndex($installer->getIdxName('eav/attribute_option_value', array('option_id')), array('option_id')) @@ -940,8 +940,8 @@ 'default' => '0', ), 'Store Id') ->addColumn('value', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Value') ->addIndex($installer->getIdxName('eav/attribute_label', array('attribute_id')), array('attribute_id')) diff --git a/app/code/core/Mage/GoogleAnalytics/Block/Ga.php b/app/code/core/Mage/GoogleAnalytics/Block/Ga.php index e6a7cb41e0..beaf7a538b 100644 --- a/app/code/core/Mage/GoogleAnalytics/Block/Ga.php +++ b/app/code/core/Mage/GoogleAnalytics/Block/Ga.php @@ -32,7 +32,7 @@ * @package Mage_GoogleAnalytics * @author Magento Core Team */ -class Mage_GoogleAnalytics_Block_Ga extends Mage_Core_Block_Text +class Mage_GoogleAnalytics_Block_Ga extends Mage_Core_Block_Template { /** * @deprecated after 1.4.1.1 @@ -119,11 +119,14 @@ protected function _getOrdersTrackingCode() $address = $order->getShippingAddress(); } $result[] = sprintf("_gaq.push(['_addTrans', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']);", - $order->getIncrementId(), Mage::app()->getStore()->getFrontendName(), $order->getBaseGrandTotal(), - $order->getBaseTaxAmount(), $order->getBaseShippingAmount(), - $this->jsQuoteEscape($address->getCity()), - $this->jsQuoteEscape($address->getRegion()), - $this->jsQuoteEscape($address->getCountry()) + $order->getIncrementId(), + $this->jsQuoteEscape(Mage::app()->getStore()->getFrontendName()), + $order->getBaseGrandTotal(), + $order->getBaseTaxAmount(), + $order->getBaseShippingAmount(), + $this->jsQuoteEscape(Mage::helper('core')->escapeHtml($address->getCity())), + $this->jsQuoteEscape(Mage::helper('core')->escapeHtml($address->getRegion())), + $this->jsQuoteEscape(Mage::helper('core')->escapeHtml($address->getCountry())) ); foreach ($order->getAllVisibleItems() as $item) { $result[] = sprintf("_gaq.push(['_addItem', '%s', '%s', '%s', '%s', '%s', '%s']);", @@ -148,22 +151,7 @@ protected function _toHtml() if (!Mage::helper('googleanalytics')->isGoogleAnalyticsAvailable()) { return ''; } - $accountId = Mage::getStoreConfig(Mage_GoogleAnalytics_Helper_Data::XML_PATH_ACCOUNT); - return ' - - -'; + return parent::_toHtml(); } } diff --git a/app/code/core/Mage/GoogleBase/Model/Observer.php b/app/code/core/Mage/GoogleBase/Model/Observer.php index 9949c7b426..2b36d27931 100644 --- a/app/code/core/Mage/GoogleBase/Model/Observer.php +++ b/app/code/core/Mage/GoogleBase/Model/Observer.php @@ -58,8 +58,7 @@ public function saveProductItem($observer) } catch (Exception $e) { if (Mage::app()->getStore()->isAdmin()) { Mage::getSingleton('adminhtml/session')->addNotice( - Mage::helper('googlebase')->__("Cannot update Google Base Item for Store '%s'", - Mage::app()->getStore($item->getStoreId())->getName()) + Mage::helper('googlebase')->__("Cannot update Google Base Item for Store '%s'", Mage::app()->getStore($item->getStoreId())->getName()) ); } else { throw $e; @@ -89,8 +88,7 @@ public function deleteProductItem($observer) } catch (Exception $e) { if (Mage::app()->getStore()->isAdmin()) { Mage::getSingleton('adminhtml/session')->addNotice( - Mage::helper('googlebase')->__("Cannot update Google Base Item for Store '%s'", - Mage::app()->getStore($item->getStoreId())->getName()) + Mage::helper('googlebase')->__("Cannot update Google Base Item for Store '%s'", Mage::app()->getStore($item->getStoreId())->getName()) ); } else { throw $e; diff --git a/app/code/core/Mage/GoogleCheckout/Model/Api/Xml/Abstract.php b/app/code/core/Mage/GoogleCheckout/Model/Api/Xml/Abstract.php index 52c89bc4b8..c4ae8a80df 100644 --- a/app/code/core/Mage/GoogleCheckout/Model/Api/Xml/Abstract.php +++ b/app/code/core/Mage/GoogleCheckout/Model/Api/Xml/Abstract.php @@ -66,7 +66,10 @@ public function getMerchantKey() public function getServerType() { if (!$this->hasData('server_type')) { - $this->setData('server_type', Mage::getStoreConfig('google/checkout/sandbox', $this->getStoreId()) ? "sandbox" : ""); + $this->setData( + 'server_type', + Mage::getStoreConfig('google/checkout/sandbox', $this->getStoreId()) ? "sandbox" : "" + ); } return $this->getData('server_type'); } @@ -173,6 +176,7 @@ public function _call($xml) $response = preg_split('/^\r?$/m', $response, 2); $response = trim($response[1]); $debugData['result'] = $response; + $http->close(); } catch (Exception $e) { $debugData['result'] = array('error' => $e->getMessage(), 'code' => $e->getCode()); @@ -183,7 +187,9 @@ public function _call($xml) $this->getApi()->debugData($debugData); $result = @simplexml_load_string($response); if (!$result) { - $result = simplexml_load_string('Invalid response from Google Checkout server'); + $result = simplexml_load_string( + 'Invalid response from Google Checkout server' + ); } if ($result->getName() == 'error') { $this->setError($this->__('Google Checkout: %s', (string)$result->{'error-message'})); @@ -199,7 +205,10 @@ public function _call($xml) protected function _getCallbackUrl() { - return Mage::getUrl('googlecheckout/api', array('_forced_secure'=>Mage::getStoreConfig('google/checkout/use_secure_callback_url', $this->getStoreId()))); + return Mage::getUrl( + 'googlecheckout/api', + array('_forced_secure'=>Mage::getStoreConfig('google/checkout/use_secure_callback_url',$this->getStoreId())) + ); } /** diff --git a/app/code/core/Mage/GoogleCheckout/Model/Api/Xml/Checkout.php b/app/code/core/Mage/GoogleCheckout/Model/Api/Xml/Checkout.php index 97b0ad3b88..3aa11ff685 100644 --- a/app/code/core/Mage/GoogleCheckout/Model/Api/Xml/Checkout.php +++ b/app/code/core/Mage/GoogleCheckout/Model/Api/Xml/Checkout.php @@ -896,10 +896,7 @@ protected function _getCustomerTaxClass() { $customerGroup = $this->getQuote()->getCustomerGroupId(); if (!$customerGroup) { - $customerGroup = Mage::getStoreConfig( - Mage_Customer_Model_Group::XML_PATH_DEFAULT_ID, - $this->getQuote()->getStoreId() - ); + $customerGroup = Mage::helper('customer')->getDefaultCustomerGroupId($this->getQuote()->getStoreId()); } return Mage::getModel('customer/group')->load($customerGroup)->getTaxClassId(); } diff --git a/app/code/core/Mage/GoogleCheckout/Model/Payment.php b/app/code/core/Mage/GoogleCheckout/Model/Payment.php index 6028996379..c944b0c847 100644 --- a/app/code/core/Mage/GoogleCheckout/Model/Payment.php +++ b/app/code/core/Mage/GoogleCheckout/Model/Payment.php @@ -59,7 +59,7 @@ public function canEdit() /** * Return Order Place Redirect URL * - * @return string Order Redirect URL + * @return string Order Redirect URL */ public function getOrderPlaceRedirectUrl() { @@ -114,21 +114,15 @@ public function capture(Varien_Object $payment, $amount) /** * Refund money * - * @param Varien_Object $invoicePayment + * @param Varien_Object $payment + * @param float $amount + * * @return Mage_GoogleCheckout_Model_Payment */ - //public function refund(Varien_Object $payment, $amount) public function refund(Varien_Object $payment, $amount) { - $hlp = Mage::helper('googlecheckout'); - -// foreach ($payment->getCreditMemo()->getCommentsCollection() as $comment) { -// $this->setReason($hlp->__('See Comments')); -// $this->setComment($comment->getComment()); -// } - - $reason = $this->getReason() ? $this->getReason() : $hlp->__('No Reason'); - $comment = $this->getComment() ? $this->getComment() : $hlp->__('No Comment'); + $reason = $this->getReason() ? $this->getReason() : Mage::helper('googlecheckout')->__('No Reason'); + $comment = $this->getComment() ? $this->getComment() : Mage::helper('googlecheckout')->__('No Comment'); $api = Mage::getModel('googlecheckout/api')->setStoreId($payment->getOrder()->getStoreId()); $api->refund($payment->getOrder()->getExtOrderId(), $amount, $reason, $comment); @@ -146,15 +140,15 @@ public function void(Varien_Object $payment) /** * Void payment * - * @param Varien_Object $invoicePayment - * @return Mage_GoogleCheckout_Model_Payment + * @param Varien_Object $payment + * + * @return Mage_GoogleCheckout_Model_Payment */ public function cancel(Varien_Object $payment) { if (!$payment->getOrder()->getBeingCanceledFromGoogleApi()) { - $hlp = Mage::helper('googlecheckout'); - $reason = $this->getReason() ? $this->getReason() : $hlp->__('Unknown Reason'); - $comment = $this->getComment() ? $this->getComment() : $hlp->__('No Comment'); + $reason = $this->getReason() ? $this->getReason() : Mage::helper('googlecheckout')->__('Unknown Reason'); + $comment = $this->getComment() ? $this->getComment() : Mage::helper('googlecheckout')->__('No Comment'); $api = Mage::getModel('googlecheckout/api')->setStoreId($payment->getOrder()->getStoreId()); $api->cancel($payment->getOrder()->getExtOrderId(), $reason, $comment); @@ -164,10 +158,11 @@ public function cancel(Varien_Object $payment) } /** - * Retrieve information from payment configuration. - * Rewrited because of custom node for checkout settings + * Retrieve information from payment configuration + * + * @param string $field + * @param int|string|null|Mage_Core_Model_Store $storeId * - * @param string $field * @return mixed */ public function getConfigData($field, $storeId = null) @@ -176,6 +171,7 @@ public function getConfigData($field, $storeId = null) $storeId = $this->getStore(); } $path = 'google/checkout/' . $field; + return Mage::getStoreConfig($path, $storeId); } diff --git a/app/code/core/Mage/GoogleCheckout/Model/Source/Shipping/Carrier.php b/app/code/core/Mage/GoogleCheckout/Model/Source/Shipping/Carrier.php index 66ea14961a..e20b8bd2e8 100644 --- a/app/code/core/Mage/GoogleCheckout/Model/Source/Shipping/Carrier.php +++ b/app/code/core/Mage/GoogleCheckout/Model/Source/Shipping/Carrier.php @@ -29,31 +29,30 @@ class Mage_GoogleCheckout_Model_Source_Shipping_Carrier { public function toOptionArray() { - $hlp = Mage::helper('googlecheckout'); return array( - array('label' => $hlp->__('FedEx'), 'value' => array( - array('label' => $hlp->__('Ground'), 'value' => 'FedEx/Ground'), - array('label' => $hlp->__('Home Delivery'), 'value' => 'FedEx/Home Delivery'), - array('label' => $hlp->__('Express Saver'), 'value' => 'FedEx/Express Saver'), - array('label' => $hlp->__('First Overnight'), 'value' => 'FedEx/First Overnight'), - array('label' => $hlp->__('Priority Overnight'), 'value' => 'FedEx/Priority Overnight'), - array('label' => $hlp->__('Standard Overnight'), 'value' => 'FedEx/Standard Overnight'), - array('label' => $hlp->__('2Day'), 'value' => 'FedEx/2Day'), + array('label' => Mage::helper('googlecheckout')->__('FedEx'), 'value' => array( + array('label' => Mage::helper('googlecheckout')->__('Ground'), 'value' => 'FedEx/Ground'), + array('label' => Mage::helper('googlecheckout')->__('Home Delivery'), 'value' => 'FedEx/Home Delivery'), + array('label' => Mage::helper('googlecheckout')->__('Express Saver'), 'value' => 'FedEx/Express Saver'), + array('label' => Mage::helper('googlecheckout')->__('First Overnight'), 'value' => 'FedEx/First Overnight'), + array('label' => Mage::helper('googlecheckout')->__('Priority Overnight'), 'value' => 'FedEx/Priority Overnight'), + array('label' => Mage::helper('googlecheckout')->__('Standard Overnight'), 'value' => 'FedEx/Standard Overnight'), + array('label' => Mage::helper('googlecheckout')->__('2Day'), 'value' => 'FedEx/2Day'), )), - array('label' => $hlp->__('UPS'), 'value' => array( - array('label' => $hlp->__('Next Day Air'), 'value' => 'UPS/Next Day Air'), - array('label' => $hlp->__('Next Day Air Early AM'), 'value' => 'UPS/Next Day Air Early AM'), - array('label' => $hlp->__('Next Day Air Saver'), 'value' => 'UPS/Next Day Air Saver'), - array('label' => $hlp->__('2nd Day Air'), 'value' => 'UPS/2nd Day Air'), - array('label' => $hlp->__('2nd Day Air AM'), 'value' => 'UPS/2nd Day Air AM'), - array('label' => $hlp->__('3 Day Select'), 'value' => 'UPS/3 Day Select'), - array('label' => $hlp->__('Ground'), 'value' => 'UPS/Ground'), + array('label' => Mage::helper('googlecheckout')->__('UPS'), 'value' => array( + array('label' => Mage::helper('googlecheckout')->__('Next Day Air'), 'value' => 'UPS/Next Day Air'), + array('label' => Mage::helper('googlecheckout')->__('Next Day Air Early AM'), 'value' => 'UPS/Next Day Air Early AM'), + array('label' => Mage::helper('googlecheckout')->__('Next Day Air Saver'), 'value' => 'UPS/Next Day Air Saver'), + array('label' => Mage::helper('googlecheckout')->__('2nd Day Air'), 'value' => 'UPS/2nd Day Air'), + array('label' => Mage::helper('googlecheckout')->__('2nd Day Air AM'), 'value' => 'UPS/2nd Day Air AM'), + array('label' => Mage::helper('googlecheckout')->__('3 Day Select'), 'value' => 'UPS/3 Day Select'), + array('label' => Mage::helper('googlecheckout')->__('Ground'), 'value' => 'UPS/Ground'), )), - array('label' => $hlp->__('USPS'), 'value' => array( - array('label' => $hlp->__('Express Mail'), 'value' => 'USPS/Express Mail'), - array('label' => $hlp->__('Priority Mail'), 'value' => 'USPS/Priority Mail'), - array('label' => $hlp->__('Parcel Post'), 'value' => 'USPS/Parcel Post'), - array('label' => $hlp->__('Media Mail'), 'value' => 'USPS/Media Mail'), + array('label' => Mage::helper('googlecheckout')->__('USPS'), 'value' => array( + array('label' => Mage::helper('googlecheckout')->__('Express Mail'), 'value' => 'USPS/Express Mail'), + array('label' => Mage::helper('googlecheckout')->__('Priority Mail'), 'value' => 'USPS/Priority Mail'), + array('label' => Mage::helper('googlecheckout')->__('Parcel Post'), 'value' => 'USPS/Parcel Post'), + array('label' => Mage::helper('googlecheckout')->__('Media Mail'), 'value' => 'USPS/Media Mail'), )), ); } diff --git a/app/code/core/Mage/GoogleCheckout/Model/Source/Shipping/Category.php b/app/code/core/Mage/GoogleCheckout/Model/Source/Shipping/Category.php index 7c9028a2fd..390224cc43 100644 --- a/app/code/core/Mage/GoogleCheckout/Model/Source/Shipping/Category.php +++ b/app/code/core/Mage/GoogleCheckout/Model/Source/Shipping/Category.php @@ -29,10 +29,9 @@ class Mage_GoogleCheckout_Model_Source_Shipping_Category { public function toOptionArray() { - $hlp = Mage::helper('googlecheckout'); return array( - array('value' => 'COMMERCIAL', 'label' => $hlp->__('Commercial')), - array('value' => 'RESIDENTIAL', 'label' => $hlp->__('Residential')), + array('value' => 'COMMERCIAL', 'label' => Mage::helper('googlecheckout')->__('Commercial')), + array('value' => 'RESIDENTIAL', 'label' => Mage::helper('googlecheckout')->__('Residential')), ); } } diff --git a/app/code/core/Mage/GoogleCheckout/Model/Source/Shipping/Units.php b/app/code/core/Mage/GoogleCheckout/Model/Source/Shipping/Units.php index 88720402a7..bc34bc101a 100644 --- a/app/code/core/Mage/GoogleCheckout/Model/Source/Shipping/Units.php +++ b/app/code/core/Mage/GoogleCheckout/Model/Source/Shipping/Units.php @@ -29,9 +29,8 @@ class Mage_GoogleCheckout_Model_Source_Shipping_Units { public function toOptionArray() { - $hlp = Mage::helper('googlecheckout'); return array( - array('value' => 'IN', 'label' => $hlp->__('Inches')), + array('value' => 'IN', 'label' => Mage::helper('googlecheckout')->__('Inches')), ); } } diff --git a/app/code/core/Mage/GoogleCheckout/Model/Source/Shipping/Virtual/Method.php b/app/code/core/Mage/GoogleCheckout/Model/Source/Shipping/Virtual/Method.php index 2408a46531..faf863a427 100644 --- a/app/code/core/Mage/GoogleCheckout/Model/Source/Shipping/Virtual/Method.php +++ b/app/code/core/Mage/GoogleCheckout/Model/Source/Shipping/Virtual/Method.php @@ -29,11 +29,10 @@ class Mage_GoogleCheckout_Model_Source_Shipping_Virtual_Method { public function toOptionArray() { - $hlp = Mage::helper('googlecheckout'); return array( - array('value' => 'email', 'label' => $hlp->__('Email delivery')), - // array('value'=>'key_url', 'label'=>$hlp->__('Key/URL delivery')), - // array('value'=>'description_based', 'label'=>$hlp->__('Description-based delivery')), + array('value' => 'email', 'label' => Mage::helper('googlecheckout')->__('Email delivery')), + // array('value'=>'key_url', 'label'=> Mage::helper('googlecheckout')->__('Key/URL delivery')), + // array('value'=>'description_based', 'label'=> Mage::helper('googlecheckout')->__('Description-based delivery')) ); } } diff --git a/app/code/core/Mage/GoogleCheckout/Model/Source/Shipping/Virtual/Schedule.php b/app/code/core/Mage/GoogleCheckout/Model/Source/Shipping/Virtual/Schedule.php index 8a2651625b..391b1ae810 100644 --- a/app/code/core/Mage/GoogleCheckout/Model/Source/Shipping/Virtual/Schedule.php +++ b/app/code/core/Mage/GoogleCheckout/Model/Source/Shipping/Virtual/Schedule.php @@ -29,10 +29,9 @@ class Mage_GoogleCheckout_Model_Source_Shipping_Virtual_Schedule { public function toOptionArray() { - $hlp = Mage::helper('googlecheckout'); return array( - array('value' => 'OPTIMISTIC', 'label' => $hlp->__('Optimistic')), - array('value' => 'PESSIMISTIC', 'label' => $hlp->__('Pessimistic')), + array('value' => 'OPTIMISTIC', 'label' => Mage::helper('googlecheckout')->__('Optimistic')), + array('value' => 'PESSIMISTIC', 'label' => Mage::helper('googlecheckout')->__('Pessimistic')), ); } } diff --git a/app/code/core/Mage/ImportExport/Model/Export/Entity/Abstract.php b/app/code/core/Mage/ImportExport/Model/Export/Entity/Abstract.php index a94c955db2..68ea2566ba 100644 --- a/app/code/core/Mage/ImportExport/Model/Export/Entity/Abstract.php +++ b/app/code/core/Mage/ImportExport/Model/Export/Entity/Abstract.php @@ -256,11 +256,13 @@ protected function _prepareEntityCollection(Mage_Eav_Model_Entity_Collection_Abs $from = array_shift($exportFilter[$attrCode]); $to = array_shift($exportFilter[$attrCode]); - if (is_scalar($from) && strtotime($from)) { - $collection->addAttributeToFilter($attrCode, array('from' => $from, 'date' => true)); + if (is_scalar($from) && !empty($from)) { + $date = Mage::app()->getLocale()->date($from,null,null,false)->toString('MM/dd/YYYY'); + $collection->addAttributeToFilter($attrCode, array('from' => $date, 'date' => true)); } - if (is_scalar($to) && strtotime($to)) { - $collection->addAttributeToFilter($attrCode, array('to' => $to, 'date' => true)); + if (is_scalar($to) && !empty($to)) { + $date = Mage::app()->getLocale()->date($to,null,null,false)->toString('MM/dd/YYYY'); + $collection->addAttributeToFilter($attrCode, array('to' => $date, 'date' => true)); } } } elseif (Mage_ImportExport_Model_Export::FILTER_TYPE_NUMBER == $attrFilterType) { diff --git a/app/code/core/Mage/ImportExport/Model/Import.php b/app/code/core/Mage/ImportExport/Model/Import.php index b6d103b11d..b253f82a9f 100644 --- a/app/code/core/Mage/ImportExport/Model/Import.php +++ b/app/code/core/Mage/ImportExport/Model/Import.php @@ -74,7 +74,8 @@ class Mage_ImportExport_Model_Import extends Mage_ImportExport_Model_Abstract 'catalog_product' => array ( 'catalog_product_price', 'catalog_category_product', - 'catalogsearch_fulltext' + 'catalogsearch_fulltext', + 'catalog_product_flat', ) ); diff --git a/app/code/core/Mage/ImportExport/Model/Import/Entity/Abstract.php b/app/code/core/Mage/ImportExport/Model/Import/Entity/Abstract.php index 50b37d1cd5..3dbc74271b 100644 --- a/app/code/core/Mage/ImportExport/Model/Import/Entity/Abstract.php +++ b/app/code/core/Mage/ImportExport/Model/Import/Entity/Abstract.php @@ -560,8 +560,8 @@ public function isAttributeValid($attrCode, array $attrParams, array $rowData, $ break; case 'datetime': $val = trim($rowData[$attrCode]); - $valid = strtotime($val) - || preg_match('/^\d{2}.\d{2}.\d{2,4}(?:\s+\d{1,2}.\d{1,2}(?:.\d{1,2})?)?$/', $val); + $valid = strtotime($val) !== false + || preg_match('/^\d{2}.\d{2}.\d{2,4}(?:\s+\d{1,2}.\d{1,2}(?:.\d{1,2})?)?$/', $val); break; case 'text': $val = Mage::helper('core/string')->cleanString($rowData[$attrCode]); diff --git a/app/code/core/Mage/ImportExport/Model/Import/Entity/Product.php b/app/code/core/Mage/ImportExport/Model/Import/Entity/Product.php index bb9e3ecc17..70c9390cb6 100644 --- a/app/code/core/Mage/ImportExport/Model/Import/Entity/Product.php +++ b/app/code/core/Mage/ImportExport/Model/Import/Entity/Product.php @@ -68,27 +68,28 @@ class Mage_ImportExport_Model_Import_Entity_Product extends Mage_ImportExport_Mo /** * Error codes. */ - const ERROR_INVALID_SCOPE = 'invalidScope'; - const ERROR_INVALID_WEBSITE = 'invalidWebsite'; - const ERROR_INVALID_STORE = 'invalidStore'; - const ERROR_INVALID_ATTR_SET = 'invalidAttrSet'; - const ERROR_INVALID_TYPE = 'invalidType'; - const ERROR_INVALID_CATEGORY = 'invalidCategory'; - const ERROR_VALUE_IS_REQUIRED = 'isRequired'; - const ERROR_TYPE_CHANGED = 'typeChanged'; - const ERROR_SKU_IS_EMPTY = 'skuEmpty'; - const ERROR_NO_DEFAULT_ROW = 'noDefaultRow'; - const ERROR_CHANGE_TYPE = 'changeProductType'; - const ERROR_DUPLICATE_SCOPE = 'duplicateScope'; - const ERROR_DUPLICATE_SKU = 'duplicateSKU'; - const ERROR_CHANGE_ATTR_SET = 'changeAttrSet'; - const ERROR_TYPE_UNSUPPORTED = 'productTypeUnsupported'; - const ERROR_ROW_IS_ORPHAN = 'rowIsOrphan'; - const ERROR_INVALID_TIER_PRICE_QTY = 'invalidTierPriceOrQty'; - const ERROR_INVALID_TIER_PRICE_SITE = 'tierPriceWebsiteInvalid'; - const ERROR_INVALID_TIER_PRICE_GROUP = 'tierPriceGroupInvalid'; - const ERROR_TIER_DATA_INCOMPLETE = 'tierPriceDataIsIncomplete'; - const ERROR_SKU_NOT_FOUND_FOR_DELETE = 'skuNotFoundToDelete'; + const ERROR_INVALID_SCOPE = 'invalidScope'; + const ERROR_INVALID_WEBSITE = 'invalidWebsite'; + const ERROR_INVALID_STORE = 'invalidStore'; + const ERROR_INVALID_ATTR_SET = 'invalidAttrSet'; + const ERROR_INVALID_TYPE = 'invalidType'; + const ERROR_INVALID_CATEGORY = 'invalidCategory'; + const ERROR_VALUE_IS_REQUIRED = 'isRequired'; + const ERROR_TYPE_CHANGED = 'typeChanged'; + const ERROR_SKU_IS_EMPTY = 'skuEmpty'; + const ERROR_NO_DEFAULT_ROW = 'noDefaultRow'; + const ERROR_CHANGE_TYPE = 'changeProductType'; + const ERROR_DUPLICATE_SCOPE = 'duplicateScope'; + const ERROR_DUPLICATE_SKU = 'duplicateSKU'; + const ERROR_CHANGE_ATTR_SET = 'changeAttrSet'; + const ERROR_TYPE_UNSUPPORTED = 'productTypeUnsupported'; + const ERROR_ROW_IS_ORPHAN = 'rowIsOrphan'; + const ERROR_INVALID_TIER_PRICE_QTY = 'invalidTierPriceOrQty'; + const ERROR_INVALID_TIER_PRICE_SITE = 'tierPriceWebsiteInvalid'; + const ERROR_INVALID_TIER_PRICE_GROUP = 'tierPriceGroupInvalid'; + const ERROR_TIER_DATA_INCOMPLETE = 'tierPriceDataIsIncomplete'; + const ERROR_SKU_NOT_FOUND_FOR_DELETE = 'skuNotFoundToDelete'; + const ERROR_SUPER_PRODUCTS_SKU_NOT_FOUND = 'superProductsSkuNotFound'; /** * Pairs of attribute set ID-to-name. @@ -149,27 +150,28 @@ class Mage_ImportExport_Model_Import_Entity_Product extends Mage_ImportExport_Mo * @var array */ protected $_messageTemplates = array( - self::ERROR_INVALID_SCOPE => 'Invalid value in Scope column', - self::ERROR_INVALID_WEBSITE => 'Invalid value in Website column (website does not exists?)', - self::ERROR_INVALID_STORE => 'Invalid value in Store column (store does not exists?)', - self::ERROR_INVALID_ATTR_SET => 'Invalid value for Attribute Set column (set does not exists?)', - self::ERROR_INVALID_TYPE => 'Product Type is invalid or not supported', - self::ERROR_INVALID_CATEGORY => 'Category does not exists', - self::ERROR_VALUE_IS_REQUIRED => "Required attribute '%s' has an empty value", - self::ERROR_TYPE_CHANGED => 'Trying to change type of existing products', - self::ERROR_SKU_IS_EMPTY => 'SKU is empty', - self::ERROR_NO_DEFAULT_ROW => 'Default values row does not exists', - self::ERROR_CHANGE_TYPE => 'Product type change is not allowed', - self::ERROR_DUPLICATE_SCOPE => 'Duplicate scope', - self::ERROR_DUPLICATE_SKU => 'Duplicate SKU', - self::ERROR_CHANGE_ATTR_SET => 'Product attribute set change is not allowed', - self::ERROR_TYPE_UNSUPPORTED => 'Product type is not supported', - self::ERROR_ROW_IS_ORPHAN => 'Orphan rows that will be skipped due default row errors', - self::ERROR_INVALID_TIER_PRICE_QTY => 'Tier Price data price or quantity value is invalid', - self::ERROR_INVALID_TIER_PRICE_SITE => 'Tier Price data website is invalid', - self::ERROR_INVALID_TIER_PRICE_GROUP => 'Tier Price customer group ID is invalid', - self::ERROR_TIER_DATA_INCOMPLETE => 'Tier Price data is incomplete', - self::ERROR_SKU_NOT_FOUND_FOR_DELETE => 'Product with specified SKU not found' + self::ERROR_INVALID_SCOPE => 'Invalid value in Scope column', + self::ERROR_INVALID_WEBSITE => 'Invalid value in Website column (website does not exists?)', + self::ERROR_INVALID_STORE => 'Invalid value in Store column (store does not exists?)', + self::ERROR_INVALID_ATTR_SET => 'Invalid value for Attribute Set column (set does not exists?)', + self::ERROR_INVALID_TYPE => 'Product Type is invalid or not supported', + self::ERROR_INVALID_CATEGORY => 'Category does not exists', + self::ERROR_VALUE_IS_REQUIRED => "Required attribute '%s' has an empty value", + self::ERROR_TYPE_CHANGED => 'Trying to change type of existing products', + self::ERROR_SKU_IS_EMPTY => 'SKU is empty', + self::ERROR_NO_DEFAULT_ROW => 'Default values row does not exists', + self::ERROR_CHANGE_TYPE => 'Product type change is not allowed', + self::ERROR_DUPLICATE_SCOPE => 'Duplicate scope', + self::ERROR_DUPLICATE_SKU => 'Duplicate SKU', + self::ERROR_CHANGE_ATTR_SET => 'Product attribute set change is not allowed', + self::ERROR_TYPE_UNSUPPORTED => 'Product type is not supported', + self::ERROR_ROW_IS_ORPHAN => 'Orphan rows that will be skipped due default row errors', + self::ERROR_INVALID_TIER_PRICE_QTY => 'Tier Price data price or quantity value is invalid', + self::ERROR_INVALID_TIER_PRICE_SITE => 'Tier Price data website is invalid', + self::ERROR_INVALID_TIER_PRICE_GROUP => 'Tier Price customer group ID is invalid', + self::ERROR_TIER_DATA_INCOMPLETE => 'Tier Price data is incomplete', + self::ERROR_SKU_NOT_FOUND_FOR_DELETE => 'Product with specified SKU not found', + self::ERROR_SUPER_PRODUCTS_SKU_NOT_FOUND => 'Product with specified super products SKU not found' ); /** @@ -573,6 +575,26 @@ protected function _isTierPriceValid(array $rowData, $rowNum) return true; } + /** + * Check super products SKU + * + * @param array $rowData + * @param int $rowNum + * @return bool + */ + protected function _isSuperProductsSkuValid($rowData, $rowNum) + { + if (!empty($rowData['_super_products_sku']) + && (!isset($this->_oldSku[$rowData['_super_products_sku']]) + && !isset($this->_newSku[$rowData['_super_products_sku']]) + ) + ) { + $this->addRowError(self::ERROR_SUPER_PRODUCTS_SKU_NOT_FOUND, $rowNum); + return false; + } + return true; + } + /** * Custom options save. * @@ -1166,7 +1188,7 @@ protected function _saveProducts() $attrTable = $attribute->getBackend()->getTable(); $storeIds = array(0); - if ('datetime' == $attribute->getBackendType()) { + if ('datetime' == $attribute->getBackendType() && strtotime($attrValue)) { $attrValue = gmstrftime($strftimeFormat, strtotime($attrValue)); } elseif ($backModel) { $attribute->getBackend()->beforeSave($product); @@ -1613,6 +1635,7 @@ public function validateRow(array $rowData, $rowNum) $this->_isProductWebsiteValid($rowData, $rowNum); $this->_isProductCategoryValid($rowData, $rowNum); $this->_isTierPriceValid($rowData, $rowNum); + $this->_isSuperProductsSkuValid($rowData, $rowNum); if (self::SCOPE_DEFAULT == $rowScope) { // SKU is specified, row is SCOPE_DEFAULT, new product block begins $this->_processedEntitiesCount ++; diff --git a/app/code/core/Mage/ImportExport/Model/Import/Entity/Product/Type/Abstract.php b/app/code/core/Mage/ImportExport/Model/Import/Entity/Product/Type/Abstract.php index 872646d3ae..a18c1a580c 100644 --- a/app/code/core/Mage/ImportExport/Model/Import/Entity/Product/Type/Abstract.php +++ b/app/code/core/Mage/ImportExport/Model/Import/Entity/Product/Type/Abstract.php @@ -298,6 +298,8 @@ public function prepareAttributesForSave(array $rowData) ('select' == $attrParams['type'] || 'multiselect' == $attrParams['type']) ? $attrParams['options'][strtolower($rowData[$attrCode])] : $rowData[$attrCode]; + } elseif (array_key_exists($attrCode, $rowData)) { + $resultAttrs[$attrCode] = $rowData[$attrCode]; } elseif (null !== $attrParams['default_value']) { $resultAttrs[$attrCode] = $attrParams['default_value']; } diff --git a/app/code/core/Mage/Index/Block/Adminhtml/Notifications.php b/app/code/core/Mage/Index/Block/Adminhtml/Notifications.php index 12de6cd103..93a92e03fb 100644 --- a/app/code/core/Mage/Index/Block/Adminhtml/Notifications.php +++ b/app/code/core/Mage/Index/Block/Adminhtml/Notifications.php @@ -34,9 +34,12 @@ class Mage_Index_Block_Adminhtml_Notifications extends Mage_Adminhtml_Block_Temp public function getProcessesForReindex() { $res = array(); - $processes = Mage::getSingleton('index/indexer')->getProcessesCollection(); + $processes = Mage::getSingleton('index/indexer')->getProcessesCollection()->addEventsStats(); + /** @var $process Mage_Index_Model_Process */ foreach ($processes as $process) { - if ($process->getStatus() == Mage_Index_Model_Process::STATUS_REQUIRE_REINDEX) { + if (($process->getStatus() == Mage_Index_Model_Process::STATUS_REQUIRE_REINDEX + || $process->getEvents() > 0) && $process->getIndexer()->isVisible() + ) { $res[] = $process->getIndexer()->getName(); } } diff --git a/app/code/core/Mage/Index/Block/Adminhtml/Process/Grid.php b/app/code/core/Mage/Index/Block/Adminhtml/Process/Grid.php index 0ad486e601..e7aa55bf93 100644 --- a/app/code/core/Mage/Index/Block/Adminhtml/Process/Grid.php +++ b/app/code/core/Mage/Index/Block/Adminhtml/Process/Grid.php @@ -26,15 +26,27 @@ class Mage_Index_Block_Adminhtml_Process_Grid extends Mage_Adminhtml_Block_Widget_Grid { + /** + * Process model + * + * @var Mage_Index_Model_Process + */ protected $_processModel; + /** + * Mass-action block + * + * @var string + */ + protected $_massactionBlockName = 'index/adminhtml_process_grid_massaction'; + /** * Class constructor */ public function __construct() { parent::__construct(); - $this->_processModel = Mage::getModel('index/process'); + $this->_processModel = Mage::getSingleton('index/process'); $this->setId('indexer_processes_grid'); $this->_filterVisibility = false; $this->_pagerVisibility = false; @@ -55,9 +67,18 @@ protected function _prepareCollection() */ protected function _afterLoadCollection() { - foreach ($this->_collection as $item) { + /** @var $item Mage_Index_Model_Process */ + foreach ($this->_collection as $key => $item) { + if (!$item->getIndexer()->isVisible()) { + $this->_collection->removeItemByKey($key); + continue; + } $item->setName($item->getIndexer()->getName()); $item->setDescription($item->getIndexer()->getDescription()); + $item->setUpdateRequired($item->getUnprocessedEventsCollection()->count() > 0 ? 1 : 0); + if ($item->isLocked()) { + $item->setStatus(Mage_Index_Model_Process::STATUS_RUNNING); + } } return $this; } @@ -102,8 +123,19 @@ protected function _prepareColumns() 'frame_callback' => array($this, 'decorateStatus') )); + $this->addColumn('update_required', array( + 'header' => Mage::helper('index')->__('Update Required'), + 'sortable' => false, + 'width' => '120', + 'align' => 'left', + 'index' => 'update_required', + 'type' => 'options', + 'options' => $this->_processModel->getUpdateRequiredOptions(), + 'frame_callback' => array($this, 'decorateUpdateRequired') + )); + $this->addColumn('ended_at', array( - 'header' => Mage::helper('index')->__('Last Run'), + 'header' => Mage::helper('index')->__('Updated At'), 'type' => 'datetime', 'width' => '180', 'align' => 'left', @@ -123,11 +155,6 @@ protected function _prepareColumns() 'url' => array('base'=> '*/*/reindexProcess'), 'field' => 'process' ), -// array( -// 'caption' => Mage::helper('index')->__('Pending Events'), -// 'url' => array('base'=> '*/*/reindexEvents'), -// 'field' => 'process' -// ) ), 'filter' => false, 'sortable' => false, @@ -140,6 +167,10 @@ protected function _prepareColumns() /** * Decorate status column values * + * @param string $value + * @param Mage_Index_Model_Process $row + * @param Mage_Adminhtml_Block_Widget_Grid_Column $column + * @param bool $isExport * @return string */ public function decorateStatus($value, $row, $column, $isExport) @@ -159,6 +190,29 @@ public function decorateStatus($value, $row, $column, $isExport) return ''.$value.''; } + /** + * Decorate "Update Required" column values + * + * @param string $value + * @param Mage_Index_Model_Process $row + * @param Mage_Adminhtml_Block_Widget_Grid_Column $column + * @param bool $isExport + * @return string + */ + public function decorateUpdateRequired($value, $row, $column, $isExport) + { + $class = ''; + switch ($row->getUpdateRequired()) { + case 0: + $class = 'grid-severity-notice'; + break; + case 1: + $class = 'grid-severity-critical'; + break; + } + return ''.$value.''; + } + /** * Decorate last run date coumn * diff --git a/app/code/core/Mage/Index/Block/Adminhtml/Process/Grid/Massaction.php b/app/code/core/Mage/Index/Block/Adminhtml/Process/Grid/Massaction.php new file mode 100644 index 0000000000..6250266a19 --- /dev/null +++ b/app/code/core/Mage/Index/Block/Adminhtml/Process/Grid/Massaction.php @@ -0,0 +1,54 @@ + + */ +class Mage_Index_Block_Adminhtml_Process_Grid_Massaction extends Mage_Adminhtml_Block_Widget_Grid_Massaction_Abstract +{ + /** + * Get ids for only visible indexers + * + * @return string + */ + public function getGridIdsJson() + { + if (!$this->getUseSelectAll()) { + return ''; + } + + $ids = array(); + foreach ($this->getParentBlock()->getCollection() as $process) { + $ids[] = $process->getId(); + } + + return implode(',', $ids); + } +} diff --git a/app/code/core/Mage/Index/Model/Event.php b/app/code/core/Mage/Index/Model/Event.php index 3cb6452165..e6d4c3db1b 100644 --- a/app/code/core/Mage/Index/Model/Event.php +++ b/app/code/core/Mage/Index/Model/Event.php @@ -37,6 +37,7 @@ * @method Mage_Index_Model_Event setCreatedAt(string $value) * @method Mage_Index_Model_Event setOldData(string $value) * @method Mage_Index_Model_Event setNewData(string $value) + * @method Varien_Object getDataObject() * * @category Mage * @package Mage_Index @@ -116,14 +117,10 @@ public function setDataNamespace($namespace) public function resetData() { if ($this->_dataNamespace) { - $data = $this->getOldData(false); - $data[$this->_dataNamespace] = null; - $this->setOldData($data); $data = $this->getNewData(false); $data[$this->_dataNamespace] = null; $this->setNewData($data); } else { - $this->setOldData(array()); $this->setNewData(array()); } return $this; @@ -151,6 +148,37 @@ public function getProcessIds() return $this->_processIds; } + /** + * Merge new data + * + * @param array $previous + * @param mixed $current + * @return array + */ + protected function _mergeNewDataRecursive($previous, $current) + { + if (!is_array($current)) { + if (!is_null($current)) { + $previous[] = $current; + } + return $previous; + } + + foreach ($previous as $key => $value) { + if (array_key_exists($key, $current) && !is_null($current[$key]) && is_array($previous[$key])) { + if (!is_string($key) || is_array($current[$key])) { + $current[$key] = $this->_mergeNewDataRecursive($previous[$key], $current[$key]); + } + } elseif (!array_key_exists($key, $current) || is_null($current[$key])) { + $current[$key] = $previous[$key]; + } elseif (!is_array($previous[$key]) && !is_string($key)) { + $current[] = $previous[$key]; + } + } + + return $current; + } + /** * Merge previous event data to object. * Used for events duplicated protection @@ -164,40 +192,61 @@ public function mergePreviousData($data) $this->setId($data['event_id']); $this->setCreatedAt($data['created_at']); } - if (!empty($data['old_data'])) { - $this->setOldData($data['old_data']); - } + if (!empty($data['new_data'])) { $previousNewData = unserialize($data['new_data']); $currentNewData = $this->getNewData(false); - $currentNewData = array_merge($previousNewData, $currentNewData); + $currentNewData = $this->_mergeNewDataRecursive($previousNewData, $currentNewData); $this->setNewData(serialize($currentNewData)); } return $this; } + /** + * Clean new data, unset data for done processes + * + * @return Mage_Index_Model_Event + */ + public function cleanNewData() + { + $processIds = $this->getProcessIds(); + if (!is_array($processIds) || empty($processIds)) { + return $this; + } + + $newData = $this->getNewData(false); + foreach ($processIds as $processId => $processStatus) { + if ($processStatus == Mage_Index_Model_Process::EVENT_STATUS_DONE) { + $process = Mage::getSingleton('index/indexer')->getProcessById($processId); + if ($process) { + $namespace = get_class($process->getIndexer()); + if (array_key_exists($namespace, $newData)) { + unset($newData[$namespace]); + } + } + } + } + $this->setNewData(serialize($newData)); + + return $this; + } + /** * Get event old data array * + * @deprecated since 1.6.2.0 + * @param bool $useNamespace * @return array */ public function getOldData($useNamespace = true) { - $data = $this->_getData('old_data'); - if (is_string($data)) { - $data = unserialize($data); - } elseif (empty($data) || !is_array($data)) { - $data = array(); - } - if ($useNamespace && $this->_dataNamespace) { - return isset($data[$this->_dataNamespace]) ? $data[$this->_dataNamespace] : array(); - } - return $data; + return array(); } /** * Get event new data array * + * @param bool $useNamespace * @return array */ public function getNewData($useNamespace = true) @@ -217,26 +266,13 @@ public function getNewData($useNamespace = true) /** * Add new values to old data array (overwrite if value with same key exist) * + * @deprecated since 1.6.2.0 * @param array | string $data * @param null | mixed $value * @return Mage_Index_Model_Event */ public function addOldData($key, $value=null) { - $oldData = $this->getOldData(false); - if (!is_array($key)) { - $key = array($key => $value); - } - - if ($this->_dataNamespace) { - if (!isset($oldData[$this->_dataNamespace])) { - $oldData[$this->_dataNamespace] = array(); - } - $oldData[$this->_dataNamespace] = array_merge($oldData[$this->_dataNamespace], $key); - } else { - $oldData = array_merge($oldData, $key); - } - $this->setOldData($oldData); return $this; } @@ -294,9 +330,7 @@ public function getType() */ protected function _beforeSave() { - $oldData = $this->getOldData(false); $newData = $this->getNewData(false); - $this->setOldData(serialize($oldData)); $this->setNewData(serialize($newData)); if (!$this->hasCreatedAt()) { $this->setCreatedAt($this->_getResource()->formatDate(time(), true)); diff --git a/app/code/core/Mage/Index/Model/Indexer.php b/app/code/core/Mage/Index/Model/Indexer.php index 4ac94a0373..9c0c29d02c 100644 --- a/app/code/core/Mage/Index/Model/Indexer.php +++ b/app/code/core/Mage/Index/Model/Indexer.php @@ -39,6 +39,7 @@ class Mage_Index_Model_Indexer /** * Indexer processes lock flag * + * @deprecated after 1.6.1.0 * @var bool */ protected $_lockFlag = false; @@ -50,6 +51,14 @@ class Mage_Index_Model_Indexer */ protected $_allowTableChanges = true; + /** + * Current processing event(s) + * In array case it should be array(Entity type, Event type) + * + * @var null|Mage_Index_Model_Event|array + */ + protected $_currentEvent = null; + /** * Class constructor. Initialize index processes based on configuration */ @@ -102,6 +111,9 @@ public function getProcessByCode($code) /** * Lock indexer actions + * @deprecated after 1.6.1.0 + * + * @return Mage_Index_Model_Indexer */ public function lockIndexer() { @@ -111,6 +123,9 @@ public function lockIndexer() /** * Unlock indexer actions + * @deprecated after 1.6.1.0 + * + * @return Mage_Index_Model_Indexer */ public function unlockIndexer() { @@ -121,6 +136,7 @@ public function unlockIndexer() /** * Check if onject actions are locked * + * @deprecated after 1.6.1.0 * @return bool */ public function isLocked() @@ -138,11 +154,32 @@ public function isLocked() */ public function indexEvents($entity=null, $type=null) { - if ($this->isLocked()) { - return $this; + Mage::dispatchEvent('start_index_events' . $this->_getEventTypeName($entity, $type)); + + /** @var $resourceModel Mage_Index_Model_Resource_Process */ + $resourceModel = Mage::getResourceSingleton('index/process'); + + $allowTableChanges = $this->_allowTableChanges && !$resourceModel->isInTransaction(); + if ($allowTableChanges) { + $this->_currentEvent = array($entity, $type); + $this->_changeKeyStatus(false); } - $this->_runAll('indexEvents', array($entity, $type)); + $resourceModel->beginTransaction(); + $this->_allowTableChanges = false; + try { + $this->_runAll('indexEvents', array($entity, $type)); + $resourceModel->commit(); + } catch (Exception $e) { + $resourceModel->rollBack(); + throw $e; + } + if ($allowTableChanges) { + $this->_allowTableChanges = true; + $this->_changeKeyStatus(true); + $this->_currentEvent = null; + } + Mage::dispatchEvent('end_index_events' . $this->_getEventTypeName($entity, $type)); return $this; } @@ -154,11 +191,7 @@ public function indexEvents($entity=null, $type=null) */ public function indexEvent(Mage_Index_Model_Event $event) { - if ($this->isLocked()) { - return $this; - } - - $this->_runAll('processEvent', array($event)); + $this->_runAll('safeProcessEvent', array($event)); return $this; } @@ -169,10 +202,6 @@ public function indexEvent(Mage_Index_Model_Event $event) */ public function registerEvent(Mage_Index_Model_Event $event) { - if ($this->isLocked()) { - return $this; - } - $this->_runAll('register', array($event)); return $this; } @@ -188,9 +217,6 @@ public function registerEvent(Mage_Index_Model_Event $event) */ public function logEvent(Varien_Object $entity, $entityType, $eventType, $doSave=true) { - if ($this->isLocked()) { - return $this; - } $event = Mage::getModel('index/event') ->setEntity($entityType) ->setType($eventType) @@ -215,31 +241,43 @@ public function logEvent(Varien_Object $entity, $entityType, $eventType, $doSave */ public function processEntityAction(Varien_Object $entity, $entityType, $eventType) { - if ($this->isLocked()) { - return $this; - } $event = $this->logEvent($entity, $entityType, $eventType, false); /** * Index and save event just in case if some process matched it */ if ($event->getProcessIds()) { - $this->_changeKeyStatus(false); - /** @var $resourceModel Mage_Index_Model_Resource_Abstract */ - $resourceModel = Mage::getResourceModel('index/process'); + Mage::dispatchEvent('start_process_event' . $this->_getEventTypeName($entityType, $eventType)); + + /** @var $resourceModel Mage_Index_Model_Resource_Process */ + $resourceModel = Mage::getResourceSingleton('index/process'); + + $allowTableChanges = $this->_allowTableChanges && !$resourceModel->isInTransaction(); + if ($allowTableChanges) { + $this->_currentEvent = $event; + $this->_changeKeyStatus(false); + } + $resourceModel->beginTransaction(); $this->_allowTableChanges = false; try { $this->indexEvent($event); $resourceModel->commit(); - $this->_allowTableChanges = true; - $this->_changeKeyStatus(); } catch (Exception $e) { $resourceModel->rollBack(); - $this->_allowTableChanges = true; - $this->_changeKeyStatus(); + if ($allowTableChanges) { + $this->_allowTableChanges = true; + $this->_changeKeyStatus(true); + $this->_currentEvent = null; + } throw $e; } + if ($allowTableChanges) { + $this->_allowTableChanges = true; + $this->_changeKeyStatus(true); + $this->_currentEvent = null; + } $event->save(); + Mage::dispatchEvent('end_process_event' . $this->_getEventTypeName($entityType, $eventType)); } return $this; } @@ -255,44 +293,44 @@ public function processEntityAction(Varien_Object $entity, $entityType, $eventTy */ protected function _runAll($method, $args) { + $checkLocks = $method != 'register'; $processed = array(); foreach ($this->_processesCollection as $process) { $code = $process->getIndexerCode(); if (in_array($code, $processed)) { continue; } + $hasLocks = false; - if (!$this->_allowTableChanges && is_callable(array($process, 'setAllowTableChanges'))) { - $process->setAllowTableChanges(false); - } if ($process->getDepends()) { foreach ($process->getDepends() as $processCode) { $dependProcess = $this->getProcessByCode($processCode); if ($dependProcess && !in_array($processCode, $processed)) { - if (!$this->_allowTableChanges && is_callable(array($dependProcess, 'setAllowTableChanges'))) { - $dependProcess->setAllowTableChanges(false); - } - call_user_func_array(array($dependProcess, $method), $args); - if (!$this->_allowTableChanges && is_callable(array($dependProcess, 'setAllowTableChanges'))) { - $dependProcess->setAllowTableChanges(true); + if ($checkLocks && $dependProcess->isLocked()) { + $hasLocks = true; + } else { + call_user_func_array(array($dependProcess, $method), $args); + if ($checkLocks && $dependProcess->getMode() == Mage_Index_Model_Process::MODE_MANUAL) { + $hasLocks = true; + } else { + $processed[] = $processCode; + } } - $processed[] = $processCode; } } } - call_user_func_array(array($process, $method), $args); - if (!$this->_allowTableChanges && is_callable(array($process, 'setAllowTableChanges'))) { - $process->setAllowTableChanges(true); + if (!$hasLocks) { + call_user_func_array(array($process, $method), $args); + $processed[] = $code; } - - $processed[] = $code; } } /** * Enable/Disable keys in index tables * + * @param bool $enable * @return Mage_Index_Model_Indexer */ protected function _changeKeyStatus($enable = true) @@ -308,28 +346,62 @@ protected function _changeKeyStatus($enable = true) foreach ($process->getDepends() as $processCode) { $dependProcess = $this->getProcessByCode($processCode); if ($dependProcess && !in_array($processCode, $processed)) { - if ($dependProcess instanceof Mage_Index_Model_Process) { - if ($enable) { - $dependProcess->enableIndexerKeys(); - } else { - $dependProcess->disableIndexerKeys(); - } + if ($this->_changeProcessKeyStatus($dependProcess, $enable)) { $processed[] = $processCode; } } } } - if ($process instanceof Mage_Index_Model_Process) { - if ($enable) { - $process->enableIndexerKeys(); - } else { - $process->disableIndexerKeys(); - } + if ($this->_changeProcessKeyStatus($process, $enable)) { $processed[] = $code; } } return $this; } + + /** + * Check if the event will be processed and disable/enable keys in index tables + * + * @param mixed|Mage_Index_Model_Process $process + * @param bool $enable + * @return bool + */ + protected function _changeProcessKeyStatus($process, $enable = true) + { + $event = $this->_currentEvent; + if ($process instanceof Mage_Index_Model_Process + && $process->getMode() !== Mage_Index_Model_Process::MODE_MANUAL + && !$process->isLocked() + && (is_null($event) + || ($event instanceof Mage_Index_Model_Event && $process->matchEvent($event)) + || (is_array($event) && $process->matchEntityAndType($event[0], $event[1])) + )) { + if ($enable) { + $process->enableIndexerKeys(); + } else { + $process->disableIndexerKeys(); + } + return true; + } + return false; + } + + /** + * Get event type name + * + * @param null|string $entityType + * @param null|string $eventType + * @return string + */ + protected function _getEventTypeName($entityType = null, $eventType = null) + { + $eventName = $entityType . '_' . $eventType; + $eventName = trim($eventName, '_'); + if (!empty($eventName)) { + $eventName = '_' . $eventName; + } + return $eventName; + } } diff --git a/app/code/core/Mage/Index/Model/Indexer/Abstract.php b/app/code/core/Mage/Index/Model/Indexer/Abstract.php index f17c98cf20..c633427359 100644 --- a/app/code/core/Mage/Index/Model/Indexer/Abstract.php +++ b/app/code/core/Mage/Index/Model/Indexer/Abstract.php @@ -35,10 +35,18 @@ abstract class Mage_Index_Model_Indexer_Abstract extends Mage_Core_Model_Abstrac /** * Whether table changes are allowed * + * @deprecated after 1.6.1.0 * @var bool */ protected $_allowTableChanges = true; + /** + * Whether the indexer should be displayed on process/list page + * + * @var bool + */ + protected $_isVisible = true; + /** * Get Indexer name * @@ -51,7 +59,10 @@ abstract public function getName(); * * @return string */ - abstract public function getDescription(); + public function getDescription() + { + return ''; + } /** * Register indexer required data inside event object @@ -149,13 +160,7 @@ public function callEventHandler(Mage_Index_Model_Event $event) $resourceModel = $this->_getResource(); if (method_exists($resourceModel, $method)) { - if (!$this->_allowTableChanges && is_callable(array($resourceModel, 'setAllowTableChanges'))) { - $resourceModel->setAllowTableChanges(false); - } $resourceModel->$method($event); - if (!$this->_allowTableChanges && is_callable(array($resourceModel, 'setAllowTableChanges'))) { - $resourceModel->setAllowTableChanges(true); - } } return $this; } @@ -163,6 +168,7 @@ public function callEventHandler(Mage_Index_Model_Event $event) /** * Set whether table changes are allowed * + * @deprecated after 1.6.1.0 * @param bool $value * @return Mage_Index_Model_Indexer_Abstract */ @@ -211,4 +217,14 @@ public function enableKeys() return $this; } + + /** + * Whether the indexer should be displayed on process/list page + * + * @return bool + */ + public function isVisible() + { + return $this->_isVisible; + } } diff --git a/app/code/core/Mage/Index/Model/Observer.php b/app/code/core/Mage/Index/Model/Observer.php index 2f0e0bd806..498bca0cf1 100644 --- a/app/code/core/Mage/Index/Model/Observer.php +++ b/app/code/core/Mage/Index/Model/Observer.php @@ -35,7 +35,7 @@ class Mage_Index_Model_Observer public function __construct() { - $this->_indexer = Mage::getModel('index/indexer'); + $this->_indexer = Mage::getSingleton('index/indexer'); } /** diff --git a/app/code/core/Mage/Index/Model/Process.php b/app/code/core/Mage/Index/Model/Process.php index b2e355b277..b8c29699eb 100644 --- a/app/code/core/Mage/Index/Model/Process.php +++ b/app/code/core/Mage/Index/Model/Process.php @@ -85,6 +85,7 @@ class Mage_Index_Model_Process extends Mage_Core_Model_Abstract /** * Whether table changes are allowed * + * @deprecated after 1.6.1.0 * @var bool */ protected $_allowTableChanges = true; @@ -135,6 +136,9 @@ public function register(Mage_Index_Model_Event $event) $this->getIndexer()->register($event); $event->addProcessId($this->getId()); $this->_resetEventNamespace($event); + if ($this->getMode() == self::MODE_MANUAL) { + $this->_getResource()->updateStatus($this, self::STATUS_REQUIRE_REINDEX); + } } return $this; @@ -151,21 +155,71 @@ public function matchEvent(Mage_Index_Model_Event $event) return $this->getIndexer()->matchEvent($event); } + /** + * Check if specific entity and action type is matched + * + * @param string $entity + * @param string $type + * @return bool + */ + public function matchEntityAndType($entity, $type) + { + if ($entity !== null && $type !== null) { + return $this->getIndexer()->matchEntityAndType($entity, $type); + } + return true; + } + /** * Reindex all data what this process responsible is * - * @return unknown_type */ public function reindexAll() { if ($this->isLocked()) { Mage::throwException(Mage::helper('index')->__('%s Index process is working now. Please try run this process later.', $this->getIndexer()->getName())); } + + $processStatus = $this->getStatus(); + $this->_getResource()->startProcess($this); $this->lock(); - $this->getIndexer()->reindexAll(); - $this->unlock(); - $this->_getResource()->endProcess($this); + try { + $eventsCollection = $this->getUnprocessedEventsCollection(); + + /** @var $eventResource Mage_Index_Model_Resource_Event */ + $eventResource = Mage::getResourceSingleton('index/event'); + + if ($eventsCollection->count() > 0 && $processStatus == self::STATUS_PENDING + || $this->getForcePartialReindex() + ) { + $this->_getResource()->beginTransaction(); + try { + $this->_processEventsCollection($eventsCollection, false); + $this->_getResource()->commit(); + } catch (Exception $e) { + $this->_getResource()->rollBack(); + throw $e; + } + } else { + //Update existing events since we'll do reindexAll + $eventResource->updateProcessEvents($this); + $this->getIndexer()->reindexAll(); + } + $this->unlock(); + + $unprocessedEvents = $eventResource->getUnprocessedEvents($this); + if ($this->getMode() == self::MODE_MANUAL && (count($unprocessedEvents) > 0)) { + $this->_getResource()->updateStatus($this, self::STATUS_REQUIRE_REINDEX); + } else { + $this->_getResource()->endProcess($this); + } + } catch (Exception $e) { + $this->unlock(); + $this->_getResource()->failProcess($this); + throw $e; + } + Mage::dispatchEvent('after_reindex_process_' . $this->getIndexerCode()); } /** @@ -180,6 +234,11 @@ public function reindexEverything() return $this; } + /** @var $eventResource Mage_Index_Model_Resource_Event */ + $eventResource = Mage::getResourceSingleton('index/event'); + $unprocessedEvents = $eventResource->getUnprocessedEvents($this); + $this->setForcePartialReindex(count($unprocessedEvents) > 0 && $this->getStatus() == self::STATUS_PENDING); + if ($this->getDepends()) { $indexer = Mage::getSingleton('index/indexer'); foreach ($this->getDepends() as $code) { @@ -202,27 +261,28 @@ public function reindexEverything() */ public function processEvent(Mage_Index_Model_Event $event) { - if ($this->getMode() == self::MODE_MANUAL) { - $this->changeStatus(self::STATUS_REQUIRE_REINDEX); + if (!$this->matchEvent($event)) { return $this; } - if (!$this->getIndexer()->matchEvent($event)) { + if ($this->getMode() == self::MODE_MANUAL) { + $this->changeStatus(self::STATUS_REQUIRE_REINDEX); return $this; } + + $this->_getResource()->updateProcessStartDate($this); $this->_setEventNamespace($event); + $isError = false; - $indexer = $this->getIndexer(); - if (!$this->_allowTableChanges && is_callable(array($indexer, 'setAllowTableChanges'))) { - $indexer->setAllowTableChanges(false); - } - $this->getIndexer()->processEvent($event); - if (!$this->_allowTableChanges && is_callable(array($indexer, 'setAllowTableChanges'))) { - $indexer->setAllowTableChanges(true); + try { + $this->getIndexer()->processEvent($event); + } catch (Exception $e) { + $isError = true; } - $event->resetData(); $this->_resetEventNamespace($event); - $event->addProcessId($this->getId(), self::EVENT_STATUS_DONE); + $this->_getResource()->updateProcessEndDate($this); + $event->addProcessId($this->getId(), $isError ? self::EVENT_STATUS_ERROR : self::EVENT_STATUS_DONE); + return $this; } @@ -278,36 +338,56 @@ public function indexEvents($entity=null, $type=null) if ($this->isLocked()) { return $this; } + $this->lock(); + try { + /** + * Prepare events collection + */ + $eventsCollection = $this->getUnprocessedEventsCollection(); + if ($entity !== null) { + $eventsCollection->addEntityFilter($entity); + } + if ($type !== null) { + $eventsCollection->addTypeFilter($type); + } - /** - * Prepare events collection - */ - $eventsCollection = Mage::getResourceModel('index/event_collection') - ->addProcessFilter($this, self::EVENT_STATUS_NEW); - if ($entity !== null) { - $eventsCollection->addEntityFilter($entity); - } - if ($type !== null) { - $eventsCollection->addTypeFilter($type); + $this->_processEventsCollection($eventsCollection); + $this->unlock(); + } catch (Exception $e) { + $this->unlock(); + throw $e; } + return $this; + } - /** - * Process all new events - */ - while ($eventsCollection->getSize()) { - foreach ($eventsCollection as $event) { - try { - $this->processEvent($event); - } catch (Exception $e) { - $event->addProcessId($this->getId(), self::EVENT_STATUS_ERROR); + /** + * Process all events of the collection + * + * @param Mage_Index_Model_Resource_Event_Collection $eventsCollection + * @param bool $skipUnmatched + * @return Mage_Index_Model_Process + */ + protected function _processEventsCollection( + Mage_Index_Model_Resource_Event_Collection $eventsCollection, + $skipUnmatched = true + ) { + // We can't reload the collection because of transaction + /** @var $event Mage_Index_Model_Event */ + while ($event = $eventsCollection->fetchItem()) { + try { + $this->processEvent($event); + if (!$skipUnmatched) { + $eventProcessIds = $event->getProcessIds(); + if (!isset($eventProcessIds[$this->getId()])) { + $event->addProcessId($this->getId(), null); + } } - $event->save(); + } catch (Exception $e) { + $event->addProcessId($this->getId(), self::EVENT_STATUS_ERROR); } - $eventsCollection->reset(); + $event->save(); } - - $this->unlock(); return $this; } @@ -451,6 +531,19 @@ public function getStatusesOptions() ); } + /** + * Get list of "Update Required" options + * + * @return array + */ + public function getUpdateRequiredOptions() + { + return array( + 0 => Mage::helper('index')->__('No'), + 1 => Mage::helper('index')->__('Yes'), + ); + } + /** * Retrieve depend indexer codes * @@ -479,6 +572,7 @@ public function getDepends() /** * Set whether table changes are allowed * + * @deprecated after 1.6.1.0 * @param bool $value * @return Mage_Index_Model_Process */ @@ -515,4 +609,42 @@ public function enableIndexerKeys() } return $this; } + + /** + * Process event with locks checking + * + * @param Mage_Index_Model_Event $event + * @return Mage_Index_Model_Process + */ + public function safeProcessEvent(Mage_Index_Model_Event $event) + { + if ($this->isLocked()) { + return $this; + } + if (!$this->matchEvent($event)) { + return $this; + } + $this->lock(); + try { + $this->processEvent($event); + $this->unlock(); + } catch (Exception $e) { + $this->unlock(); + throw $e; + } + return $this; + } + + /** + * Get unprocessed events collection + * + * @return Mage_Index_Model_Resource_Event_Collection + */ + public function getUnprocessedEventsCollection() + { + /** @var $eventsCollection Mage_Index_Model_Resource_Event_Collection */ + $eventsCollection = Mage::getResourceModel('index/event_collection'); + $eventsCollection->addProcessFilter($this, self::EVENT_STATUS_NEW); + return $eventsCollection; + } } diff --git a/app/code/core/Mage/Index/Model/Resource/Abstract.php b/app/code/core/Mage/Index/Model/Resource/Abstract.php index 0484f929fe..adf051f6f5 100755 --- a/app/code/core/Mage/Index/Model/Resource/Abstract.php +++ b/app/code/core/Mage/Index/Model/Resource/Abstract.php @@ -53,6 +53,7 @@ abstract class Mage_Index_Model_Resource_Abstract extends Mage_Core_Model_Resour /** * Whether table changes are allowed * + * @deprecated after 1.6.1.0 * @var bool */ protected $_allowTableChanges = true; @@ -104,12 +105,17 @@ public function getIdxTable($table = null) public function syncData() { $this->beginTransaction(); - /** - * Can't use truncate because of transaction - */ - $this->_getWriteAdapter()->delete($this->getMainTable()); - $this->insertFromTable($this->getIdxTable(), $this->getMainTable(), false); - $this->commit(); + try { + /** + * Can't use truncate because of transaction + */ + $this->_getWriteAdapter()->delete($this->getMainTable()); + $this->insertFromTable($this->getIdxTable(), $this->getMainTable(), false); + $this->commit(); + } catch (Exception $e) { + $this->rollBack(); + throw $e; + } return $this; } @@ -168,9 +174,6 @@ public function insertFromSelect($select, $destTable, array $columns, $readToInd $to = $this->_getWriteAdapter(); } - if ($this->useDisableKeys() && $this->_allowTableChanges) { - $to->disableTableKeys($destTable); - } if ($from === $to) { $query = $select->insertFromSelect($destTable, $columns); $to->query($query); @@ -191,9 +194,7 @@ public function insertFromSelect($select, $destTable, array $columns, $readToInd $to->insertArray($destTable, $columns, $data); } } - if ($this->useDisableKeys() && $this->_allowTableChanges) { - $to->enableTableKeys($destTable); - } + return $this; } @@ -237,6 +238,7 @@ public function clearTemporaryIndexTable() /** * Set whether table changes are allowed * + * @deprecated after 1.6.1.0 * @param bool $value * @return Mage_Index_Model_Resource_Abstract */ diff --git a/app/code/core/Mage/Index/Model/Resource/Event.php b/app/code/core/Mage/Index/Model/Resource/Event.php index 32aa54804e..b411fce1c1 100755 --- a/app/code/core/Mage/Index/Model/Resource/Event.php +++ b/app/code/core/Mage/Index/Model/Resource/Event.php @@ -67,6 +67,7 @@ protected function _beforeSave(Mage_Core_Model_Abstract $object) $object->mergePreviousData($data); } } + $object->cleanNewData(); return parent::_beforeSave($object); } @@ -85,10 +86,17 @@ protected function _afterSave(Mage_Core_Model_Abstract $object) $this->_getWriteAdapter()->delete($processTable); } else { foreach ($processIds as $processId => $processStatus) { + if (is_null($processStatus) || $processStatus == Mage_Index_Model_Process::EVENT_STATUS_DONE) { + $this->_getWriteAdapter()->delete($processTable, array( + 'process_id = ?' => $processId, + 'event_id = ?' => $object->getId(), + )); + continue; + } $data = array( - 'process_id'=> $processId, - 'event_id' => $object->getId(), - 'status' => $processStatus + 'process_id' => $processId, + 'event_id' => $object->getId(), + 'status' => $processStatus ); $this->_getWriteAdapter()->insertOnDuplicate($processTable, $data, array('status')); } @@ -96,4 +104,45 @@ protected function _afterSave(Mage_Core_Model_Abstract $object) } return parent::_afterSave($object); } + + /** + * Update status for events of process + * + * @param int|array|Mage_Index_Model_Process $process + * @param string $status + * @return Mage_Index_Model_Resource_Event + */ + public function updateProcessEvents($process, $status = Mage_Index_Model_Process::EVENT_STATUS_DONE) + { + $whereCondition = ''; + if ($process instanceof Mage_Index_Model_Process) { + $whereCondition = array('process_id = ?' => $process->getId()); + } elseif (is_array($process) && !empty($process)) { + $whereCondition = array('process_id IN (?)' => $process); + } elseif (!is_array($whereCondition)) { + $whereCondition = array('process_id = ?' => $process); + } + $this->_getWriteAdapter()->update( + $this->getTable('index/process_event'), + array('status' => $status), + $whereCondition + ); + return $this; + } + + /** + * Retrieve unprocessed events list by specified process + * + * @param Mage_Index_Model_Process $process + * @return array + */ + public function getUnprocessedEvents($process) + { + $select = $this->_getReadAdapter()->select() + ->from($this->getTable('index/process_event')) + ->where('process_id = ?', $process->getId()) + ->where('status = ?', Mage_Index_Model_Process::EVENT_STATUS_NEW); + + return $this->_getReadAdapter()->fetchAll($select); + } } diff --git a/app/code/core/Mage/Index/Model/Resource/Event/Collection.php b/app/code/core/Mage/Index/Model/Resource/Event/Collection.php index 3e998a2441..1e1eb4c9f4 100755 --- a/app/code/core/Mage/Index/Model/Resource/Event/Collection.php +++ b/app/code/core/Mage/Index/Model/Resource/Event/Collection.php @@ -88,13 +88,17 @@ public function addProcessFilter($process, $status = null) if ($process instanceof Mage_Index_Model_Process) { $this->addFieldToFilter('process_event.process_id', $process->getId()); } elseif (is_array($process) && !empty($process)) { - $this->addFieldToFilter('process_event.process_id', array('in'=>$process)); + $this->addFieldToFilter('process_event.process_id', array('in' => $process)); } else { $this->addFieldToFilter('process_event.process_id', $process); } if ($status !== null) { - $this->addFieldToFilter('process_event.status', $status); + if (is_array($status) && !empty($status)) { + $this->addFieldToFilter('process_event.status', array('in' => $status)); + } else { + $this->addFieldToFilter('process_event.status', $status); + } } return $this; } @@ -126,6 +130,7 @@ public function reset() $this->_totalRecords = null; $this->_data = null; $this->_isCollectionLoaded = false; + $this->_items = array(); return $this; } } diff --git a/app/code/core/Mage/Index/Model/Resource/Process.php b/app/code/core/Mage/Index/Model/Resource/Process.php index 1b7554d566..ec35a2d653 100755 --- a/app/code/core/Mage/Index/Model/Resource/Process.php +++ b/app/code/core/Mage/Index/Model/Resource/Process.php @@ -94,6 +94,22 @@ public function startProcess(Mage_Index_Model_Process $process) return $this; } + /** + * Register process fail + * + * @param Mage_Index_Model_Process $process + * @return Mage_Index_Model_Resource_Process + */ + public function failProcess(Mage_Index_Model_Process $process) + { + $data = array( + 'status' => Mage_Index_Model_Process::STATUS_REQUIRE_REINDEX, + 'ended_at' => $this->formatDate(time()), + ); + $this->_updateProcessData($process->getId(), $data); + return $this; + } + /** * Update process status field * @@ -115,11 +131,45 @@ public function updateStatus($process, $status) * @param array $data * @return Mage_Index_Model_Resource_Process */ - protected function _updateProcessData($processId,$data) + protected function _updateProcessData($processId, $data) { $bind = array('process_id=?' => $processId); $this->_getWriteAdapter()->update($this->getMainTable(), $data, $bind); return $this; } + + /** + * Update process start date + * + * @param Mage_Index_Model_Process $process + * @return Mage_Index_Model_Resource_Process + */ + public function updateProcessStartDate(Mage_Index_Model_Process $process) + { + $this->_updateProcessData($process->getId(), array('started_at' => $this->formatDate(time()))); + return $this; + } + + /** + * Update process end date + * + * @param Mage_Index_Model_Process $process + * @return Mage_Index_Model_Resource_Process + */ + public function updateProcessEndDate(Mage_Index_Model_Process $process) + { + $this->_updateProcessData($process->getId(), array('ended_at' => $this->formatDate(time()))); + return $this; + } + + /** + * Whether transaction is already started + * + * @return bool + */ + public function isInTransaction() + { + return $this->_getWriteAdapter()->getTransactionLevel() > 0; + } } diff --git a/app/code/core/Mage/Index/Model/Resource/Process/Collection.php b/app/code/core/Mage/Index/Model/Resource/Process/Collection.php index 3f3390daef..9b0b1e908c 100755 --- a/app/code/core/Mage/Index/Model/Resource/Process/Collection.php +++ b/app/code/core/Mage/Index/Model/Resource/Process/Collection.php @@ -42,4 +42,27 @@ protected function _construct() { $this->_init('index/process'); } + + /** + * Add count of unprocessed events to process collection + * + * @return Mage_Index_Model_Resource_Process_Collection + */ + public function addEventsStats() + { + $countsSelect = $this->getConnection() + ->select() + ->from($this->getTable('index/process_event'), array('process_id', 'events' => 'COUNT(*)')) + ->where('status=?', Mage_Index_Model_Process::EVENT_STATUS_NEW) + ->group('process_id'); + $this->getSelect() + ->joinLeft( + array('e' => $countsSelect), + 'e.process_id=main_table.process_id', + array('events' => $this->getConnection()->getCheckSql( + $this->getConnection()->prepareSqlCondition('e.events', array('null' => null)), 0, 'e.events' + )) + ); + return $this; + } } diff --git a/app/code/core/Mage/Install/Model/Installer/Db.php b/app/code/core/Mage/Install/Model/Installer/Db.php index 992d9dc1f3..43c5b31d22 100644 --- a/app/code/core/Mage/Install/Model/Installer/Db.php +++ b/app/code/core/Mage/Install/Model/Installer/Db.php @@ -67,8 +67,8 @@ public function checkDbConnectionData($data) } } if (!empty($absenteeExtensions)) { - Mage::throwException(Mage::helper('install')->__('PHP Extensions "%s" must be loaded.', - implode(',', $absenteeExtensions)) + Mage::throwException( + Mage::helper('install')->__('PHP Extensions "%s" must be loaded.', implode(',', $absenteeExtensions)) ); } @@ -78,14 +78,16 @@ public function checkDbConnectionData($data) // check DB server version if (version_compare($version, $requiredVersion) == -1) { - Mage::throwException(Mage::helper('install')->__('The database server version ' - . 'does not match system requirements (required: %s, actual: %s).', $requiredVersion, $version)); + Mage::throwException( + Mage::helper('install')->__('The database server version doesn\'t match system requirements (required: %s, actual: %s).', $requiredVersion, $version) + ); } // check InnoDB support if (!$resource->supportEngine()) { - Mage::throwException(Mage::helper('install')->__('Database server does not support ' - . 'the InnoDB storage engine.')); + Mage::throwException( + Mage::helper('install')->__('Database server does not support the InnoDB storage engine.') + ); } // TODO: check user roles @@ -121,8 +123,8 @@ protected function _getCheckedData($data) if ($data['db_prefix'] != '') { if (!preg_match('/^[a-z]+[a-z0-9_]*$/', $data['db_prefix'])) { Mage::throwException( - Mage::helper('install')->__('The table prefix should contain only letters (a-z), ' - . 'numbers (0-9) or underscores (_), the first character should be a letter.')); + Mage::helper('install')->__('The table prefix should contain only letters (a-z), numbers (0-9) or underscores (_), the first character should be a letter.') + ); } } //set default db model @@ -158,7 +160,9 @@ protected function _getDbResource($model) if (!isset($this->_dbResource)) { $resource = Mage::getSingleton(sprintf('install/installer_db_%s', $model)); if (!$resource) { - Mage::throwException(Mage::helper('install')->__(sprintf('Installer does not exist for %s database type', $model))); + Mage::throwException( + Mage::helper('install')->__('Installer does not exist for %s database type', $model) + ); } $this->_dbResource = $resource; } diff --git a/app/code/core/Mage/Log/Model/Resource/Visitor.php b/app/code/core/Mage/Log/Model/Resource/Visitor.php index a015174447..aea1cac85f 100755 --- a/app/code/core/Mage/Log/Model/Resource/Visitor.php +++ b/app/code/core/Mage/Log/Model/Resource/Visitor.php @@ -119,6 +119,26 @@ protected function _afterSave(Mage_Core_Model_Abstract $visitor) return $this; } + /** + * Perform actions after object load + * + * @param Varien_Object $object + * @return Mage_Core_Model_Resource_Db_Abstract + */ + protected function _afterLoad(Mage_Core_Model_Abstract $object) + { + parent::_afterLoad($object); + // Add information about quote to visitor + $adapter = $this->_getReadAdapter(); + $select = $adapter->select()->from($this->getTable('log/quote_table'), 'quote_id') + ->where('visitor_id = ?', $object->getId())->limit(1); + $result = $adapter->query($select)->fetch(); + if (isset($result['quote_id'])) { + $object->setQuoteId((int) $result['quote_id']); + } + return $this; + } + /** * Saving visitor information * diff --git a/app/code/core/Mage/Log/sql/log_setup/install-1.6.0.0.php b/app/code/core/Mage/Log/sql/log_setup/install-1.6.0.0.php index 83a4e88c64..ab67fc4530 100644 --- a/app/code/core/Mage/Log/sql/log_setup/install-1.6.0.0.php +++ b/app/code/core/Mage/Log/sql/log_setup/install-1.6.0.0.php @@ -127,8 +127,8 @@ 'primary' => true, ), 'Type ID') ->addColumn('type_code', Varien_Db_Ddl_Table::TYPE_TEXT, 64, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Type Code') ->addColumn('period', Varien_Db_Ddl_Table::TYPE_SMALLINT, null, array( 'unsigned' => true, @@ -176,8 +176,8 @@ 'primary' => true, ), 'URL ID') ->addColumn('url', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'URL') ->addColumn('referer', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array( ), 'Referrer') @@ -196,8 +196,8 @@ 'primary' => true, ), 'Visitor ID') ->addColumn('session_id', Varien_Db_Ddl_Table::TYPE_TEXT, 64, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Session ID') ->addColumn('first_visit_at', Varien_Db_Ddl_Table::TYPE_TIMESTAMP, null, array( ), 'First Visit Time') diff --git a/app/code/core/Mage/Log/sql/log_setup/mysql4-upgrade-1.5.9.9-1.6.0.0.php b/app/code/core/Mage/Log/sql/log_setup/mysql4-upgrade-1.5.9.9-1.6.0.0.php index 3d7fc81487..9b91186bf2 100644 --- a/app/code/core/Mage/Log/sql/log_setup/mysql4-upgrade-1.5.9.9-1.6.0.0.php +++ b/app/code/core/Mage/Log/sql/log_setup/mysql4-upgrade-1.5.9.9-1.6.0.0.php @@ -218,8 +218,8 @@ 'url' => array( 'type' => Varien_Db_Ddl_Table::TYPE_TEXT, 'length' => 255, - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, 'comment' => 'URL' ), 'referer' => array( diff --git a/app/code/core/Mage/Newsletter/Model/Resource/Problem/Collection.php b/app/code/core/Mage/Newsletter/Model/Resource/Problem/Collection.php index faae66c0f3..9fc65ac49e 100755 --- a/app/code/core/Mage/Newsletter/Model/Resource/Problem/Collection.php +++ b/app/code/core/Mage/Newsletter/Model/Resource/Problem/Collection.php @@ -68,6 +68,7 @@ public function addSubscriberInfo() 'main_table.subscriber_id = subscriber.subscriber_id', array('subscriber_email','customer_id','subscriber_status') ); + $this->addFilterToMap('subscriber_id', 'main_table.subscriber_id'); $this->_subscribersInfoJoinedFlag = true; return $this; diff --git a/app/code/core/Mage/Newsletter/Model/Resource/Queue/Collection.php b/app/code/core/Mage/Newsletter/Model/Resource/Queue/Collection.php index 811710ce52..b16383edb6 100755 --- a/app/code/core/Mage/Newsletter/Model/Resource/Queue/Collection.php +++ b/app/code/core/Mage/Newsletter/Model/Resource/Queue/Collection.php @@ -82,24 +82,21 @@ public function addTemplateInfo() */ protected function _addSubscriberInfoToSelect() { - if (!$this->_addSubscribersFlag) { - $this->_addSubscribersFlag = true; - //Possibel solution with join select - $select = $this->getConnection()->select() - ->from(array('qlt' => $this->getTable('newsletter/queue_link')), 'COUNT(qlt.queue_link_id)') - ->where('qlt.queue_id = main_table.queue_id'); - $totalExpr = new Zend_Db_Expr(sprintf('(%s)', $select->assemble())); - $select = $this->getConnection()->select() - ->from(array('qls' => $this->getTable('newsletter/queue_link')), 'COUNT(qls.queue_link_id)') - ->where('qls.queue_id = main_table.queue_id') - ->where('qls.letter_sent_at IS NOT NULL'); - $sentExpr = new Zend_Db_Expr(sprintf('(%s)', $select->assemble())); - - $this->getSelect()->columns(array( - 'subscribers_sent' => $sentExpr, - 'subscribers_total' => $totalExpr - )); - } + /** @var $select Varien_Db_Select */ + $select = $this->getConnection()->select() + ->from(array('qlt' => $this->getTable('newsletter/queue_link')), 'COUNT(qlt.queue_link_id)') + ->where('qlt.queue_id = main_table.queue_id'); + $totalExpr = new Zend_Db_Expr(sprintf('(%s)', $select->assemble())); + $select = $this->getConnection()->select() + ->from(array('qls' => $this->getTable('newsletter/queue_link')), 'COUNT(qls.queue_link_id)') + ->where('qls.queue_id = main_table.queue_id') + ->where('qls.letter_sent_at IS NOT NULL'); + $sentExpr = new Zend_Db_Expr(sprintf('(%s)', $select->assemble())); + + $this->getSelect()->columns(array( + 'subscribers_sent' => $sentExpr, + 'subscribers_total' => $totalExpr + )); return $this; } diff --git a/app/code/core/Mage/Newsletter/data/newsletter_setup/data-upgrade-1.6.0.0-1.6.0.1.php b/app/code/core/Mage/Newsletter/data/newsletter_setup/data-upgrade-1.6.0.0-1.6.0.1.php new file mode 100644 index 0000000000..14b76128b0 --- /dev/null +++ b/app/code/core/Mage/Newsletter/data/newsletter_setup/data-upgrade-1.6.0.0-1.6.0.1.php @@ -0,0 +1,43 @@ +getTable('newsletter/subscriber'); + +$select = $installer->getConnection()->select() + ->from(array('main_table' => $subscriberTable)) + ->join( + array('customer' => $installer->getTable('customer/entity')), + 'main_table.customer_id = customer.entity_id', + array('website_id') + ) + ->where('customer.website_id = 0'); + +$installer->getConnection()->query( + $installer->getConnection()->deleteFromSelect($select, 'main_table') +); diff --git a/app/code/core/Mage/Newsletter/etc/config.xml b/app/code/core/Mage/Newsletter/etc/config.xml index 9c9b2e25d9..660540f165 100644 --- a/app/code/core/Mage/Newsletter/etc/config.xml +++ b/app/code/core/Mage/Newsletter/etc/config.xml @@ -28,7 +28,7 @@ - 1.6.0.0 + 1.6.0.1 diff --git a/app/code/core/Mage/Newsletter/sql/newsletter_setup/install-1.6.0.0.php b/app/code/core/Mage/Newsletter/sql/newsletter_setup/install-1.6.0.0.php index 76a679880a..7a80417e85 100644 --- a/app/code/core/Mage/Newsletter/sql/newsletter_setup/install-1.6.0.0.php +++ b/app/code/core/Mage/Newsletter/sql/newsletter_setup/install-1.6.0.0.php @@ -60,8 +60,8 @@ 'default' => '0', ), 'Customer Id') ->addColumn('subscriber_email', Varien_Db_Ddl_Table::TYPE_TEXT, 150, array( - 'nullable' => false, - 'default' => '', + 'nullable' => true, + 'default' => null, ), 'Subscriber Email') ->addColumn('subscriber_status', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array( 'nullable' => false, @@ -202,7 +202,8 @@ ->addForeignKey($installer->getFkName('newsletter/queue_link', 'queue_id', 'newsletter/queue', 'queue_id'), 'queue_id', $installer->getTable('newsletter/queue'), 'queue_id', Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE) - ->addForeignKey($installer->getFkName('newsletter/queue_link', 'subscriber_id', 'newsletter/subscriber', 'subscriber_id'), + ->addForeignKey( + $installer->getFkName('newsletter/queue_link', 'subscriber_id', 'newsletter/subscriber', 'subscriber_id'), 'subscriber_id', $installer->getTable('newsletter/subscriber'), 'subscriber_id', Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE) ->setComment('Newsletter Queue Link'); @@ -268,7 +269,8 @@ ->addForeignKey($installer->getFkName('newsletter/problem', 'queue_id', 'newsletter/queue', 'queue_id'), 'queue_id', $installer->getTable('newsletter/queue'), 'queue_id', Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE) - ->addForeignKey($installer->getFkName('newsletter/problem', 'subscriber_id', 'newsletter/subscriber', 'subscriber_id'), + ->addForeignKey( + $installer->getFkName('newsletter/problem', 'subscriber_id', 'newsletter/subscriber', 'subscriber_id'), 'subscriber_id', $installer->getTable('newsletter/subscriber'), 'subscriber_id', Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE) ->setComment('Newsletter Problems'); diff --git a/app/code/core/Mage/Paygate/Helper/Data.php b/app/code/core/Mage/Paygate/Helper/Data.php index b4f915be2c..9c13e888e7 100644 --- a/app/code/core/Mage/Paygate/Helper/Data.php +++ b/app/code/core/Mage/Paygate/Helper/Data.php @@ -51,8 +51,29 @@ public function convertMessagesToMessage($messages) * @param string $exception * @return bool|string */ - public function getTransactionMessage($payment, $requestType, $lastTransactionId, $card, $amount = false, $exception = false) - { + public function getTransactionMessage($payment, $requestType, $lastTransactionId, $card, $amount = false, + $exception = false + ) { + return $this->getExtendedTransactionMessage( + $payment, $requestType, $lastTransactionId, $card, $amount, $exception + ); + } + + /** + * Return message for gateway transaction request + * + * @param Mage_Payment_Model_Info $payment + * @param string $requestType + * @param string $lastTransactionId + * @param Varien_Object $card + * @param float $amount + * @param string $exception + * @param string $additionalMessage Custom message, which will be added to the end of generated message + * @return bool|string + */ + public function getExtendedTransactionMessage($payment, $requestType, $lastTransactionId, $card, $amount = false, + $exception = false, $additionalMessage = false + ) { $operation = $this->_getOperation($requestType); if (!$operation) { @@ -70,9 +91,23 @@ public function getTransactionMessage($payment, $requestType, $lastTransactionId } $card = $this->__('Credit Card: xxxx-%s', $card->getCcLast4()); - $transaction = $this->__('Authorize.Net Transaction ID %s', $lastTransactionId); - return $this->__('%s %s %s - %s. %s. %s', $card, $amount, $operation, $result, $transaction, $exception ); + $pattern = '%s %s %s - %s.'; + $texts = array($card, $amount, $operation, $result); + + if (!is_null($lastTransactionId)) { + $pattern .= ' %s.'; + $texts[] = $this->__('Authorize.Net Transaction ID %s', $lastTransactionId); + } + + if ($additionalMessage) { + $pattern .= ' %s.'; + $texts[] = $additionalMessage; + } + $pattern .= ' %s'; + $texts[] = $exception; + + return call_user_func_array(array($this, '__'), array_merge(array($pattern), $texts)); } /** @@ -98,7 +133,7 @@ protected function _getOperation($requestType) return false; } } - + /** * Format price with currency sign * @param Mage_Payment_Model_Info $payment diff --git a/app/code/core/Mage/Paygate/Model/Authorizenet.php b/app/code/core/Mage/Paygate/Model/Authorizenet.php index 76783007b8..312b3a2081 100644 --- a/app/code/core/Mage/Paygate/Model/Authorizenet.php +++ b/app/code/core/Mage/Paygate/Model/Authorizenet.php @@ -64,6 +64,7 @@ class Mage_Paygate_Model_Authorizenet extends Mage_Payment_Model_Method_Cc const RESPONSE_CODE_HELD = 4; const RESPONSE_REASON_CODE_APPROVED = 1; + const RESPONSE_REASON_CODE_NOT_FOUND = 16; const RESPONSE_REASON_CODE_PARTIAL_APPROVE = 295; const RESPONSE_REASON_CODE_PENDING_REVIEW_AUTHORIZED = 252; const RESPONSE_REASON_CODE_PENDING_REVIEW = 253; @@ -79,6 +80,8 @@ class Mage_Paygate_Model_Authorizenet extends Mage_Payment_Model_Method_Cc const METHOD_CODE = 'authorizenet'; + const TRANSACTION_STATUS_EXPIRED = 'expired'; + protected $_code = self::METHOD_CODE; /** @@ -828,6 +831,34 @@ protected function _voidCardTransaction($payment, $card) break; case self::RESPONSE_CODE_DECLINED: case self::RESPONSE_CODE_ERROR: + if ($result->getResponseReasonCode() == self::RESPONSE_REASON_CODE_NOT_FOUND + && $this->_isTransactionExpired($realAuthTransactionId) + ) { + $voidTransactionId = $realAuthTransactionId . '-void'; + return $this->_addTransaction( + $payment, + $voidTransactionId, + Mage_Sales_Model_Order_Payment_Transaction::TYPE_VOID, + array( + 'is_transaction_closed' => 1, + 'should_close_parent_transaction' => 1, + 'parent_transaction_id' => $authTransactionId + ), + array(), + Mage::helper('paygate')->getExtendedTransactionMessage( + $payment, + self::REQUEST_TYPE_VOID, + null, + $card, + false, + false, + Mage::helper('paygate')->__( + 'Parent Authorize.Net transaction (ID %s) expired', + $realAuthTransactionId + ) + ) + ); + } $exceptionMessage = $this->_wrapGatewayError($result->getResponseReasonText()); break; default: @@ -841,6 +872,18 @@ protected function _voidCardTransaction($payment, $card) Mage::throwException($exceptionMessage); } + /** + * Check if transaction is expired + * + * @param string $realAuthTransactionId + * @return bool + */ + protected function _isTransactionExpired($realAuthTransactionId) + { + $transactionDetails = $this->_getTransactionDetails($realAuthTransactionId); + return $transactionDetails->getTransactionStatus() == self::TRANSACTION_STATUS_EXPIRED; + } + /** * Refund the card transaction through gateway * @@ -1509,6 +1552,7 @@ protected function _getTransactionDetails($transactionId) $response ->setResponseCode((string)$responseXmlDocument->transaction->responseCode) ->setResponseReasonCode((string)$responseXmlDocument->transaction->responseReasonCode) + ->setTransactionStatus((string)$responseXmlDocument->transaction->transactionStatus) ; return $response; } diff --git a/app/code/core/Mage/Payment/Block/Form/Container.php b/app/code/core/Mage/Payment/Block/Form/Container.php index a54b4d6972..37e8c4a6ea 100644 --- a/app/code/core/Mage/Payment/Block/Form/Container.php +++ b/app/code/core/Mage/Payment/Block/Form/Container.php @@ -116,7 +116,7 @@ public function getMethods() $quote = $this->getQuote(); $store = $quote ? $quote->getStoreId() : null; $methods = $this->helper('payment')->getStoreMethods($store, $quote); - $total = $quote->getBaseSubtotal(); + $total = $quote->getBaseSubtotal() + $quote->getShippingAddress()->getBaseShippingAmount(); foreach ($methods as $key => $method) { if ($this->_canUseMethod($method) && ($total != 0 diff --git a/app/code/core/Mage/Payment/Block/Info/Cc.php b/app/code/core/Mage/Payment/Block/Info/Cc.php index dfac07e831..920e54b9e7 100644 --- a/app/code/core/Mage/Payment/Block/Info/Cc.php +++ b/app/code/core/Mage/Payment/Block/Info/Cc.php @@ -44,6 +44,16 @@ public function getCcTypeName() return (empty($ccType)) ? Mage::helper('payment')->__('N/A') : $ccType; } + /** + * Whether current payment method has credit card expiration info + * + * @return bool + */ + public function hasCcExpDate() + { + return (int)$this->getInfo()->getCcExpMonth() || (int)$this->getInfo()->getCcExpYear(); + } + /** * Retrieve CC expiration month * diff --git a/app/code/core/Mage/Payment/Model/Method/Abstract.php b/app/code/core/Mage/Payment/Model/Method/Abstract.php index 5d869c9f73..d4aac27bfa 100644 --- a/app/code/core/Mage/Payment/Model/Method/Abstract.php +++ b/app/code/core/Mage/Payment/Model/Method/Abstract.php @@ -297,7 +297,8 @@ public function canManageBillingAgreements() */ public function canManageRecurringProfiles() { - return $this->_canManageRecurringProfiles && ($this instanceof Mage_Payment_Model_Recurring_Profile_MethodInterface); + return $this->_canManageRecurringProfiles + && ($this instanceof Mage_Payment_Model_Recurring_Profile_MethodInterface); } /** @@ -318,7 +319,7 @@ protected function _getHelper() public function getCode() { if (empty($this->_code)) { - Mage::throwException($this->_getHelper()->__('Cannot retrieve the payment method code.')); + Mage::throwException(Mage::helper('payment')->__('Cannot retrieve the payment method code.')); } return $this->_code; } @@ -352,7 +353,7 @@ public function getInfoInstance() { $instance = $this->getData('info_instance'); if (!($instance instanceof Mage_Payment_Model_Info)) { - Mage::throwException($this->_getHelper()->__('Cannot retrieve the payment information object instance.')); + Mage::throwException(Mage::helper('payment')->__('Cannot retrieve the payment information object instance.')); } return $instance; } @@ -360,13 +361,12 @@ public function getInfoInstance() /** * Validate payment method information object * - * @param Varien_Object $info - * @return Mage_Payment_Model_Abstract + * @return Mage_Payment_Model_Abstract */ public function validate() { /** - * to validate paymene method is allowed for billing country or not + * to validate payment method is allowed for billing country or not */ $paymentInfo = $this->getInfoInstance(); if ($paymentInfo instanceof Mage_Sales_Model_Order_Payment) { @@ -375,51 +375,55 @@ public function validate() $billingCountry = $paymentInfo->getQuote()->getBillingAddress()->getCountryId(); } if (!$this->canUseForCountry($billingCountry)) { - Mage::throwException($this->_getHelper()->__('Selected payment type is not allowed for billing country.')); + Mage::throwException(Mage::helper('payment')->__('Selected payment type is not allowed for billing country.')); } return $this; } /** - * Order + * Order payment abstract method + * + * @param Varien_Object $payment + * @param float $amount * - * @param Varien_Object $orderPayment - * @return Mage_Payment_Model_Abstract + * @return Mage_Payment_Model_Abstract */ public function order(Varien_Object $payment, $amount) { if (!$this->canOrder()) { - Mage::throwException($this->_getHelper()->__('Order action is not available.')); + Mage::throwException(Mage::helper('payment')->__('Order action is not available.')); } return $this; } /** - * Authorize + * Authorize payment abstract method * - * @param Varien_Object $orderPayment + * @param Varien_Object $payment * @param float $amount - * @return Mage_Payment_Model_Abstract + * + * @return Mage_Payment_Model_Abstract */ public function authorize(Varien_Object $payment, $amount) { if (!$this->canAuthorize()) { - Mage::throwException($this->_getHelper()->__('Authorize action is not available.')); + Mage::throwException(Mage::helper('payment')->__('Authorize action is not available.')); } return $this; } /** - * Capture payment + * Capture payment abstract method * - * @param Varien_Object $orderPayment + * @param Varien_Object $payment * @param float $amount - * @return Mage_Payment_Model_Abstract + * + * @return Mage_Payment_Model_Abstract */ public function capture(Varien_Object $payment, $amount) { if (!$this->canCapture()) { - Mage::throwException($this->_getHelper()->__('Capture action is not available.')); + Mage::throwException(Mage::helper('payment')->__('Capture action is not available.')); } return $this; @@ -453,18 +457,18 @@ public function processBeforeRefund($invoice, $payment) } /** - * Refund money + * Refund specified amount for payment * - * @param Varien_Object $invoicePayment + * @param Varien_Object $payment * @param float $amount - * @return Mage_Payment_Model_Abstract + * + * @return Mage_Payment_Model_Abstract */ - //public function refund(Varien_Object $payment, $amount) public function refund(Varien_Object $payment, $amount) { if (!$this->canRefund()) { - Mage::throwException($this->_getHelper()->__('Refund action is not available.')); + Mage::throwException(Mage::helper('payment')->__('Refund action is not available.')); } @@ -484,10 +488,11 @@ public function processCreditmemo($creditmemo, $payment) } /** - * Cancel payment (GoogleCheckout) + * Cancel payment abstract method + * + * @param Varien_Object $payment * - * @param Varien_Object $invoicePayment - * @return Mage_Payment_Model_Abstract + * @return Mage_Payment_Model_Abstract */ public function cancel(Varien_Object $payment) { @@ -509,15 +514,16 @@ public function processBeforeVoid($invoice, $payment) } /** - * Void payment + * Void payment abstract method + * + * @param Varien_Object $payment * - * @param Varien_Object $invoicePayment - * @return Mage_Payment_Model_Abstract + * @return Mage_Payment_Model_Abstract */ public function void(Varien_Object $payment) { if (!$this->canVoid($payment)) { - Mage::throwException($this->_getHelper()->__('Void action is not available.')); + Mage::throwException(Mage::helper('payment')->__('Void action is not available.')); } return $this; } @@ -526,7 +532,7 @@ public function void(Varien_Object $payment) * Whether this method can accept or deny payment * * @param Mage_Payment_Model_Info $payment - * @param bool $soft + * * @return bool */ public function canReviewPayment(Mage_Payment_Model_Info $payment) @@ -577,8 +583,10 @@ public function getTitle() /** * Retrieve information from payment configuration * - * @param string $field - * @return mixed + * @param string $field + * @param int|string|null|Mage_Core_Model_Store $storeId + * + * @return mixed */ public function getConfigData($field, $storeId = null) { @@ -618,8 +626,11 @@ public function prepareSave() /** * Check whether payment method can be used + * * TODO: payment method instance is not supposed to know about quote - * @param Mage_Sales_Model_Quote + * + * @param Mage_Sales_Model_Quote $quote + * * @return bool */ public function isAvailable($quote = null) @@ -645,10 +656,12 @@ public function isAvailable($quote = null) /** * Method that will be executed instead of authorize or capture - * if flag isInitilizeNeeded set to true + * if flag isInitializeNeeded set to true * - * @param string $paymentAction - * @return Mage_Payment_Model_Abstract + * @param string $paymentAction + * @param object $stateObject + * + * @return Mage_Payment_Model_Abstract */ public function initialize($paymentAction, $stateObject) { @@ -656,7 +669,7 @@ public function initialize($paymentAction, $stateObject) } /** - * Get config peyment action url + * Get config payment action url * Used to universalize payment actions when processing payment place * * @return string diff --git a/app/code/core/Mage/Payment/Model/Method/Cc.php b/app/code/core/Mage/Payment/Model/Method/Cc.php index b20a9945ce..861f45329b 100644 --- a/app/code/core/Mage/Payment/Model/Method/Cc.php +++ b/app/code/core/Mage/Payment/Model/Method/Cc.php @@ -107,14 +107,29 @@ public function validate() $ccType = 'OT'; $ccTypeRegExpList = array( //Solo, Switch or Maestro. International safe - //'SS' => '/^((6759[0-9]{12})|(6334|6767[0-9]{12})|(6334|6767[0-9]{14,15})|(5018|5020|5038|6304|6759|6761|6763[0-9]{12,19})|(49[013][1356][0-9]{12})|(633[34][0-9]{12})|(633110[0-9]{10})|(564182[0-9]{10}))([0-9]{2,3})?$/', // Maestro / Solo - 'SO' => '/(^(6334)[5-9](\d{11}$|\d{13,14}$))|(^(6767)(\d{12}$|\d{14,15}$))/', // Solo only - 'SM' => '/(^(5[0678])\d{11,18}$)|(^(6[^05])\d{11,18}$)|(^(601)[^1]\d{9,16}$)|(^(6011)\d{9,11}$)|(^(6011)\d{13,16}$)|(^(65)\d{11,13}$)|(^(65)\d{15,18}$)|(^(49030)[2-9](\d{10}$|\d{12,13}$))|(^(49033)[5-9](\d{10}$|\d{12,13}$))|(^(49110)[1-2](\d{10}$|\d{12,13}$))|(^(49117)[4-9](\d{10}$|\d{12,13}$))|(^(49118)[0-2](\d{10}$|\d{12,13}$))|(^(4936)(\d{12}$|\d{14,15}$))/', - 'VI' => '/^4[0-9]{12}([0-9]{3})?$/', // Visa - 'MC' => '/^5[1-5][0-9]{14}$/', // Master Card - 'AE' => '/^3[47][0-9]{13}$/', // American Express - 'DI' => '/^6011[0-9]{12}$/', // Discovery - 'JCB' => '/^(3[0-9]{15}|(2131|1800)[0-9]{11})$/', // JCB + /* + // Maestro / Solo + 'SS' => '/^((6759[0-9]{12})|(6334|6767[0-9]{12})|(6334|6767[0-9]{14,15})' + . '|(5018|5020|5038|6304|6759|6761|6763[0-9]{12,19})|(49[013][1356][0-9]{12})' + . '|(633[34][0-9]{12})|(633110[0-9]{10})|(564182[0-9]{10}))([0-9]{2,3})?$/', + */ + // Solo only + 'SO' => '/(^(6334)[5-9](\d{11}$|\d{13,14}$))|(^(6767)(\d{12}$|\d{14,15}$))/', + 'SM' => '/(^(5[0678])\d{11,18}$)|(^(6[^05])\d{11,18}$)|(^(601)[^1]\d{9,16}$)|(^(6011)\d{9,11}$)' + . '|(^(6011)\d{13,16}$)|(^(65)\d{11,13}$)|(^(65)\d{15,18}$)' + . '|(^(49030)[2-9](\d{10}$|\d{12,13}$))|(^(49033)[5-9](\d{10}$|\d{12,13}$))' + . '|(^(49110)[1-2](\d{10}$|\d{12,13}$))|(^(49117)[4-9](\d{10}$|\d{12,13}$))' + . '|(^(49118)[0-2](\d{10}$|\d{12,13}$))|(^(4936)(\d{12}$|\d{14,15}$))/', + // Visa + 'VI' => '/^4[0-9]{12}([0-9]{3})?$/', + // Master Card + 'MC' => '/^5[1-5][0-9]{14}$/', + // American Express + 'AE' => '/^3[47][0-9]{13}$/', + // Discovery + 'DI' => '/^6011[0-9]{12}$/', + // JCB + 'JCB' => '/^(3[0-9]{15}|(2131|1800)[0-9]{11})$/' ); foreach ($ccTypeRegExpList as $ccTypeMatch=>$ccTypeRegExp) { @@ -125,19 +140,16 @@ public function validate() } if (!$this->OtherCcType($info->getCcType()) && $ccType!=$info->getCcType()) { - $errorCode = 'ccsave_cc_type,ccsave_cc_number'; - $errorMsg = $this->_getHelper()->__('Credit card number mismatch with credit card type.'); + $errorMsg = Mage::helper('payment')->__('Credit card number mismatch with credit card type.'); } } else { - $errorCode = 'ccsave_cc_number'; - $errorMsg = $this->_getHelper()->__('Invalid Credit Card Number'); + $errorMsg = Mage::helper('payment')->__('Invalid Credit Card Number'); } } else { - $errorCode = 'ccsave_cc_type'; - $errorMsg = $this->_getHelper()->__('Credit card type is not allowed for this payment method.'); + $errorMsg = Mage::helper('payment')->__('Credit card type is not allowed for this payment method.'); } //validate credit card verification number @@ -145,18 +157,16 @@ public function validate() $verifcationRegEx = $this->getVerificationRegEx(); $regExp = isset($verifcationRegEx[$info->getCcType()]) ? $verifcationRegEx[$info->getCcType()] : ''; if (!$info->getCcCid() || !$regExp || !preg_match($regExp ,$info->getCcCid())){ - $errorMsg = $this->_getHelper()->__('Please enter a valid credit card verification number.'); + $errorMsg = Mage::helper('payment')->__('Please enter a valid credit card verification number.'); } } if ($ccType != 'SS' && !$this->_validateExpDate($info->getCcExpYear(), $info->getCcExpMonth())) { - $errorCode = 'ccsave_expiration,ccsave_expiration_yr'; - $errorMsg = $this->_getHelper()->__('Incorrect credit card expiration date.'); + $errorMsg = Mage::helper('payment')->__('Incorrect credit card expiration date.'); } if($errorMsg){ Mage::throwException($errorMsg); - //throw Mage::exception('Mage_Payment', $errorMsg, $errorCode); } //This must be after all validation conditions @@ -195,7 +205,9 @@ public function getVerificationRegEx() protected function _validateExpDate($expYear, $expMonth) { $date = Mage::app()->getLocale()->date(); - if (!$expYear || !$expMonth || ($date->compareYear($expYear)==1) || ($date->compareYear($expYear) == 0 && ($date->compareMonth($expMonth)==1 ) )) { + if (!$expYear || !$expMonth || ($date->compareYear($expYear) == 1) + || ($date->compareYear($expYear) == 0 && ($date->compareMonth($expMonth) == 1)) + ) { return false; } return true; diff --git a/app/code/core/Mage/Payment/Model/Method/Free.php b/app/code/core/Mage/Payment/Model/Method/Free.php index c77c6b227f..0ca45647aa 100644 --- a/app/code/core/Mage/Payment/Model/Method/Free.php +++ b/app/code/core/Mage/Payment/Model/Method/Free.php @@ -46,7 +46,6 @@ class Mage_Payment_Model_Method_Free extends Mage_Payment_Model_Method_Abstract * @var bool */ protected $_canAuthorize = true; - protected $_canCapture = true; /** * Payment code name diff --git a/app/code/core/Mage/Payment/etc/system.xml b/app/code/core/Mage/Payment/etc/system.xml index 977c4d7063..8cd6b1bfde 100644 --- a/app/code/core/Mage/Payment/etc/system.xml +++ b/app/code/core/Mage/Payment/etc/system.xml @@ -79,6 +79,7 @@ 1 1 0 + validate-number <label>Title</label> @@ -202,6 +203,7 @@ <show_in_default>1</show_in_default> <show_in_website>1</show_in_website> <show_in_store>0</show_in_store> + <frontend_class>validate-number</frontend_class> </sort_order> <title translate="label"> <label>Title</label> @@ -307,6 +309,7 @@ <show_in_default>1</show_in_default> <show_in_website>1</show_in_website> <show_in_store>0</show_in_store> + <frontend_class>validate-number</frontend_class> </sort_order> <title translate="label"> <label>Title</label> @@ -390,6 +393,7 @@ <show_in_default>1</show_in_default> <show_in_website>1</show_in_website> <show_in_store>0</show_in_store> + <frontend_class>validate-number</frontend_class> </sort_order> <title translate="label"> <label>Title</label> diff --git a/app/code/core/Mage/Paypal/Block/Adminhtml/System/Config/Payflowlink/Advanced.php b/app/code/core/Mage/Paypal/Block/Adminhtml/System/Config/Payflowlink/Advanced.php new file mode 100644 index 0000000000..1767bc98db --- /dev/null +++ b/app/code/core/Mage/Paypal/Block/Adminhtml/System/Config/Payflowlink/Advanced.php @@ -0,0 +1,43 @@ +<?php +/** + * Magento + * + * NOTICE OF LICENSE + * + * This source file is subject to the Open Software License (OSL 3.0) + * that is bundled with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://opensource.org/licenses/osl-3.0.php + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@magentocommerce.com so we can send you a copy immediately. + * + * DISCLAIMER + * + * Do not edit or add to this file if you wish to upgrade Magento to newer + * versions in the future. If you wish to customize Magento for your + * needs please refer to http://www.magentocommerce.com for more information. + * + * @category Mage + * @package Mage_Paypal + * @copyright Copyright (c) 2011 Magento Inc. (http://www.magentocommerce.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ + +/** + * Renderer for Payflow Link information + * + * @category Mage + * @package Mage_Paypal + * @author Magento Core Team <core@magentocommerce.com> + */ + class Mage_Paypal_Block_Adminhtml_System_Config_Payflowlink_Advanced + extends Mage_Paypal_Block_Adminhtml_System_Config_Payflowlink_Info +{ + /** + * Template path + * + * @var string + */ + protected $_template = 'paypal/system/config/payflowlink/advanced.phtml'; +} diff --git a/app/code/core/Mage/Paypal/Block/Iframe.php b/app/code/core/Mage/Paypal/Block/Iframe.php index bcc3bba935..35ba60256b 100644 --- a/app/code/core/Mage/Paypal/Block/Iframe.php +++ b/app/code/core/Mage/Paypal/Block/Iframe.php @@ -75,8 +75,9 @@ protected function _construct() ->getMethod(); if (in_array($paymentCode, $this->helper('paypal/hss')->getHssMethods())) { $this->_paymentMethodCode = $paymentCode; + $template_path = str_replace('_', '', $paymentCode); + $this->setTemplate("paypal/{$template_path}/iframe.phtml"); } - $this->setTemplate('paypal/hss/iframe.phtml'); } /** diff --git a/app/code/core/Mage/Paypal/Block/Payflow/Advanced/Form.php b/app/code/core/Mage/Paypal/Block/Payflow/Advanced/Form.php new file mode 100644 index 0000000000..33999784ee --- /dev/null +++ b/app/code/core/Mage/Paypal/Block/Payflow/Advanced/Form.php @@ -0,0 +1,56 @@ +<?php +/** + * Magento + * + * NOTICE OF LICENSE + * + * This source file is subject to the Open Software License (OSL 3.0) + * that is bundled with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://opensource.org/licenses/osl-3.0.php + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@magentocommerce.com so we can send you a copy immediately. + * + * DISCLAIMER + * + * Do not edit or add to this file if you wish to upgrade Magento to newer + * versions in the future. If you wish to customize Magento for your + * needs please refer to http://www.magentocommerce.com for more information. + * + * @category Mage + * @package Mage_Paypal + * @copyright Copyright (c) 2011 Magento Inc. (http://www.magentocommerce.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ + +/** + * Payflow Advanced iframe block + * + * @category Mage + * @package Mage_Paypal + * @author Magento Core Team <core@magentocommerce.com> + */ +class Mage_Paypal_Block_Payflow_Advanced_Form extends Mage_Paypal_Block_Payflow_Link_Form +{ + /** + * Internal constructor + * Set payment method code + * + */ + protected function _construct() + { + parent::_construct(); + $this->setTemplate('paypal/payflowadvanced/info.phtml'); + } + + /** + * Get frame action URL + * + * @return string + */ + public function getFrameActionUrl() + { + return $this->getUrl('paypal/payflowadvanced/form', array('_secure' => true)); + } +} diff --git a/app/code/core/Mage/Paypal/Block/Payflow/Advanced/Iframe.php b/app/code/core/Mage/Paypal/Block/Payflow/Advanced/Iframe.php new file mode 100644 index 0000000000..c66b35e5ed --- /dev/null +++ b/app/code/core/Mage/Paypal/Block/Payflow/Advanced/Iframe.php @@ -0,0 +1,67 @@ +<?php +/** + * Magento + * + * NOTICE OF LICENSE + * + * This source file is subject to the Open Software License (OSL 3.0) + * that is bundled with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://opensource.org/licenses/osl-3.0.php + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@magentocommerce.com so we can send you a copy immediately. + * + * DISCLAIMER + * + * Do not edit or add to this file if you wish to upgrade Magento to newer + * versions in the future. If you wish to customize Magento for your + * needs please refer to http://www.magentocommerce.com for more information. + * + * @category Mage + * @package Mage_Paypal + * @copyright Copyright (c) 2011 Magento Inc. (http://www.magentocommerce.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ + +/** + * Payflow Advanced iframe block + * + * @category Mage + * @package Mage_Paypal + * @author Magento Core Team <core@magentocommerce.com> + */ +class Mage_Paypal_Block_Payflow_Advanced_Iframe extends Mage_Paypal_Block_Payflow_Link_Iframe +{ + /** + * Set payment method code + */ + protected function _construct() + { + parent::_construct(); + $this->_paymentMethodCode = Mage_Paypal_Model_Config::METHOD_PAYFLOWADVANCED; + } + + /** + * Get frame action URL + * + * @return string + */ + public function getFrameActionUrl() + { + return $this->getUrl('paypal/payflowadvanced/form', array('_secure' => true)); + } + + /** + * Check sandbox mode + * + * @return bool + */ + public function isTestMode() + { + $mode = Mage::helper('payment') + ->getMethodInstance(Mage_Paypal_Model_Config::METHOD_PAYFLOWADVANCED) + ->getConfigData('sandbox_flag'); + return (bool) $mode; + } +} diff --git a/app/code/core/Mage/Paypal/Block/Payflow/Advanced/Info.php b/app/code/core/Mage/Paypal/Block/Payflow/Advanced/Info.php new file mode 100644 index 0000000000..0fa276f768 --- /dev/null +++ b/app/code/core/Mage/Paypal/Block/Payflow/Advanced/Info.php @@ -0,0 +1,37 @@ +<?php +/** + * Magento + * + * NOTICE OF LICENSE + * + * This source file is subject to the Open Software License (OSL 3.0) + * that is bundled with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://opensource.org/licenses/osl-3.0.php + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@magentocommerce.com so we can send you a copy immediately. + * + * DISCLAIMER + * + * Do not edit or add to this file if you wish to upgrade Magento to newer + * versions in the future. If you wish to customize Magento for your + * needs please refer to http://www.magentocommerce.com for more information. + * + * @category Mage + * @package Mage_Paypal + * @copyright Copyright (c) 2011 Magento Inc. (http://www.magentocommerce.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ + +/** + * Payflow Advanced infoblock + * + * @category Mage + * @package Mage_Paypal + * @author Magento Core Team <core@magentocommerce.com> + */ +class Mage_Paypal_Block_Payflow_Advanced_Info extends Mage_Paypal_Block_Payflow_Link_Info +{ + +} diff --git a/app/code/core/Mage/Paypal/Block/Payflow/Advanced/Review.php b/app/code/core/Mage/Paypal/Block/Payflow/Advanced/Review.php new file mode 100644 index 0000000000..0686af95a7 --- /dev/null +++ b/app/code/core/Mage/Paypal/Block/Payflow/Advanced/Review.php @@ -0,0 +1,38 @@ +<?php +/** + * Magento + * + * NOTICE OF LICENSE + * + * This source file is subject to the Open Software License (OSL 3.0) + * that is bundled with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://opensource.org/licenses/osl-3.0.php + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@magentocommerce.com so we can send you a copy immediately. + * + * DISCLAIMER + * + * Do not edit or add to this file if you wish to upgrade Magento to newer + * versions in the future. If you wish to customize Magento for your + * needs please refer to http://www.magentocommerce.com for more information. + * + * @category Mage + * @package Mage_Paypal + * @copyright Copyright (c) 2011 Magento Inc. (http://www.magentocommerce.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ + +/** + * Paypal Payflow Advanced Express Onepage checkout block + * + * @deprecated since 1.6.2.0 + * @category Mage + * @package Mage_Paypal + * @author Magento Core Team <core@magentocommerce.com> + */ +class Mage_Paypal_Block_Payflow_Advanced_Review extends Mage_Paypal_Block_Payflow_Link_Review +{ + +} diff --git a/app/code/core/Mage/Paypal/Block/Payflow/Link/Form.php b/app/code/core/Mage/Paypal/Block/Payflow/Link/Form.php index 09838bc757..bd84e7b001 100644 --- a/app/code/core/Mage/Paypal/Block/Payflow/Link/Form.php +++ b/app/code/core/Mage/Paypal/Block/Payflow/Link/Form.php @@ -41,7 +41,7 @@ class Mage_Paypal_Block_Payflow_Link_Form extends Mage_Payment_Block_Form protected function _construct() { parent::_construct(); - $this->setTemplate('paypal/payflowlink/iframe.phtml'); + $this->setTemplate('paypal/payflowlink/info.phtml'); } /** diff --git a/app/code/core/Mage/Paypal/Block/Payflow/Link/Iframe.php b/app/code/core/Mage/Paypal/Block/Payflow/Link/Iframe.php index 62b2c55c63..d3b6866e4e 100644 --- a/app/code/core/Mage/Paypal/Block/Payflow/Link/Iframe.php +++ b/app/code/core/Mage/Paypal/Block/Payflow/Link/Iframe.php @@ -31,8 +31,17 @@ * @package Mage_Paypal * @author Magento Core Team <core@magentocommerce.com> */ -class Mage_Paypal_Block_Payflow_Link_Iframe extends Mage_Payment_Block_Form +class Mage_Paypal_Block_Payflow_Link_Iframe extends Mage_Paypal_Block_Iframe { + /** + * Set payment method code + */ + protected function _construct() + { + parent::_construct(); + $this->_paymentMethodCode = Mage_Paypal_Model_Config::METHOD_PAYFLOWLINK; + } + /** * Get frame action URL * @@ -50,7 +59,7 @@ public function getFrameActionUrl() */ public function getSecureToken() { - return $this->_getSalesDocument() + return $this->_getOrder() ->getPayment() ->getAdditionalInformation('secure_token'); } @@ -62,7 +71,7 @@ public function getSecureToken() */ public function getSecureTokenId() { - return $this->_getSalesDocument() + return $this->_getOrder() ->getPayment() ->getAdditionalInformation('secure_token_id'); } @@ -85,18 +94,8 @@ public function getTransactionUrl() public function isTestMode() { $mode = Mage::helper('payment') - ->getMethodInstance(Mage_Paypal_Model_Config::METHOD_PAYFLOWLINK) + ->getMethodInstance($this->_paymentMethodCode) ->getConfigData('sandbox_flag'); return (bool) $mode; } - - /** - * Get sales document object - * - * @return Mage_Sales_Model_Order|Mage_Sales_Model_Quote - */ - protected function _getSalesDocument() - { - return Mage::getSingleton('checkout/session')->getQuote(); - } } diff --git a/app/code/core/Mage/Paypal/Block/Payflow/Link/Review.php b/app/code/core/Mage/Paypal/Block/Payflow/Link/Review.php new file mode 100644 index 0000000000..5e4747305c --- /dev/null +++ b/app/code/core/Mage/Paypal/Block/Payflow/Link/Review.php @@ -0,0 +1,47 @@ +<?php +/** + * Magento + * + * NOTICE OF LICENSE + * + * This source file is subject to the Open Software License (OSL 3.0) + * that is bundled with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://opensource.org/licenses/osl-3.0.php + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@magentocommerce.com so we can send you a copy immediately. + * + * DISCLAIMER + * + * Do not edit or add to this file if you wish to upgrade Magento to newer + * versions in the future. If you wish to customize Magento for your + * needs please refer to http://www.magentocommerce.com for more information. + * + * @category Mage + * @package Mage_Paypal + * @copyright Copyright (c) 2011 Magento Inc. (http://www.magentocommerce.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ + +/** + * Paypal PayflowLink Express Onepage checkout block + * + * @deprecated since 1.6.2.0 + * @category Mage + * @package Mage_Paypal + * @author Magento Core Team <core@magentocommerce.com> + */ +class Mage_Paypal_Block_Payflow_Link_Review extends Mage_Paypal_Block_Express_Review +{ + + /** + * Retrieve payment method and assign additional template values + * + * @return Mage_Paypal_Block_Express_Review + */ + protected function _beforeToHtml() + { + return parent::_beforeToHtml(); + } +} diff --git a/app/code/core/Mage/Paypal/Controller/Express/Abstract.php b/app/code/core/Mage/Paypal/Controller/Express/Abstract.php index 8dd3da2e1e..a661171277 100644 --- a/app/code/core/Mage/Paypal/Controller/Express/Abstract.php +++ b/app/code/core/Mage/Paypal/Controller/Express/Abstract.php @@ -69,7 +69,7 @@ public function startAction() $customer = Mage::getSingleton('customer/session')->getCustomer(); if ($customer && $customer->getId()) { $this->_checkout->setCustomerWithAddressChange( - $customer, null, $this->_getQuote()->getShippingAddress() + $customer, $this->_getQuote()->getBillingAddress(), $this->_getQuote()->getShippingAddress() ); } diff --git a/app/code/core/Mage/Paypal/Helper/Hss.php b/app/code/core/Mage/Paypal/Helper/Hss.php index 569f3af2fe..7764604705 100644 --- a/app/code/core/Mage/Paypal/Helper/Hss.php +++ b/app/code/core/Mage/Paypal/Helper/Hss.php @@ -35,7 +35,9 @@ class Mage_Paypal_Helper_Hss extends Mage_Core_Helper_Abstract * @var array */ protected $_hssMethods = array( - Mage_Paypal_Model_Config::METHOD_HOSTEDPRO + Mage_Paypal_Model_Config::METHOD_HOSTEDPRO, + Mage_Paypal_Model_Config::METHOD_PAYFLOWLINK, + Mage_Paypal_Model_Config::METHOD_PAYFLOWADVANCED ); /** diff --git a/app/code/core/Mage/Paypal/Model/Api/Nvp.php b/app/code/core/Mage/Paypal/Model/Api/Nvp.php index 54bbb7ec2e..f3783313eb 100644 --- a/app/code/core/Mage/Paypal/Model/Api/Nvp.php +++ b/app/code/core/Mage/Paypal/Model/Api/Nvp.php @@ -950,8 +950,6 @@ public function call($methodName, array $request) throw $e; } - $http->close(); - $response = preg_split('/^\r?$/m', $response, 2); $response = trim($response[1]); $response = $this->_deformatNVP($response); @@ -964,9 +962,13 @@ public function call($methodName, array $request) Mage::logException(new Exception( sprintf('PayPal NVP CURL connection error #%s: %s', $http->getErrno(), $http->getError()) )); + $http->close(); + Mage::throwException(Mage::helper('paypal')->__('Unable to communicate with the PayPal gateway.')); } + // cUrl resource must be closed after checking it for errors + $http->close(); if (!$this->_validateResponse($methodName, $response)) { Mage::logException(new Exception( @@ -1036,6 +1038,10 @@ protected function _handleCallErrors($response) */ protected function _isCallSuccessful($response) { + if (!isset($response['ACK'])) { + return false; + } + $ack = strtoupper($response['ACK']); $this->_callWarnings = array(); if ($ack == 'SUCCESS' || $ack == 'SUCCESSWITHWARNING') { diff --git a/app/code/core/Mage/Paypal/Model/Cart.php b/app/code/core/Mage/Paypal/Model/Cart.php index ab165bb909..8254ebfef4 100644 --- a/app/code/core/Mage/Paypal/Model/Cart.php +++ b/app/code/core/Mage/Paypal/Model/Cart.php @@ -213,7 +213,7 @@ public function addItem($name, $qty, $amount, $identifier = null) /** * Remove item from cart by identifier - * + * * @param string $identifier * @return bool */ @@ -335,12 +335,6 @@ protected function _render() ); } - $this->_validate(); - // if cart items are invalid, prepare cart for transfer without line items - if (!$this->_areItemsValid) { - $this->removeItem($shippingItemId); - } - // compound non-regular items into subtotal foreach ($this->_items as $key => $item) { if ($key > $lastRegularItemKey && $item->getAmount() != 0) { @@ -348,6 +342,12 @@ protected function _render() } } + $this->_validate(); + // if cart items are invalid, prepare cart for transfer without line items + if (!$this->_areItemsValid) { + $this->removeItem($shippingItemId); + } + $this->_shouldRender = false; } diff --git a/app/code/core/Mage/Paypal/Model/Config.php b/app/code/core/Mage/Paypal/Model/Config.php index e0f4a267a2..2adf45ea60 100644 --- a/app/code/core/Mage/Paypal/Model/Config.php +++ b/app/code/core/Mage/Paypal/Model/Config.php @@ -64,13 +64,14 @@ class Mage_Paypal_Model_Config * Payflow Pro Gateway * @var string */ - const METHOD_PAYFLOWPRO = 'verisign'; + const METHOD_PAYFLOWPRO = 'verisign'; - const METHOD_PAYFLOWLINK = 'payflow_link'; + const METHOD_PAYFLOWLINK = 'payflow_link'; + const METHOD_PAYFLOWADVANCED = 'payflow_advanced'; - const METHOD_HOSTEDPRO = 'hosted_pro'; + const METHOD_HOSTEDPRO = 'hosted_pro'; - const METHOD_BILLING_AGREEMENT = 'paypal_billing_agreement'; + const METHOD_BILLING_AGREEMENT = 'paypal_billing_agreement'; /** * Buttons and images @@ -97,6 +98,8 @@ class Mage_Paypal_Model_Config /** * Authorization amounts for Account Verification + * + * @deprecated since 1.6.2.0 * @var int */ const AUTHORIZATION_AMOUNT_ZERO = 0; @@ -481,6 +484,7 @@ public function getCountryMethods($countryCode = null) self::METHOD_WPP_PE_EXPRESS, self::METHOD_PAYFLOWPRO, self::METHOD_PAYFLOWLINK, + self::METHOD_PAYFLOWADVANCED, ), 'CA' => array( self::METHOD_WPS, @@ -871,16 +875,12 @@ public function getPaymentAction() /** * Returns array of possible Authorization Amounts for Account Verification * + * @deprecated since 1.6.2.0 * @return array */ public function getAuthorizationAmounts() { - $authorizationAmount = array( - self::AUTHORIZATION_AMOUNT_ZERO => Mage::helper('paypal')->__('$0 Auth'), - self::AUTHORIZATION_AMOUNT_ONE => Mage::helper('paypal')->__('$1 Auth'), - self::AUTHORIZATION_AMOUNT_FULL => Mage::helper('paypal')->__('Full Auth'), - ); - return $authorizationAmount; + return array(); } /** @@ -1004,6 +1004,7 @@ public static function getIsCreditCardMethod($code) case self::METHOD_WPP_PE_DIRECT: case self::METHOD_PAYFLOWPRO: case self::METHOD_PAYFLOWLINK: + case self::METHOD_PAYFLOWADVANCED: case self::METHOD_HOSTEDPRO: return true; } diff --git a/app/code/core/Mage/Paypal/Model/Ipn.php b/app/code/core/Mage/Paypal/Model/Ipn.php index e9da8a45db..f84321fbb4 100644 --- a/app/code/core/Mage/Paypal/Model/Ipn.php +++ b/app/code/core/Mage/Paypal/Model/Ipn.php @@ -132,7 +132,7 @@ protected function _postBack(Zend_Http_Client_Adapter_Interface $httpAdapter) { $sReq = ''; foreach ($this->_request as $k => $v) { - $sReq .= '&'.$k.'='.urlencode(stripslashes($v)); + $sReq .= '&'.$k.'='.urlencode($v); } $sReq .= "&cmd=_notify-validate"; $sReq = substr($sReq, 1); @@ -170,7 +170,12 @@ protected function _getOrder() $id = $this->_request['invoice']; $this->_order = Mage::getModel('sales/order')->loadByIncrementId($id); if (!$this->_order->getId()) { - throw new Exception(sprintf('Wrong order ID: "%s".', $id)); + $this->_debugData['exception'] = sprintf('Wrong order ID: "%s".', $id); + $this->_debug(); + Mage::app()->getResponse() + ->setHeader('HTTP/1.1','503 Service Unavailable') + ->sendResponse(); + exit; } // re-initialize config with the method code and store id $methodCode = $this->_order->getPayment()->getMethod(); diff --git a/app/code/core/Mage/Paypal/Model/Observer.php b/app/code/core/Mage/Paypal/Model/Observer.php index 7b2a5bc7ea..29aefce254 100644 --- a/app/code/core/Mage/Paypal/Model/Observer.php +++ b/app/code/core/Mage/Paypal/Model/Observer.php @@ -57,35 +57,12 @@ public function fetchReports() /** * Clean unfinished transaction * + * @deprecated since 1.6.2.0 * @return Mage_Paypal_Model_Observer */ public function cleanTransactions() { - /** @var $date Mage_Core_Model_Date */ - $date = Mage::getModel('core/date'); - $createdBefore = strtotime('-1 hour', $date->timestamp()); - - /** @var $collection Mage_Paypal_Model_Resource_Payment_Transaction_Collection */ - $collection = Mage::getModel('paypal/payment_transaction')->getCollection(); - $collection->addCreatedBeforeFilter($date->gmtDate(null, $createdBefore)); - - /** @var $method Mage_Paypal_Model_Payflowlink */ - $method = Mage::helper('payment')->getMethodInstance(Mage_Paypal_Model_Config::METHOD_PAYFLOWLINK); - - /** @var $item Mage_Paypal_Model_Payment_Transaction */ - foreach ($collection as $item) { - try { - $method->void(new Varien_Object(array( - 'transaction_id' => $item->getTxnId(), - 'store' => $item->getAdditionalInformation('store_id') - ))); - $item->delete(); - } catch (Mage_Paypal_Exception $e) { - $item->delete(); - } catch (Exception $e) { - Mage::logException($e); - } - } + return $this; } /** diff --git a/app/code/core/Mage/Paypal/Model/Payflowadvanced.php b/app/code/core/Mage/Paypal/Model/Payflowadvanced.php new file mode 100644 index 0000000000..3f6ee0becb --- /dev/null +++ b/app/code/core/Mage/Paypal/Model/Payflowadvanced.php @@ -0,0 +1,57 @@ +<?php +/** + * Magento + * + * NOTICE OF LICENSE + * + * This source file is subject to the Open Software License (OSL 3.0) + * that is bundled with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://opensource.org/licenses/osl-3.0.php + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@magentocommerce.com so we can send you a copy immediately. + * + * DISCLAIMER + * + * Do not edit or add to this file if you wish to upgrade Magento to newer + * versions in the future. If you wish to customize Magento for your + * needs please refer to http://www.magentocommerce.com for more information. + * + * @category Mage + * @package Mage_Paypal + * @copyright Copyright (c) 2011 Magento Inc. (http://www.magentocommerce.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ + +/** + * Payments Advanced gateway model + * + * @category Mage + * @package Mage_Paypal + * @author Magento Core Team <core@magentocommerce.com> + */ + +class Mage_Paypal_Model_Payflowadvanced extends Mage_Paypal_Model_Payflowlink +{ + /** + * Payment method code + * + * @var string + */ + protected $_code = Mage_Paypal_Model_Config::METHOD_PAYFLOWADVANCED; + + /** + * Type of block that generates method form + * + * @var string + */ + protected $_formBlockType = 'paypal/payflow_advanced_form'; + + /** + * Type of block that displays method information + * + * @var string + */ + protected $_infoBlockType = 'paypal/payflow_advanced_info'; +} diff --git a/app/code/core/Mage/Paypal/Model/Payflowlink.php b/app/code/core/Mage/Paypal/Model/Payflowlink.php index 2d833e86f5..46b6f6e6c3 100644 --- a/app/code/core/Mage/Paypal/Model/Payflowlink.php +++ b/app/code/core/Mage/Paypal/Model/Payflowlink.php @@ -47,6 +47,7 @@ class Mage_Paypal_Model_Payflowlink extends Mage_Paypal_Model_Payflowpro */ protected $_canUseInternal = false; protected $_canUseForMultishipping = false; + protected $_isInitializeNeeded = true; /** * Request & response model @@ -66,12 +67,6 @@ class Mage_Paypal_Model_Payflowlink extends Mage_Paypal_Model_Payflowpro */ const RESPONSE_ERROR_MSG = 'Payment error. %s was not found.'; - /** - * Quote Changed Error message - * @var string - */ - const SHOPPING_CART_CHANGED_ERROR_MSG = 'Shopping cart contents has been changed.'; - /** * Key for storing secure hash in additional information of payment model * @@ -113,194 +108,29 @@ public function isAvailable($quote = null) */ public function initialize($paymentAction, $stateObject) { - $payment = $this->getInfoInstance(); - - $salesDocument = $payment->getOrder(); - if (!$salesDocument) { - $salesDocument = $payment->getQuote(); - $amount = $salesDocument->getBaseGrandTotal(); - } else { - $amount = $salesDocument->getBaseTotalDue(); - } - //create reference transaction if Verification Authorization Amount set to $0 or $1 - $authorizationAmount = $this->getConfigData('authorization_amount', $salesDocument->getStoreId()); - switch ($authorizationAmount) { - case Mage_Paypal_Model_Config::AUTHORIZATION_AMOUNT_FULL: - $this->_initialize($payment, $amount); + switch ($paymentAction) { + case Mage_Paypal_Model_Config::PAYMENT_ACTION_AUTH: + case Mage_Paypal_Model_Config::PAYMENT_ACTION_SALE: + $payment = $this->getInfoInstance(); + $order = $payment->getOrder(); + $order->setCanSendNewEmailFlag(false); + $payment->setAmountAuthorized($order->getTotalDue()); + $payment->setBaseAmountAuthorized($order->getBaseTotalDue()); + $this->_generateSecureSilentPostHash($payment); + $request = $this->_buildTokenRequest($payment); + $response = $this->_postRequest($request); + $this->_processTokenErrors($response, $payment); + + $order = $payment->getOrder(); + $order->setCanSendNewEmailFlag(false); + + $stateObject->setState(Mage_Sales_Model_Order::STATE_PENDING_PAYMENT); + $stateObject->setStatus('pending_payment'); + $stateObject->setIsNotified(false); break; - case Mage_Paypal_Model_Config::AUTHORIZATION_AMOUNT_ONE: - $this->_initialize($payment, 1); - break; - case Mage_Paypal_Model_Config::AUTHORIZATION_AMOUNT_ZERO: - try { - $this->_initialize($payment, 0); - } catch (Mage_Paypal_Exception $e) { - $authorizationAmount = Mage_Paypal_Model_Config::AUTHORIZATION_AMOUNT_ONE; - $this->_initialize($payment, 1); - } + default: break; } - $payment->setAdditionalInformation('authorization_amount', $authorizationAmount); - - return $this; - } - - /** - * Add transaction with correct transaction Id - * - * @param Varien_Object $payment - * @param string $txnId - * @return void - */ - protected function _addTransaction($payment, $txnId) - { - $previousTxnId = $payment->getTransactionId(); - $payment->setTransactionId($txnId); - $payment->addTransaction(Mage_Sales_Model_Order_Payment_Transaction::TYPE_AUTH); - $payment->setTransactionId($previousTxnId); - } - /** - * Initialize request - * - * @param Varien_Object $payment - * @param $amount - * @return Mage_Paypal_Model_Payflowlink - */ - protected function _initialize(Varien_Object $payment, $amount) - { - $this->_generateSecureSilentPostHash($payment); - $request = $this->_buildTokenRequest($payment, $amount); - $response = $this->_postRequest($request); - $this->_processTokenErrors($response, $payment); - return $this; - } - - /** - * Authorize payment - * - * @param Mage_Sales_Model_Order_Payment | Mage_Sales_Model_Quote_Payment $payment - * @param mixed $amount - * @return Mage_Paypal_Model_Payflowlink - */ - public function authorize(Varien_Object $payment, $amount) - { - $txnId = $payment->getAdditionalInformation('authorization_id'); - /** @var $transaction Mage_Paypal_Model_Payment_Transaction */ - $transaction = Mage::getModel('paypal/payment_transaction'); - $transaction->loadByTxnId($txnId); - - $payment->setTransactionId($txnId)->setIsTransactionClosed(0); - if ($payment->getAdditionalInformation('paypal_fraud_filters') !== null) { - $payment->setIsTransactionPending(true); - $payment->setIsFraudDetected(true); - } - - if ($transaction->getId() && $payment->getAdditionalInformation('authorization_amount') != - Mage_Paypal_Model_Config::AUTHORIZATION_AMOUNT_FULL - ) { - $this->_addTransaction($payment, $txnId); - } - - $this->_authorize($payment, $amount, $transaction, $txnId); - if ($payment->getAdditionalInformation('authorization_amount') != - Mage_Paypal_Model_Config::AUTHORIZATION_AMOUNT_FULL - ) { - $payment->setParentTransactionId($txnId); - parent::authorize($payment, $amount); - if ($payment->getTransactionId()) { - $payment->setAdditionalInformation('authorization_id', $payment->getTransactionId()); - } - } - - $transaction->delete(); - return $this; - } - - /** - * Additional authorization logic for Account Verification - * - * @param Varien_Object $payment - * @param mixed $amount - * @param Mage_Paypal_Model_Payment_Transaction $transaction - * @param string $txnId - * @return Mage_Paypal_Model_Payflowlink - */ - protected function _authorize(Varien_Object $payment, $amount, $transaction, $txnId) - { - $authorizationAmount = $payment->getAdditionalInformation('authorization_amount'); - if ($authorizationAmount == Mage_Paypal_Model_Config::AUTHORIZATION_AMOUNT_ONE) { - $payment->setParentTransactionId($txnId); - $this->void($payment); - } elseif ($authorizationAmount == Mage_Paypal_Model_Config::AUTHORIZATION_AMOUNT_FULL) { - $this->_checkTransaction($transaction, $amount); - } - return $this; - } - - /** - * Capture payment - * - * @param Mage_Sales_Model_Order_Payment | Mage_Sales_Model_Quote_Payment $payment - * @param mixed $amount - * @return Mage_Paypal_Model_Payflowlink - */ - public function capture(Varien_Object $payment, $amount) - { - $removePaypalTransaction = false; - /** @var $transaction Mage_Paypal_Model_Payment_Transaction */ - $transaction = Mage::getModel('paypal/payment_transaction'); - $txnId = $payment->getAdditionalInformation('authorization_id'); - $transaction->loadByTxnId($txnId); - if ($transaction->getId()) { - $removePaypalTransaction = true; - $this->_authorize($payment, $amount, $transaction, $txnId); - - $this->_addTransaction($payment, $txnId); - - $payment->setReferenceTransactionId($payment->getAdditionalInformation('authorization_id')); - } - - $payment->setParentTransactionId($txnId); - - $payment->setRequestAmount(round($amount,2)); - parent::capture($payment, $amount); - - if ($removePaypalTransaction) { - $transaction->delete(); - } - - return $this; - } - - /** - * Void payment - * - * @param Varien_Object $payment - * @return Mage_Paypal_Model_Payflowlink - */ - public function void(Varien_Object $payment) - { - /** @var $payment Mage_Sales_Model_Quote_Payment */ - if ($payment instanceof Mage_Sales_Model_Order_Payment) { - parent::void($payment); - $payment->addTransaction(Mage_Sales_Model_Order_Payment_Transaction::TYPE_VOID); - return $this; - } elseif ($payment instanceof Mage_Sales_Model_Quote_Payment) { - $this->setStore($payment->getQuote()->getStoreId()); - } else { - if ($payment->getStore()) { - $this->setStore($payment->getStore()); - } - } - - $request = $this->_buildBasicRequest($payment); - $request->setTrxtype(self::TRXTYPE_DELAYED_VOID); - - $request->setOrigid($payment->getTransactionId()); - $response = $this->_postRequest($request); - $this->_processErrors($response); - - return $this; } /** @@ -346,49 +176,63 @@ public function process($responseData) $this->setResponseData($responseData); - $document = $this->_getDocumentFromResponse(); - if ($document) { - $this->_process($document); + if ($order = $this->_getOrderFromResponse()) { + $this->_processOrder($order); } } /** - * Operate with order or quote using information from silent post + * Operate with order using information from silent post * - * @param Varien_Object $document + * @param Mage_Sales_Model_Order $order */ - protected function _process(Varien_Object $document) + protected function _processOrder(Mage_Sales_Model_Order $order) { $response = $this->getResponse(); - $payment = $document->getPayment(); + $payment = $order->getPayment(); + $payment->setTransactionId($response->getPnref()) + ->setIsTransactionClosed(0); + $canSendNewOrderEmail = true; if ($response->getResult() == self::RESPONSE_CODE_FRAUDSERVICE_FILTER || $response->getResult() == self::RESPONSE_CODE_DECLINED_BY_FILTER ) { - $fraudMessage = $this->_getFraudMessage() ? $response->getFraudMessage() : $response->getRespmsg(); - $payment->setAdditionalInformation('paypal_fraud_filters', $fraudMessage); + $canSendNewOrderEmail = false; + $fraudMessage = $this->_getFraudMessage() ? + $response->getFraudMessage() : $response->getRespmsg(); + $payment->setIsTransactionPending(true) + ->setIsFraudDetected(true) + ->setAdditionalInformation('paypal_fraud_filters', $fraudMessage); } if ($response->getAvsdata() && strstr(substr($response->getAvsdata(), 0, 2), 'N')) { $payment->setAdditionalInformation('paypal_avs_code', substr($response->getAvsdata(), 0, 2)); } - if ($response->getCvv2match() && $response->getCvv2match() != 'Y') { $payment->setAdditionalInformation('paypal_cvv2_match', $response->getCvv2match()); } - $payment->setAdditionalInformation('authorization_id', $response->getPnref()); - - /** @var $transaction Mage_Paypal_Model_Payment_Transaction */ - $transaction = Mage::getModel('paypal/payment_transaction'); - $transaction->setTxnId($response->getPnref()); - - $transaction->setAdditionalInformation('amt', $response->getAmt()); - $transaction->setAdditionalInformation('store_id', $document->getStoreId()); + switch ($response->getType()){ + case self::TRXTYPE_AUTH_ONLY: + $payment->registerAuthorizationNotification($payment->getBaseAmountAuthorized()); + break; + case self::TRXTYPE_SALE: + $payment->registerCaptureNotification($payment->getBaseAmountAuthorized()); + break; + } + $order->save(); - $document->setIsChanged(1); - $document->save(); - $transaction->save(); + try { + if ($canSendNewOrderEmail) { + $order->sendNewOrderEmail(); + } + Mage::getModel('sales/quote') + ->load($order->getQuoteId()) + ->setIsActive(false) + ->save(); + } catch (Exception $e) { + Mage::throwException(Mage::helper('paypal')->__('Can not send new order email.')); + } } /** @@ -407,58 +251,49 @@ protected function _getFraudMessage() return false; } - /** - * Check Transaction - * - * @param Mage_Paypal_Model_Payment_Transaction $transaction - * @param mixed $amount - * @return Mage_Paypal_Model_Payflowlink - */ - protected function _checkTransaction($transaction, $amount) - { - if (!$transaction->getId()) { - Mage::throwException(Mage::helper('paypal')->__(self::SHOPPING_CART_CHANGED_ERROR_MSG)); - } - - $authorizedAmt = $transaction->getAdditionalInformation('amt'); - - if (!$authorizedAmt || $amount > $authorizedAmt) { - Mage::throwException(Mage::helper('paypal')->__(self::SHOPPING_CART_CHANGED_ERROR_MSG)); - } - return $this; - } - /** * Check response from Payflow gateway. * - * @return Mage_Sales_Model_Abstract in case of validation passed + * @return Mage_Sales_Model_Order in case of validation passed * @throws Mage_Core_Exception in other cases */ - protected function _getDocumentFromResponse() + protected function _getOrderFromResponse() { $response = $this->getResponse(); - $salesDocument = Mage::getModel('sales/quote')->load($response->getPonum()); - $salesDocument->getPayment()->setMethod(Mage_Paypal_Model_Config::METHOD_PAYFLOWLINK); + $order = Mage::getModel('sales/order') + ->loadByIncrementId($response->getInvnum()); - if ($this->_getSecureSilentPostHash($salesDocument->getPayment()) != $response->getUser2() - || $this->_code != $salesDocument->getPayment()->getMethodInstance()->getCode()) { + if ($this->_getSecureSilentPostHash($order->getPayment()) != $response->getUser2() + || $this->_code != $order->getPayment()->getMethodInstance()->getCode() + ) { return false; } - if ($response->getResult() != self::RESPONSE_CODE_FRAUDSERVICE_FILTER && - $response->getResult() != self::RESPONSE_CODE_DECLINED_BY_FILTER && - $response->getResult() != self::RESPONSE_CODE_APPROVED + if ($response->getResult() != self::RESPONSE_CODE_FRAUDSERVICE_FILTER + && $response->getResult() != self::RESPONSE_CODE_DECLINED_BY_FILTER + && $response->getResult() != self::RESPONSE_CODE_APPROVED ) { + if ($order->getState() != Mage_Sales_Model_Order::STATE_CANCELED) { + $order->registerCancellation($response->getRespmsg())->save(); + } Mage::throwException($response->getRespmsg()); } - $fetchData = $this->fetchTransactionInfo($salesDocument->getPayment(), $response->getPnref()); - if (!isset($fetchData['custref']) || $fetchData['custref'] != $salesDocument->getReservedOrderId()) { - Mage::throwException($this->_formatStr(self::RESPONSE_ERROR_MSG, 'Transaction error')); + $amountCompared = ($response->getAmt() == $order->getPayment()->getBaseAmountAuthorized()) ? true : false; + if (!$order->getId() + || $order->getState() != Mage_Sales_Model_Order::STATE_PENDING_PAYMENT + || !$amountCompared + ) { + Mage::throwException($this->_formatStr(self::RESPONSE_ERROR_MSG, 'Order')); } - return $salesDocument; + $fetchData = $this->fetchTransactionInfo($order->getPayment(), $response->getPnref()); + if (!isset($fetchData['custref']) || $fetchData['custref'] != $order->getIncrementId()) { + Mage::throwException($this->_formatStr(self::RESPONSE_ERROR_MSG, 'Transaction')); + } + + return $order; } /** @@ -467,42 +302,27 @@ protected function _getDocumentFromResponse() * @param Mage_Sales_Model_Order_Payment $payment * @return Varien_Object */ - protected function _buildTokenRequest(Varien_Object $payment, $amount) + protected function _buildTokenRequest(Mage_Sales_Model_Order_Payment $payment) { - $orderId = null; - $fullAmount = $payment->getAdditionalInformation('authorization_amount'); - - $salesDocument = $payment->getOrder(); - if (!$salesDocument) { - $salesDocument = $payment->getQuote(); - if (!$salesDocument->getReservedOrderId()) { - $salesDocument->reserveOrderId(); - } - $orderId = $salesDocument->getReservedOrderId(); - } else { - $orderId = $salesDocument->getIncrementId(); - } - $request = $this->_buildBasicRequest($payment); - if (empty($salesDocument)) { - return $request; - } - $request->setCreatesecuretoken('Y') ->setSecuretokenid($this->_generateSecureTokenId()) ->setTrxtype($this->_getTrxTokenType()) - ->setAmt($this->_formatStr('%.2F', $amount)) - ->setCurrency($salesDocument->getBaseCurrencyCode()) - ->setInvnum($orderId) - ->setCustref($orderId) - ->setPonum($salesDocument->getId()); - if ($fullAmount != Mage_Paypal_Model_Config::AUTHORIZATION_AMOUNT_FULL) { - $request->setSubtotal($this->_formatStr('%.2F', $salesDocument->getBaseSubtotal())) - ->setTaxamt($this->_formatStr('%.2F', $salesDocument->getBaseTaxAmount())) - ->setFreightamt($this->_formatStr('%.2F', $salesDocument->getBaseShippingAmount())); + ->setAmt($this->_formatStr('%.2F', $payment->getOrder()->getBaseTotalDue())) + ->setCurrency($payment->getOrder()->getBaseCurrencyCode()) + ->setInvnum($payment->getOrder()->getIncrementId()) + ->setCustref($payment->getOrder()->getIncrementId()) + ->setPonum($payment->getOrder()->getId()) + ->setSubtotal($payment->getOrder()->getBaseSubtotal()) + ->setTaxamt($this->_formatStr('%.2F', $payment->getOrder()->getBaseTaxAmount())) + ->setFreightamt($this->_formatStr('%.2F', $payment->getOrder()->getBaseShippingAmount())); + + $order = $payment->getOrder(); + if (empty($order)) { + return $request; } - $billing = $salesDocument->getBillingAddress(); + $billing = $order->getBillingAddress(); if (!empty($billing)) { $request->setFirstname($billing->getFirstname()) ->setLastname($billing->getLastname()) @@ -511,9 +331,9 @@ protected function _buildTokenRequest(Varien_Object $payment, $amount) ->setState($billing->getRegionCode()) ->setZip($billing->getPostcode()) ->setCountry($billing->getCountry()) - ->setEmail($salesDocument->getCustomerEmail()); + ->setEmail($order->getCustomerEmail()); } - $shipping = $salesDocument->getShippingAddress(); + $shipping = $order->getShippingAddress(); if (!empty($shipping)) { $this->_applyCountryWorkarounds($shipping); $request->setShiptofirstname($shipping->getFirstname()) @@ -525,7 +345,7 @@ protected function _buildTokenRequest(Varien_Object $payment, $amount) ->setShiptocountry($shipping->getCountry()); } //pass store Id to request - $request->setUser1($salesDocument->getStoreId()) + $request->setUser1($order->getStoreId()) ->setUser2($this->_getSecureSilentPostHash($payment)); return $request; @@ -563,25 +383,6 @@ protected function _buildBasicRequest(Varien_Object $payment) ->setPwd($this->getConfigData('pwd', $this->_getStoreId())) ->setVerbosity($this->getConfigData('verbosity', $this->_getStoreId())) ->setTender(self::TENDER_CC); - if ($payment->getRequestAmount() > 0) { - $request->setAmt(round($payment->getRequestAmount(),2)); - } - return $request; - } - - /** - * Return request object with information for 'authorization' or 'sale' action - * - * @param Mage_Sales_Model_Order_Payment $payment - * @param float $amount - * @return Varien_Object - */ - protected function _buildPlaceRequest(Varien_Object $payment, $amount) - { - $request = $this->_buildBasicRequest($payment); - $request->setAmt(round($amount,2)); - $request->setCurrency($payment->getOrder()->getBaseCurrencyCode()); - return $request; } @@ -592,7 +393,12 @@ protected function _buildPlaceRequest(Varien_Object $payment, $amount) */ protected function _getTrxTokenType() { - return self::TRXTYPE_AUTH_ONLY; + switch ($this->getConfigData('payment_action')) { + case Mage_Paypal_Model_Config::PAYMENT_ACTION_AUTH: + return self::TRXTYPE_AUTH_ONLY; + case Mage_Paypal_Model_Config::PAYMENT_ACTION_SALE: + return self::TRXTYPE_SALE; + } } /** @@ -627,9 +433,7 @@ protected function _formatStr($format, $string) */ protected function _processTokenErrors($response, $payment) { - if ($response->getResult() == self::RESPONSE_CODE_INVALID_AMOUNT) { - throw new Mage_Paypal_Exception(Mage::helper('paypal')->__('Invalid Amount')); - } elseif (!$response->getSecuretoken() && + if (!$response->getSecuretoken() && $response->getResult() != self::RESPONSE_CODE_APPROVED && $response->getResult() != self::RESPONSE_CODE_FRAUDSERVICE_FILTER) { Mage::throwException($response->getRespmsg()); @@ -664,21 +468,88 @@ protected function _generateSecureSilentPostHash($payment) } /** - * Set reference transaction data into request + * Add transaction with correct transaction Id + * + * @deprecated since 1.6.2.0 + * @param Varien_Object $payment + * @param string $txnId + * @return void + */ + protected function _addTransaction($payment, $txnId) + { + } + + /** + * Initialize request * + * @deprecated since 1.6.2.0 * @param Varien_Object $payment - * @param Varien_Object $request + * @param $amount * @return Mage_Paypal_Model_Payflowlink */ - protected function _setReferenceTransaction(Varien_Object $payment, $request) + protected function _initialize(Varien_Object $payment, $amount) { - if ($payment->getParentTransactionId()) { - $request->setOrigid($payment->getParentTransactionId()); + return $this; + } - $request->unsAcct(); - $request->unsExpdate(); - $request->unsCvv2(); - } + /** + * Check whether order review has enough data to initialize + * + * @deprecated since 1.6.2.0 + * @param $token + * @throws Mage_Core_Exception + */ + public function prepareOrderReview($token = null) + { + } + + /** + * Additional authorization logic for Account Verification + * + * @deprecated since 1.6.2.0 + * @param Varien_Object $payment + * @param mixed $amount + * @param Mage_Paypal_Model_Payment_Transaction $transaction + * @param string $txnId + * @return Mage_Paypal_Model_Payflowlink + */ + protected function _authorize(Varien_Object $payment, $amount, $transaction, $txnId) + { + return $this; + } + + /** + * Operate with order or quote using information from silent post + * + * @deprecated since 1.6.2.0 + * @param Varien_Object $document + */ + protected function _process(Varien_Object $document) + { + } + + /** + * Check Transaction + * + * @deprecated since 1.6.2.0 + * @param Mage_Paypal_Model_Payment_Transaction $transaction + * @param mixed $amount + * @return Mage_Paypal_Model_Payflowlink + */ + protected function _checkTransaction($transaction, $amount) + { return $this; } + + /** + * Check response from Payflow gateway. + * + * @deprecated since 1.6.2.0 + * @return Mage_Sales_Model_Abstract in case of validation passed + * @throws Mage_Core_Exception in other cases + */ + protected function _getDocumentFromResponse() + { + return null; + } } diff --git a/app/code/core/Mage/Paypal/Model/Payflowpro.php b/app/code/core/Mage/Paypal/Model/Payflowpro.php index 45af5ebe71..71336fd6ae 100644 --- a/app/code/core/Mage/Paypal/Model/Payflowpro.php +++ b/app/code/core/Mage/Paypal/Model/Payflowpro.php @@ -416,8 +416,11 @@ protected function _buildPlaceRequest(Varien_Object $payment, $amount) $order = $payment->getOrder(); if(!empty($order)){ - $request->setCurrency($order->getBaseCurrencyCode()) - ->setCustref($order->getIncrementId()); + $request->setCurrency($order->getBaseCurrencyCode()); + + $orderIncrementId = $order->getIncrementId(); + $request->setCustref($orderIncrementId) + ->setComment1($orderIncrementId); $billing = $order->getBillingAddress(); if (!empty($billing)) { diff --git a/app/code/core/Mage/Paypal/Model/Report/Settlement.php b/app/code/core/Mage/Paypal/Model/Report/Settlement.php index fe21bdffd3..952868b65c 100644 --- a/app/code/core/Mage/Paypal/Model/Report/Settlement.php +++ b/app/code/core/Mage/Paypal/Model/Report/Settlement.php @@ -70,7 +70,7 @@ class Mage_Paypal_Model_Report_Settlement extends Mage_Core_Model_Abstract const REPORTS_PATH = "/ppreports/outgoing"; /** - * Original charset of report files + * Original charset of old report files * @var string */ const FILES_IN_CHARSET = "UTF-16"; @@ -87,6 +87,84 @@ class Mage_Paypal_Model_Report_Settlement extends Mage_Core_Model_Abstract */ protected $_rows = array(); + protected $_csvColumns = array( + 'old' => array( + 'section_columns' => array( + '' => 0, + 'TransactionID' => 1, + 'InvoiceID' => 2, + 'PayPalReferenceID' => 3, + 'PayPalReferenceIDType' => 4, + 'TransactionEventCode' => 5, + 'TransactionInitiationDate' => 6, + 'TransactionCompletionDate' => 7, + 'TransactionDebitOrCredit' => 8, + 'GrossTransactionAmount' => 9, + 'GrossTransactionCurrency' => 10, + 'FeeDebitOrCredit' => 11, + 'FeeAmount' => 12, + 'FeeCurrency' => 13, + 'CustomField' => 14, + 'ConsumerID' => 15 + ), + 'rowmap' => array( + 'TransactionID' => 'transaction_id', + 'InvoiceID' => 'invoice_id', + 'PayPalReferenceID' => 'paypal_reference_id', + 'PayPalReferenceIDType' => 'paypal_reference_id_type', + 'TransactionEventCode' => 'transaction_event_code', + 'TransactionInitiationDate' => 'transaction_initiation_date', + 'TransactionCompletionDate' => 'transaction_completion_date', + 'TransactionDebitOrCredit' => 'transaction_debit_or_credit', + 'GrossTransactionAmount' => 'gross_transaction_amount', + 'GrossTransactionCurrency' => 'gross_transaction_currency', + 'FeeDebitOrCredit' => 'fee_debit_or_credit', + 'FeeAmount' => 'fee_amount', + 'FeeCurrency' => 'fee_currency', + 'CustomField' => 'custom_field', + 'ConsumerID' => 'consumer_id' + ) + ), + 'new' => array( + 'section_columns' => array( + '' => 0, + 'Transaction ID' => 1, + 'Invoice ID' => 2, + 'PayPal Reference ID' => 3, + 'PayPal Reference ID Type' => 4, + 'Transaction Event Code' => 5, + 'Transaction Initiation Date' => 6, + 'Transaction Completion Date' => 7, + 'Transaction Debit or Credit' => 8, + 'Gross Transaction Amount' => 9, + 'Gross Transaction Currency' => 10, + 'Fee Debit or Credit' => 11, + 'Fee Amount' => 12, + 'Fee Currency' => 13, + 'Custom Field' => 14, + 'Consumer ID' => 15, + 'Payment Tracking ID' => 16 + ), + 'rowmap' => array( + 'Transaction ID' => 'transaction_id', + 'Invoice ID' => 'invoice_id', + 'PayPal Reference ID' => 'paypal_reference_id', + 'PayPal Reference ID Type' => 'paypal_reference_id_type', + 'Transaction Event Code' => 'transaction_event_code', + 'Transaction Initiation Date' => 'transaction_initiation_date', + 'Transaction Completion Date' => 'transaction_completion_date', + 'Transaction Debit or Credit' => 'transaction_debit_or_credit', + 'Gross Transaction Amount' => 'gross_transaction_amount', + 'Gross Transaction Currency' => 'gross_transaction_currency', + 'Fee Debit or Credit' => 'fee_debit_or_credit', + 'Fee Amount' => 'fee_amount', + 'Fee Currency' => 'fee_currency', + 'Custom Field' => 'custom_field', + 'Consumer ID' => 'consumer_id', + 'Payment Tracking ID' => 'payment_tracking_id' + ) + ) + ); /** * Initialize resource model */ @@ -138,8 +216,12 @@ public function fetchAndSave($config) } $encoded = file_get_contents($localCsv); - $decoded = @iconv(self::FILES_IN_CHARSET, self::FILES_OUT_CHARSET.'//IGNORE', $encoded); - file_put_contents($localCsv, $decoded); + $csvFormat = 'new'; + if (self::FILES_OUT_CHARSET != mb_detect_encoding(($encoded))) { + $decoded = @iconv(self::FILES_IN_CHARSET, self::FILES_OUT_CHARSET.'//IGNORE', $encoded); + file_put_contents($localCsv, $decoded); + $csvFormat = 'old'; + } // Set last modified date, this value will be overwritten during parsing if (isset($attributes['mtime'])) { @@ -149,7 +231,7 @@ public function fetchAndSave($config) $this->setReportDate($this->_fileNameToDate($filename)) ->setFilename($filename) - ->parseCsv($localCsv); + ->parseCsv($localCsv, $csvFormat); if ($this->getAccountId()) { $this->save(); @@ -170,47 +252,17 @@ public function fetchAndSave($config) * Parse CSV file and collect report rows * * @param string $localCsv Path to CSV file + * @param string $format CSV format(column names) * @return Mage_Paypal_Model_Report_Settlement */ - public function parseCsv($localCsv) + public function parseCsv($localCsv, $format = 'new') { $this->_rows = array(); - $section_columns = array('' => 0, - 'TransactionID' => 1, - 'InvoiceID' => 2, - 'PayPalReferenceID' => 3, - 'PayPalReferenceIDType' => 4, - 'TransactionEventCode' => 5, - 'TransactionInitiationDate' => 6, - 'TransactionCompletionDate' => 7, - 'TransactionDebitOrCredit' => 8, - 'GrossTransactionAmount' => 9, - 'GrossTransactionCurrency' => 10, - 'FeeDebitOrCredit' => 11, - 'FeeAmount' => 12, - 'FeeCurrency' => 13, - 'CustomField' => 14, - 'ConsumerID' => 15, - ); - $rowmap = array( - 'TransactionID' => 'transaction_id', - 'InvoiceID' => 'invoice_id', - 'PayPalReferenceID' => 'paypal_reference_id', - 'PayPalReferenceIDType' => 'paypal_reference_id_type', - 'TransactionEventCode' => 'transaction_event_code', - 'TransactionInitiationDate' => 'transaction_initiation_date', - 'TransactionCompletionDate' => 'transaction_completion_date', - 'TransactionDebitOrCredit' => 'transaction_debit_or_credit', - 'GrossTransactionAmount' => 'gross_transaction_amount', - 'GrossTransactionCurrency' => 'gross_transaction_currency', - 'FeeDebitOrCredit' => 'fee_debit_or_credit', - 'FeeAmount' => 'fee_amount', - 'FeeCurrency' => 'fee_currency', - 'CustomField' => 'custom_field', - 'ConsumerID' => 'consumer_id', - ); - $flipped_section_columns = array_flip($section_columns); + $sectionColumns = $this->_csvColumns[$format]['section_columns']; + $rowMap = $this->_csvColumns[$format]['rowmap']; + + $flippedSectionColumns = array_flip($sectionColumns); $fp = fopen($localCsv, 'r'); while($line = fgetcsv($fp)) { if (empty($line)) { // The line was empty, so skip it. @@ -234,16 +286,16 @@ public function parseCsv($localCsv) // In case ever the column order is changed, we will have the items recorded properly // anyway. We have named, not numbered columns. for ($i = 1; $i < count($line); $i++) { - $section_columns[$line[$i]] = $i; + $sectionColumns[$line[$i]] = $i; } - $flipped_section_columns = array_flip($section_columns); + $flippedSectionColumns = array_flip($sectionColumns); break; case 'SB': // Section body. - $bodyitem = array(); + $bodyItem = array(); for($i = 1; $i < count($line); $i++) { - $bodyitem[$rowmap[$flipped_section_columns[$i]]] = $line[$i]; + $bodyItem[$rowMap[$flippedSectionColumns[$i]]] = $line[$i]; } - $this->_rows[] = $bodyitem; + $this->_rows[] = $bodyItem; break; case 'SC': // Section records count. case 'RC': // Report records count. diff --git a/app/code/core/Mage/Paypal/Model/Resource/Payment/Transaction.php b/app/code/core/Mage/Paypal/Model/Resource/Payment/Transaction.php index cddaf8bcd3..981f4c3889 100644 --- a/app/code/core/Mage/Paypal/Model/Resource/Payment/Transaction.php +++ b/app/code/core/Mage/Paypal/Model/Resource/Payment/Transaction.php @@ -27,6 +27,7 @@ /** * Paypal transaction resource model * + * @deprecated since 1.6.2.0 * @category Mage * @package Mage_Paypal * @author Magento Core Team <core@magentocommerce.com> diff --git a/app/code/core/Mage/Paypal/Model/Resource/Payment/Transaction/Collection.php b/app/code/core/Mage/Paypal/Model/Resource/Payment/Transaction/Collection.php index 53adcfde91..569bc36a7e 100644 --- a/app/code/core/Mage/Paypal/Model/Resource/Payment/Transaction/Collection.php +++ b/app/code/core/Mage/Paypal/Model/Resource/Payment/Transaction/Collection.php @@ -27,6 +27,7 @@ /** * Payment transactions collection * + * @deprecated since 1.6.2.0 * @category Mage * @package Mage_Paypal * @author Magento Core Team <core@magentocommerce.com> diff --git a/app/code/core/Mage/Paypal/Model/System/Config/Source/AuthorizationAmounts.php b/app/code/core/Mage/Paypal/Model/System/Config/Source/AuthorizationAmounts.php index 3160a23606..eec78d64a9 100644 --- a/app/code/core/Mage/Paypal/Model/System/Config/Source/AuthorizationAmounts.php +++ b/app/code/core/Mage/Paypal/Model/System/Config/Source/AuthorizationAmounts.php @@ -26,6 +26,11 @@ /** * Source model for available Authorization Amounts for Account Verification + * + * @deprecated since 1.6.2.0 + * @category Mage + * @package Mage_Paypal + * @author Magento Core Team <core@magentocommerce.com> */ class Mage_Paypal_Model_System_Config_Source_AuthorizationAmounts { @@ -36,8 +41,6 @@ class Mage_Paypal_Model_System_Config_Source_AuthorizationAmounts */ public function toOptionArray() { - /** @var $configModel Mage_Paypal_Model_Config */ - $configModel = Mage::getModel('paypal/config'); - return $configModel->getAuthorizationAmounts(); + return array(); } } diff --git a/app/code/core/Mage/Paypal/controllers/PayflowController.php b/app/code/core/Mage/Paypal/controllers/PayflowController.php index f9b6459a22..c05edd5f5e 100644 --- a/app/code/core/Mage/Paypal/controllers/PayflowController.php +++ b/app/code/core/Mage/Paypal/controllers/PayflowController.php @@ -31,7 +31,7 @@ * @package Mage_Paypal * @author Magento Core Team <core@magentocommerce.com> */ -class Mage_Paypal_PayflowController extends Mage_Core_Controller_Front_Action +class Mage_Paypal_PayflowController extends Mage_Paypal_Controller_Express_Abstract { /** * When a customer cancel payment from payflow gateway. @@ -54,20 +54,54 @@ public function returnUrlAction() ->setTemplate('paypal/payflowlink/redirect.phtml'); $session = $this->_getCheckout(); - $quote = $session->getQuote(); - /** @var $payment Mage_Sales_Model_Quote_Payment */ - $payment = $quote->getPayment(); - $gotoSection = 'payment'; - if ($payment->getAdditionalInformation('authorization_id')) { - $gotoSection = 'review'; - } else { - $gotoSection = 'payment'; - $redirectBlock->setErrorMsg($this->__('Payment has been declined. Please try again.')); + if ($session->getLastRealOrderId()) { + $order = Mage::getModel('sales/order')->loadByIncrementId($session->getLastRealOrderId()); + + if ($order && $order->getIncrementId() == $session->getLastRealOrderId()) { + $allowedOrderStates = array( + Mage_Sales_Model_Order::STATE_PROCESSING, + Mage_Sales_Model_Order::STATE_COMPLETE + ); + if (in_array($order->getState(), $allowedOrderStates)) { + $session->unsLastRealOrderId(); + $redirectBlock->setGotoSuccessPage(true); + } else { + $gotoSection = $this->_cancelPayment(strval($this->getRequest()->getParam('RESPMSG'))); + $redirectBlock->setGotoSection($gotoSection); + $redirectBlock->setErrorMsg($this->__('Payment has been declined. Please try again.')); + } + } } - $redirectBlock->setGotoSection($gotoSection); + $this->getResponse()->setBody($redirectBlock->toHtml()); } + /** + * Submit transaction to Payflow getaway into iframe + */ + public function formAction() + { + $this->getResponse() + ->setBody($this->_getIframeBlock()->toHtml()); + } + + /** + * Get response from PayPal by silent post method + */ + public function silentPostAction() + { + $data = $this->getRequest()->getPost(); + if (isset($data['INVNUM'])) { + /** @var $paymentModel Mage_Paypal_Model_Payflowlink */ + $paymentModel = Mage::getModel('paypal/payflowlink'); + try { + $paymentModel->process($data); + } catch (Exception $e) { + Mage::logException($e); + } + } + } + /** * Cancel order, return quote to customer * @@ -104,53 +138,6 @@ protected function _cancelPayment($errorMsg = '') return $gotoSection; } - /** - * Submit transaction to Payflow getaway into iframe - */ - public function formAction() - { - $quote = $this->_getCheckout()->getQuote(); - $payment = $quote->getPayment(); - - try { - $method = Mage::helper('payment')->getMethodInstance(Mage_Paypal_Model_Config::METHOD_PAYFLOWLINK); - $method->setData('info_instance', $payment); - $method->initialize($method->getConfigData('payment_action'), new Varien_Object()); - - $quote->save(); - } catch (Mage_Core_Exception $e) { - $this->loadLayout('paypal_payflow_link_iframe'); - - $block = $this->getLayout()->getBlock('payflow.link.info'); - $block->setErrorMessage($e->getMessage()); - - $this->getResponse()->setBody( - $block->toHtml() - ); - return; - } catch (Exception $e) { - Mage::logException($e); - } - $this->getResponse() - ->setBody($this->_getIframeBlock()->toHtml()); - } - - /** - * Get response from PayPal by silent post method - */ - public function silentPostAction() - { - $data = $this->getRequest()->getPost(); - if (isset($data['INVNUM'])) { - $paymentModel = Mage::getModel('paypal/payflowlink'); - try { - $paymentModel->process($data); - } catch (Exception $e) { - Mage::logException($e); - } - } - } - /** * Get frontend checkout session object * diff --git a/app/code/core/Mage/Paypal/controllers/PayflowadvancedController.php b/app/code/core/Mage/Paypal/controllers/PayflowadvancedController.php new file mode 100644 index 0000000000..64c9840809 --- /dev/null +++ b/app/code/core/Mage/Paypal/controllers/PayflowadvancedController.php @@ -0,0 +1,170 @@ +<?php +/** + * Magento + * + * NOTICE OF LICENSE + * + * This source file is subject to the Open Software License (OSL 3.0) + * that is bundled with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://opensource.org/licenses/osl-3.0.php + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@magentocommerce.com so we can send you a copy immediately. + * + * DISCLAIMER + * + * Do not edit or add to this file if you wish to upgrade Magento to newer + * versions in the future. If you wish to customize Magento for your + * needs please refer to http://www.magentocommerce.com for more information. + * + * @category Mage + * @package Mage_Paypal + * @copyright Copyright (c) 2011 Magento Inc. (http://www.magentocommerce.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ + +/** + * Payflow Advanced Checkout Controller + * + * @category Mage + * @package Mage_Paypal + * @author Magento Core Team <core@magentocommerce.com> + */ +class Mage_Paypal_PayflowadvancedController extends Mage_Paypal_Controller_Express_Abstract +{ + /** + * When a customer cancel payment from payflow gateway. + * + * @return void + */ + public function cancelPaymentAction() + { + $gotoSection = $this->_cancelPayment(); + $redirectBlock = $this->_getIframeBlock() + ->setGotoSection($gotoSection) + ->setTemplate('paypal/payflowadvanced/redirect.phtml'); + $this->getResponse()->setBody($redirectBlock->toHtml()); + } + + /** + * When a customer return to website from payflow gateway. + * + * @return void + */ + public function returnUrlAction() + { + $redirectBlock = $this->_getIframeBlock() + ->setTemplate('paypal/payflowadvanced/redirect.phtml'); + + $session = $this->_getCheckout(); + if ($session->getLastRealOrderId()) { + $order = Mage::getModel('sales/order')->loadByIncrementId($session->getLastRealOrderId()); + + if ($order && $order->getIncrementId() == $session->getLastRealOrderId()) { + $allowedOrderStates = array( + Mage_Sales_Model_Order::STATE_PROCESSING, + Mage_Sales_Model_Order::STATE_COMPLETE + ); + if (in_array($order->getState(), $allowedOrderStates)) { + $session->unsLastRealOrderId(); + $redirectBlock->setGotoSuccessPage(true); + } else { + $gotoSection = $this->_cancelPayment(strval($this->getRequest()->getParam('RESPMSG'))); + $redirectBlock->setGotoSection($gotoSection); + $redirectBlock->setErrorMsg($this->__('Payment has been declined. Please try again.')); + } + } + } + + $this->getResponse()->setBody($redirectBlock->toHtml()); + } + + /** + * Submit transaction to Payflow getaway into iframe + * + * @return void + */ + public function formAction() + { + $this->getResponse() + ->setBody($this->_getIframeBlock()->toHtml()); + } + + /** + * Get response from PayPal by silent post method + * + * @return void + */ + public function silentPostAction() + { + $data = $this->getRequest()->getPost(); + if (isset($data['INVNUM'])) { + /** @var $paymentModel Mage_Paypal_Model_Payflowadvanced */ + $paymentModel = Mage::getModel('paypal/payflowadvanced'); + try { + $paymentModel->process($data); + } catch (Exception $e) { + Mage::logException($e); + } + } + } + + /** + * Cancel order, return quote to customer + * + * @param string $errorMsg + * @return bool|string + */ + protected function _cancelPayment($errorMsg = '') + { + $gotoSection = false; + $session = $this->_getCheckout(); + if ($session->getLastRealOrderId()) { + $order = Mage::getModel('sales/order')->loadByIncrementId($session->getLastRealOrderId()); + if ($order->getId()) { + //Cancel order + if ($order->getState() != Mage_Sales_Model_Order::STATE_CANCELED) { + $order->registerCancellation($errorMsg)->save(); + } + $quote = Mage::getModel('sales/quote') + ->load($order->getQuoteId()); + //Return quote + if ($quote->getId()) { + $quote->setIsActive(1) + ->setReservedOrderId(NULL) + ->save(); + $session->replaceQuote($quote); + } + //Unset data + $session->unsLastRealOrderId(); + //Redirect to payment step + $gotoSection = 'payment'; + } + } + + return $gotoSection; + } + + /** + * Get frontend checkout session object + * + * @return Mage_Checkout_Model_Session + */ + protected function _getCheckout() + { + return Mage::getSingleton('checkout/session'); + } + + /** + * Get iframe block + * + * @return Mage_Paypal_Block_Payflow_Advanced_Iframe + */ + protected function _getIframeBlock() + { + $this->loadLayout('paypal_payflow_advanced_iframe'); + return $this->getLayout() + ->getBlock('payflow.advanced.iframe'); + } +} diff --git a/app/code/core/Mage/Paypal/etc/config.xml b/app/code/core/Mage/Paypal/etc/config.xml index 86a5feb541..d804cf46e1 100644 --- a/app/code/core/Mage/Paypal/etc/config.xml +++ b/app/code/core/Mage/Paypal/etc/config.xml @@ -28,7 +28,7 @@ <config> <modules> <Mage_Paypal> - <version>1.6.0.1</version> + <version>1.6.0.2</version> </Mage_Paypal> </modules> <global> @@ -254,12 +254,23 @@ </paypal_billing_agreement> <payflow_link> <model>paypal/payflowlink</model> - <title>Payflow Link Authorization HIGH paypal + Credit Card + + paypal/payflowadvanced + Authorization + HIGH + + paypal + Credit Card + PayPal + PayPal + PayPal + paypal/hostedpro Payment by cards or by PayPal account @@ -275,14 +286,6 @@ paypal/observer::fetchReports - - - */15 * * * * - - - paypal/observer::cleanTransactions - - diff --git a/app/code/core/Mage/Paypal/etc/system.xml b/app/code/core/Mage/Paypal/etc/system.xml index 8da14aa53f..7002b25677 100644 --- a/app/code/core/Mage/Paypal/etc/system.xml +++ b/app/code/core/Mage/Paypal/etc/system.xml @@ -98,6 +98,11 @@ all + + + + + payment/paypal_express/active checkbox @@ -112,6 +117,11 @@ all + + + + + payment/paypal_standard/active checkbox @@ -173,7 +183,7 @@ - Quick set-up service lets your customers securely complete transactions. + Accept payments with a PCI-compliant checkout that keeps customers on your site. For use with your own merchant account. US, CA @@ -184,6 +194,18 @@ 1 1 + + + Accept payments with a PCI-compliant checkout that keeps customers on your site. Includes a merchant account from PayPal. + + US + + payment/payflow_advanced/active + checkbox + 55 + 1 + 1 + Contact PayPal before activating]]> @@ -209,6 +231,11 @@
Pasarela integral Settings
+ + + + + payment/hosted_pro/active checkbox @@ -355,6 +382,7 @@ 1 1 1 + validate-number @@ -530,6 +558,7 @@ 1 1 1 + validate-number @@ -627,6 +656,7 @@ 1 1 1 + validate-number @@ -776,6 +806,7 @@ 1 1 1 + validate-number @@ -863,6 +894,7 @@ 1 1 1 + validate-number @@ -1080,6 +1112,7 @@ 1 1 1 + validate-number @@ -1286,6 +1319,7 @@ 10 1 1 + validate-number @@ -1562,6 +1596,7 @@ 1 1 1 + validate-number @@ -1572,16 +1607,6 @@ 1 1 - - - Learn More]]> - payment/payflow_link/authorization_amount - select - paypal/system_config_source_authorizationAmounts - 15 - 1 - 1 - payment/payflow_link/allowspecific @@ -1690,6 +1715,158 @@
+ + + text + 90 + 1 + 1 + 1 + + + paypal/adminhtml_system_config_payflowlink_advanced + 1 + 1 + 1 + 22 + + + <label>Title</label> + <comment>It is recommended to set this value to "Debit or Credit Card" per store views.</comment> + <config_path>payment/payflow_advanced/title</config_path> + <frontend_type>text</frontend_type> + <sort_order>5</sort_order> + <show_in_default>1</show_in_default> + <show_in_website>1</show_in_website> + <show_in_store>1</show_in_store> + + + + payment/payflow_advanced/sort_order + text + 10 + 1 + 1 + 1 + validate-number + + + + payment/payflow_advanced/payment_action + select + paypal/system_config_source_paymentActions + 15 + 1 + 1 + + + + payment/payflow_advanced/allowspecific + select + adminhtml/system_config_source_payment_allspecificcountries + 20 + 1 + 1 + + + + payment/payflow_advanced/specificcountry + multiselect + paypal/system_config_source_buyerCountry + 25 + 1 + 1 + 1 + + + + payment/payflow_advanced/debug + select + adminhtml/system_config_source_yesno + 30 + 1 + 1 + + + + adminhtml/system_config_form_field_heading + 35 + 1 + 1 + + + + payment/payflow_advanced/partner + text + 40 + 1 + 1 + + + + payment/payflow_advanced/vendor + text + 45 + 1 + 1 + + + + If you do not have multiple users set up on your account, please re-enter your Vendor/Merchant Login here. + text + payment/payflow_advanced/user + 50 + 1 + 1 + + + + payment/payflow_advanced/pwd + obscure + adminhtml/system_config_backend_encrypted + 55 + 1 + 1 + + + + payment/payflow_advanced/sandbox_flag + select + adminhtml/system_config_source_yesno + 60 + 1 + 1 + + + + payment/payflow_advanced/use_proxy + select + adminhtml/system_config_source_yesno + 65 + 1 + 1 + + + + payment/payflow_advanced/proxy_host + text + 70 + 1 + 1 + 1 + + + + payment/payflow_advanced/proxy_port + text + 75 + 1 + 1 + 1 + + + + text @@ -1716,6 +1893,7 @@ 1 1 1 + validate-number diff --git a/app/code/core/Mage/Paypal/sql/paypal_setup/upgrade-1.6.0.1-1.6.0.2.php b/app/code/core/Mage/Paypal/sql/paypal_setup/upgrade-1.6.0.1-1.6.0.2.php new file mode 100644 index 0000000000..397b660940 --- /dev/null +++ b/app/code/core/Mage/Paypal/sql/paypal_setup/upgrade-1.6.0.1-1.6.0.2.php @@ -0,0 +1,35 @@ +getConnection() + ->addColumn($installer->getTable('paypal/settlement_report_row'), 'payment_tracking_id', array( + 'type' => Varien_Db_Ddl_Table::TYPE_TEXT, + 'comment' => 'Payment Tracking ID', + 'length' => '255' + )); diff --git a/app/code/core/Mage/Poll/Model/Poll/Answer.php b/app/code/core/Mage/Poll/Model/Poll/Answer.php index 31b6912ba2..f846d2e56e 100644 --- a/app/code/core/Mage/Poll/Model/Poll/Answer.php +++ b/app/code/core/Mage/Poll/Model/Poll/Answer.php @@ -52,7 +52,9 @@ protected function _construct() public function countPercent($poll) { - $this->setPercent(round(( $poll->getVotesCount() > 0 ) ? ($this->getVotesCount() * 100 / $poll->getVotesCount()) : 0)); + $this->setPercent( + round(($poll->getVotesCount() > 0 ) ? ($this->getVotesCount() * 100 / $poll->getVotesCount()) : 0, 2) + ); return $this; } diff --git a/app/code/core/Mage/Reports/Model/Flag.php b/app/code/core/Mage/Reports/Model/Flag.php index fe2fad95f9..b64b5b5114 100644 --- a/app/code/core/Mage/Reports/Model/Flag.php +++ b/app/code/core/Mage/Reports/Model/Flag.php @@ -41,6 +41,7 @@ class Mage_Reports_Model_Flag extends Mage_Core_Model_Flag const REPORT_REFUNDED_FLAG_CODE = 'report_refunded_aggregated'; const REPORT_COUPONS_FLAG_CODE = 'report_coupons_aggregated'; const REPORT_BESTSELLERS_FLAG_CODE = 'report_bestsellers_aggregated'; + const REPORT_PRODUCT_VIEWED_FLAG_CODE = 'report_product_viewed_aggregated'; /** * Setter for flag code diff --git a/app/code/core/Mage/Reports/Model/Mysql4/Product/Index/Abstract.php b/app/code/core/Mage/Reports/Model/Mysql4/Product/Index/Abstract.php index c7bd644200..4142cc0e1c 100644 --- a/app/code/core/Mage/Reports/Model/Mysql4/Product/Index/Abstract.php +++ b/app/code/core/Mage/Reports/Model/Mysql4/Product/Index/Abstract.php @@ -32,6 +32,7 @@ * @package Mage_Reports * @author Magento Core Team */ -class Mage_Reports_Model_Mysql4_Product_Index_Abstract extends Mage_Reports_Model_Resource_Product_Index_Abstract +abstract class Mage_Reports_Model_Mysql4_Product_Index_Abstract + extends Mage_Reports_Model_Resource_Product_Index_Abstract { } diff --git a/app/code/core/Mage/Reports/Model/Mysql4/Product/Index/Collection/Abstract.php b/app/code/core/Mage/Reports/Model/Mysql4/Product/Index/Collection/Abstract.php index 096d056da1..196c49017c 100644 --- a/app/code/core/Mage/Reports/Model/Mysql4/Product/Index/Collection/Abstract.php +++ b/app/code/core/Mage/Reports/Model/Mysql4/Product/Index/Collection/Abstract.php @@ -32,7 +32,7 @@ * @package Mage_Reports * @author Magento Core Team */ -class Mage_Reports_Model_Mysql4_Product_Index_Collection_Abstract +abstract class Mage_Reports_Model_Mysql4_Product_Index_Collection_Abstract extends Mage_Reports_Model_Resource_Product_Index_Collection_Abstract { } diff --git a/app/code/core/Mage/Reports/Model/Mysql4/Report/Abstract.php b/app/code/core/Mage/Reports/Model/Mysql4/Report/Abstract.php index c73bd373bf..ebe9c32730 100644 --- a/app/code/core/Mage/Reports/Model/Mysql4/Report/Abstract.php +++ b/app/code/core/Mage/Reports/Model/Mysql4/Report/Abstract.php @@ -32,6 +32,6 @@ * @package Mage_Reports * @author Magento Core Team */ -class Mage_Reports_Model_Mysql4_Report_Abstract extends Mage_Reports_Model_Resource_Report_Abstract +abstract class Mage_Reports_Model_Mysql4_Report_Abstract extends Mage_Reports_Model_Resource_Report_Abstract { } diff --git a/app/code/core/Mage/Reports/Model/Resource/Helper/Interface.php b/app/code/core/Mage/Reports/Model/Resource/Helper/Interface.php new file mode 100644 index 0000000000..8e2442e597 --- /dev/null +++ b/app/code/core/Mage/Reports/Model/Resource/Helper/Interface.php @@ -0,0 +1,52 @@ + */ class Mage_Reports_Model_Resource_Helper_Mysql4 extends Mage_Core_Model_Resource_Helper_Mysql4 + implements Mage_Reports_Model_Resource_Helper_Interface { /** @@ -48,4 +49,71 @@ public function mergeVisitorProductIndex($mainTable, $data, $matchFields) return $result; } + /** + * Update rating position + * + * @param string $type day|month|year + * @param string $column + * @param string $mainTable + * @param string $aggregationTable + * @return Mage_Core_Model_Resource_Helper_Mysql4 + */ + public function updateReportRatingPos($type, $column, $mainTable, $aggregationTable) { + $adapter = $this->_getWriteAdapter(); + $periodSubSelect = $adapter->select(); + $ratingSubSelect = $adapter->select(); + $ratingSelect = $adapter->select(); + + switch ($type) { + case 'year': + $periodCol = $adapter->getDateFormatSql('t.period', '%Y-01-01'); + break; + case 'month': + $periodCol = $adapter->getDateFormatSql('t.period', '%Y-%m-01'); + break; + default: + $periodCol = 't.period'; + break; + } + + $columns = array( + 'period' => 't.period', + 'store_id' => 't.store_id', + 'product_id' => 't.product_id', + 'product_name' => 't.product_name', + 'product_price' => 't.product_price', + ); + + if ($type == 'day') { + $columns['id'] = 't.id'; // to speed-up insert on duplicate key update + } + + $cols = array_keys($columns); + $cols['total_qty'] = new Zend_Db_Expr('SUM(t.' . $column . ')'); + $periodSubSelect->from(array('t' => $mainTable), $cols) + ->group(array('t.store_id', $periodCol, 't.product_id')) + ->order(array('t.store_id', $periodCol, 'total_qty DESC')); + + $cols = $columns; + $cols[$column] = 't.total_qty'; + $cols['rating_pos'] = new Zend_Db_Expr( + "(@pos := IF(t.`store_id` <> @prevStoreId OR {$periodCol} <> @prevPeriod, 1, @pos+1))"); + $cols['prevStoreId'] = new Zend_Db_Expr('(@prevStoreId := t.`store_id`)'); + $cols['prevPeriod'] = new Zend_Db_Expr("(@prevPeriod := {$periodCol})"); + $ratingSubSelect->from($periodSubSelect, $cols); + + $cols = $columns; + $cols['period'] = $periodCol; + $cols[$column] = 't.' . $column; + $cols['rating_pos'] = 't.rating_pos'; + $ratingSelect->from($ratingSubSelect, $cols); + + $sql = $ratingSelect->insertFromSelect($aggregationTable, array_keys($cols)); + $adapter->query("SET @pos = 0, @prevStoreId = -1, @prevPeriod = '0000-00-00'"); + + $adapter->query($sql); + + return $this; + } + } diff --git a/app/code/core/Mage/Reports/Model/Resource/Product/Viewed/Collection.php b/app/code/core/Mage/Reports/Model/Resource/Product/Viewed/Collection.php index 4f228527c3..6c7476732b 100755 --- a/app/code/core/Mage/Reports/Model/Resource/Product/Viewed/Collection.php +++ b/app/code/core/Mage/Reports/Model/Resource/Product/Viewed/Collection.php @@ -34,6 +34,13 @@ */ class Mage_Reports_Model_Resource_Product_Viewed_Collection extends Mage_Reports_Model_Resource_Product_Collection { + /** + * List of store ids for the current collection will be filtered by + * + * @var array + */ + protected $_storeIds = array(); + /** * Join fields * @@ -73,6 +80,45 @@ public function setStoreIds($storeIds) $storeId = array_pop($storeIds); $this->setStoreId($storeId); $this->addStoreFilter($storeId); + $this->addStoreIds($storeId); + return $this; + } + + /** + * Add store ids to filter 'report_event' data by store + * + * @param array|int $storeIds + * @return Mage_Reports_Model_Resource_Product_Viewed_Collection + */ + public function addStoreIds($storeIds) + { + if (is_array($storeIds)) { + $this->_storeIds = array_merge($this->_storeIds, $storeIds); + } else { + $this->_storeIds[] = $storeIds; + } + return $this; + } + + /** + * Apply store filter + * + * @return Mage_Reports_Model_Resource_Product_Viewed_Collection + */ + protected function _applyStoreIds() + { + $this->getSelect()->where('store_id IN(?)', $this->_storeIds); return $this; } + + /** + * Apply filters + * + * @return Mage_Catalog_Model_Resource_Product_Collection + */ + protected function _beforeLoad() + { + $this->_applyStoreIds(); + return parent::_beforeLoad(); + } } diff --git a/app/code/core/Mage/Reports/Model/Resource/Quote/Collection.php b/app/code/core/Mage/Reports/Model/Resource/Quote/Collection.php index 4f435d4213..1c8a10241b 100755 --- a/app/code/core/Mage/Reports/Model/Resource/Quote/Collection.php +++ b/app/code/core/Mage/Reports/Model/Resource/Quote/Collection.php @@ -89,7 +89,7 @@ public function prepareForAbandonedReport($storeIds, $filter = null) ->addSubtotal($storeIds, $filter) ->addCustomerData($filter) ->setOrder('updated_at'); - if (is_array($storeIds)) { + if (is_array($storeIds) && !empty($storeIds)) { $this->addFieldToFilter('store_id', array('in' => $storeIds)); } @@ -139,7 +139,7 @@ public function prepareForProductsInCarts() ->joinInner( array('product_price' => $productAttrPriceTable), "product_price.entity_id = e.entity_id AND product_price.attribute_id = {$productAttrPriceId}", - array('price'=>'product_price.value')) + array('price' => new Zend_Db_Expr('product_price.value * main_table.base_to_global_rate'))) ->joinLeft( array('order_items' => new Zend_Db_Expr(sprintf('(%s)', $ordersSubSelect))), 'order_items.product_id = e.entity_id', diff --git a/app/code/core/Mage/Reports/Model/Resource/Report/Abstract.php b/app/code/core/Mage/Reports/Model/Resource/Report/Abstract.php index 50c98eb514..cde4d93a2f 100755 --- a/app/code/core/Mage/Reports/Model/Resource/Report/Abstract.php +++ b/app/code/core/Mage/Reports/Model/Resource/Report/Abstract.php @@ -368,7 +368,7 @@ public function getStoreTZOffsetQuery($table, $column, $from = null, $to = null, foreach ($periods as $offset => $timestamps) { $subParts = array(); foreach ($timestamps as $ts) { - $subParts[] = "($column between '{$ts['from']}' and '{$ts['to']}')"; + $subParts[] = "($column between {$ts['from']} and {$ts['to']})"; } $then = $this->_getWriteAdapter() @@ -393,16 +393,17 @@ protected function _getTZOffsetTransitions($timezone, $from = null, $to = null) $tzTransitions = array(); try { if (!empty($from)) { - $from = new Zend_Date($from, 'Y-m-d H:i:s'); + $from = new Zend_Date($from, Varien_Date::DATETIME_INTERNAL_FORMAT); $from = $from->getTimestamp(); } - $to = new Zend_Date($to); - $nextPeriod = $to->toString('c'); + $to = new Zend_Date($to, Varien_Date::DATETIME_INTERNAL_FORMAT); + $nextPeriod = $this->_getWriteAdapter()->formatDate($to->toString(Varien_Date::DATETIME_INTERNAL_FORMAT)); $to = $to->getTimestamp(); $dtz = new DateTimeZone($timezone); $transitions = $dtz->getTransitions(); + $dateTimeObject = new Zend_Date('c'); for ($i = count($transitions) - 1; $i >= 0; $i--) { $tr = $transitions[$i]; @@ -410,6 +411,9 @@ protected function _getTZOffsetTransitions($timezone, $from = null, $to = null) continue; } + $dateTimeObject->set($tr['time']); + $tr['time'] = $this->_getWriteAdapter() + ->formatDate($dateTimeObject->toString(Varien_Date::DATETIME_INTERNAL_FORMAT)); $tzTransitions[$tr['offset']][] = array('from' => $tr['time'], 'to' => $nextPeriod); if (!empty($from) && $tr['ts'] < $from) { diff --git a/app/code/core/Mage/Reports/Model/Resource/Report/Collection/Abstract.php b/app/code/core/Mage/Reports/Model/Resource/Report/Collection/Abstract.php new file mode 100644 index 0000000000..d6326e2d65 --- /dev/null +++ b/app/code/core/Mage/Reports/Model/Resource/Report/Collection/Abstract.php @@ -0,0 +1,287 @@ + + */ +class Mage_Reports_Model_Resource_Report_Collection_Abstract extends Mage_Core_Model_Resource_Db_Collection_Abstract +{ + /** + * From date + * + * @var string + */ + protected $_from = null; + + /** + * To date + * + * @var string + */ + protected $_to = null; + + /** + * Period + * + * @var string + */ + protected $_period = null; + + /** + * Store ids + * + * @var int|array + */ + protected $_storesIds = 0; + + /** + * Does filters should be applied + * + * @var bool + */ + protected $_applyFilters = true; + + /** + * Is totals + * + * @var bool + */ + protected $_isTotals = false; + + /** + * Is subtotals + * + * @var bool + */ + protected $_isSubTotals = false; + + /** + * Aggregated columns + * + * @var array + */ + protected $_aggregatedColumns = array(); + + /** + * Set array of columns that should be aggregated + * + * @param array $columns + * @return Mage_Sales_Model_Resource_Report_Collection_Abstract + */ + public function setAggregatedColumns(array $columns) + { + $this->_aggregatedColumns = $columns; + return $this; + } + + /** + * Retrieve array of columns that should be aggregated + * + * @return array + */ + public function getAggregatedColumns() + { + return $this->_aggregatedColumns; + } + + /** + * Set date range + * + * @param mixed $from + * @param mixed $to + * @return Mage_Sales_Model_Resource_Report_Collection_Abstract + */ + public function setDateRange($from = null, $to = null) + { + $this->_from = $from; + $this->_to = $to; + return $this; + } + + /** + * Set period + * + * @param string $period + * @return Mage_Sales_Model_Resource_Report_Collection_Abstract + */ + public function setPeriod($period) + { + $this->_period = $period; + return $this; + } + + /** + * Apply date range filter + * + * @return Mage_Sales_Model_Resource_Report_Collection_Abstract + */ + protected function _applyDateRangeFilter() + { + // Remember that field PERIOD is a DATE(YYYY-MM-DD) in all databases including Oracle + if ($this->_from !== null) { + $this->getSelect()->where('period >= ?', $this->_from); + } + if ($this->_to !== null) { + $this->getSelect()->where('period <= ?', $this->_to); + } + + return $this; + } + + /** + * Set store ids + * + * @param mixed $storeIds (null, int|string, array, array may contain null) + * @return Mage_Sales_Model_Resource_Report_Collection_Abstract + */ + public function addStoreFilter($storeIds) + { + $this->_storesIds = $storeIds; + return $this; + } + + /** + * Apply stores filter to select object + * + * @param Zend_Db_Select $select + * @return Mage_Sales_Model_Resource_Report_Collection_Abstract + */ + protected function _applyStoresFilterToSelect(Zend_Db_Select $select) + { + $nullCheck = false; + $storeIds = $this->_storesIds; + + if (!is_array($storeIds)) { + $storeIds = array($storeIds); + } + + $storeIds = array_unique($storeIds); + + if ($index = array_search(null, $storeIds)) { + unset($storeIds[$index]); + $nullCheck = true; + } + + $storeIds[0] = ($storeIds[0] == '') ? 0 : $storeIds[0]; + + if ($nullCheck) { + $select->where('store_id IN(?) OR store_id IS NULL', $storeIds); + } else { + $select->where('store_id IN(?)', $storeIds); + } + + return $this; + } + + /** + * Apply stores filter + * + * @return Mage_Sales_Model_Resource_Report_Collection_Abstract + */ + protected function _applyStoresFilter() + { + return $this->_applyStoresFilterToSelect($this->getSelect()); + } + + /** + * Set apply filters flag + * + * @param boolean $flag + * @return Mage_Sales_Model_Resource_Report_Collection_Abstract + */ + public function setApplyFilters($flag) + { + $this->_applyFilters = $flag; + return $this; + } + + /** + * Getter/Setter for isTotals + * + * @param null|boolean $flag + * @return Mage_Sales_Model_Resource_Report_Collection_Abstract + */ + public function isTotals($flag = null) + { + if (is_null($flag)) { + return $this->_isTotals; + } + $this->_isTotals = $flag; + return $this; + } + + /** + * Getter/Setter for isSubTotals + * + * @param null|boolean $flag + * @return Mage_Sales_Model_Resource_Report_Collection_Abstract + */ + public function isSubTotals($flag = null) + { + if (is_null($flag)) { + return $this->_isSubTotals; + } + $this->_isSubTotals = $flag; + return $this; + } + + /** + * Custom filters application ability + * + * @return Mage_Reports_Model_Resource_Report_Collection_Abstract + */ + protected function _applyCustomFilter() + { + return $this; + } + + /** + * Load data + * Redeclare parent load method just for adding method _beforeLoad + * + * @param bool $printQuery + * @param bool $logQuery + * @return Mage_Sales_Model_Resource_Report_Collection_Abstract + */ + public function load($printQuery = false, $logQuery = false) + { + if ($this->isLoaded()) { + return $this; + } + $this->_initSelect(); + if ($this->_applyFilters) { + $this->_applyDateRangeFilter(); + $this->_applyStoresFilter(); + $this->_applyCustomFilter(); + } + return parent::load($printQuery, $logQuery); + } +} diff --git a/app/code/core/Mage/Reports/Model/Resource/Report/Product/Viewed.php b/app/code/core/Mage/Reports/Model/Resource/Report/Product/Viewed.php new file mode 100644 index 0000000000..e4403e8717 --- /dev/null +++ b/app/code/core/Mage/Reports/Model/Resource/Report/Product/Viewed.php @@ -0,0 +1,212 @@ + + */ +class Mage_Reports_Model_Resource_Report_Product_Viewed extends Mage_Sales_Model_Resource_Report_Abstract +{ + const AGGREGATION_DAILY = 'reports/viewed_aggregated_daily'; + const AGGREGATION_MONTHLY = 'reports/viewed_aggregated_monthly'; + const AGGREGATION_YEARLY = 'reports/viewed_aggregated_yearly'; + + /** + * Model initialization + * + */ + protected function _construct() + { + $this->_init(self::AGGREGATION_DAILY, 'id'); + } + + /** + * Aggregate products view data + * + * @param mixed $from + * @param mixed $to + * @return Mage_Sales_Model_Resource_Report_Bestsellers + */ + public function aggregate($from = null, $to = null) + { + $mainTable = $this->getMainTable(); + $adapter = $this->_getWriteAdapter(); + + // convert input dates to UTC to be comparable with DATETIME fields in DB + $from = $this->_dateToUtc($from); + $to = $this->_dateToUtc($to); + + $this->_checkDates($from, $to); + + if ($from !== null || $to !== null) { + $subSelect = $this->_getTableDateRangeSelect( + $this->getTable('reports/event'), + 'logged_at', 'logged_at', $from, $to + ); + } else { + $subSelect = null; + } + $this->_clearTableByDateRange($mainTable, $from, $to, $subSelect); + // convert dates from UTC to current admin timezone + $periodExpr = $adapter->getDatePartSql( + $this->getStoreTZOffsetQuery( + array('source_table' => $this->getTable('reports/event')), + 'source_table.logged_at', $from, $to + ) + ); + + $helper = Mage::getResourceHelper('core'); + $select = $adapter->select(); + + $select->group(array( + $periodExpr, + 'source_table.store_id', + 'source_table.object_id' + )); + + $viewsNumExpr = new Zend_Db_Expr('COUNT(source_table.event_id)'); + + $columns = array( + 'period' => $periodExpr, + 'store_id' => 'source_table.store_id', + 'product_id' => 'source_table.object_id', + 'product_name' => new Zend_Db_Expr( + sprintf('MIN(%s)', + $adapter->getIfNullSql('product_name.value','product_default_name.value') + ) + ), + 'product_price' => new Zend_Db_Expr( + sprintf('%s', + $helper->prepareColumn( + sprintf('MIN(%s)', + $adapter->getIfNullSql( + $adapter->getIfNullSql('product_price.value','product_default_price.value'), 0) + ), + $select->getPart(Zend_Db_Select::GROUP) + ) + ) + ), + 'views_num' => $viewsNumExpr + ); + + $select + ->from( + array( + 'source_table' => $this->getTable('reports/event')), + $columns) + ->where('source_table.event_type_id = ?', Mage_Reports_Model_Event::EVENT_PRODUCT_VIEW); + + /** @var Mage_Catalog_Model_Resource_Product $product */ + $product = Mage::getResourceSingleton('catalog/product'); + + $select->joinInner( + array( + 'product' => $this->getTable('catalog/product')), + 'product.entity_id = source_table.object_id', + array() + ); + + // join product attributes Name & Price + $nameAttribute = $product->getAttribute('name'); + $joinExprProductName = array( + 'product_name.entity_id = product.entity_id', + 'product_name.store_id = source_table.store_id', + $adapter->quoteInto('product_name.attribute_id = ?', $nameAttribute->getAttributeId()) + ); + $joinExprProductName = implode(' AND ', $joinExprProductName); + $joinExprProductDefaultName = array( + 'product_default_name.entity_id = product.entity_id', + 'product_default_name.store_id = 0', + $adapter->quoteInto('product_default_name.attribute_id = ?', $nameAttribute->getAttributeId()) + ); + $joinExprProductDefaultName = implode(' AND ', $joinExprProductDefaultName); + $select->joinLeft( + array( + 'product_name' => $nameAttribute->getBackend()->getTable()), + $joinExprProductName, + array() + ) + ->joinLeft( + array( + 'product_default_name' => $nameAttribute->getBackend()->getTable()), + $joinExprProductDefaultName, + array() + ); + $priceAttribute = $product->getAttribute('price'); + $joinExprProductPrice = array( + 'product_price.entity_id = product.entity_id', + 'product_price.store_id = source_table.store_id', + $adapter->quoteInto('product_price.attribute_id = ?', $priceAttribute->getAttributeId()) + ); + $joinExprProductPrice = implode(' AND ', $joinExprProductPrice); + + $joinExprProductDefPrice = array( + 'product_default_price.entity_id = product.entity_id', + 'product_default_price.store_id = 0', + $adapter->quoteInto('product_default_price.attribute_id = ?', $priceAttribute->getAttributeId()) + ); + $joinExprProductDefPrice = implode(' AND ', $joinExprProductDefPrice); + $select->joinLeft( + array('product_price' => $priceAttribute->getBackend()->getTable()), + $joinExprProductPrice, + array() + ) + ->joinLeft( + array('product_default_price' => $priceAttribute->getBackend()->getTable()), + $joinExprProductDefPrice, + array() + ); + + $havingPart = array($adapter->prepareSqlCondition($viewsNumExpr, array('gt' => 0))); + if (!is_null($subSelect)) { + $subSelectHavingPart = $this->_makeConditionFromDateRangeSelect($subSelect, 'period'); + if ($subSelectHavingPart) { + $havingPart[] = '(' . $subSelectHavingPart . ')'; + } + } + $select->having(implode(' AND ', $havingPart)); + + $select->useStraightJoin(); + $insertQuery = $helper->getInsertFromSelectUsingAnalytic($select, $this->getMainTable(), + array_keys($columns)); + $adapter->query($insertQuery); + + Mage::getResourceHelper('reports') + ->updateReportRatingPos('day', 'views_num', $mainTable, $this->getTable(self::AGGREGATION_DAILY)); + Mage::getResourceHelper('reports') + ->updateReportRatingPos('month', 'views_num', $mainTable, $this->getTable(self::AGGREGATION_MONTHLY)); + Mage::getResourceHelper('reports') + ->updateReportRatingPos('year', 'views_num', $mainTable, $this->getTable(self::AGGREGATION_YEARLY)); + + $this->_setFlagData(Mage_Reports_Model_Flag::REPORT_PRODUCT_VIEWED_FLAG_CODE); + + return $this; + } +} diff --git a/app/code/core/Mage/Reports/Model/Resource/Report/Product/Viewed/Collection.php b/app/code/core/Mage/Reports/Model/Resource/Report/Product/Viewed/Collection.php new file mode 100644 index 0000000000..1f95ddd16b --- /dev/null +++ b/app/code/core/Mage/Reports/Model/Resource/Report/Product/Viewed/Collection.php @@ -0,0 +1,367 @@ +setModel('adminhtml/report_item'); + $this->_resource = Mage::getResourceModel('sales/report') + ->init(Mage_Reports_Model_Resource_Report_Product_Viewed::AGGREGATION_DAILY); + $this->setConnection($this->getResource()->getReadConnection()); + // overwrite default behaviour + $this->_applyFilters = false; + } + + /** + * Retrieve selected columns + * + * @return array + */ + protected function _getSelectedColumns() + { + $adapter = $this->getConnection(); + + if (!$this->_selectedColumns) { + if ($this->isTotals()) { + $this->_selectedColumns = $this->getAggregatedColumns(); + } else { + $this->_selectedColumns = array( + 'period' => sprintf('MAX(%s)', $adapter->getDateFormatSql('period', '%Y-%m-%d')), + 'views_num' => 'SUM(views_num)', + 'product_id' => 'product_id', + 'product_name' => 'MAX(product_name)', + 'product_price' => 'MAX(product_price)', + ); + if ('year' == $this->_period) { + $this->_selectedColumns['period'] = $adapter->getDateFormatSql('period', '%Y'); + } elseif ('month' == $this->_period) { + $this->_selectedColumns['period'] = $adapter->getDateFormatSql('period', '%Y-%m'); + } + } + } + return $this->_selectedColumns; + } + + /** + * Make select object for date boundary + * + * @param mixed $from + * @param mixed $to + * @return Zend_Db_Select + */ + protected function _makeBoundarySelect($from, $to) + { + $adapter = $this->getConnection(); + $cols = $this->_getSelectedColumns(); + $cols['views_num'] = 'SUM(views_num)'; + $select = $adapter->select() + ->from($this->getResource()->getMainTable(), $cols) + ->where('period >= ?', $from) + ->where('period <= ?', $to) + ->group('product_id') + ->order('views_num DESC') + ->limit($this->_ratingLimit); + + $this->_applyStoresFilterToSelect($select); + + return $select; + } + + /** + * Init collection select + * + * @return Mage_Reports_Model_Resource_Report_Product_Viewed_Collection + */ + protected function _initSelect() + { + $select = $this->getSelect(); + + // if grouping by product, not by period + if (!$this->_period) { + $cols = $this->_getSelectedColumns(); + $cols['views_num'] = 'SUM(views_num)'; + if ($this->_from || $this->_to) { + $mainTable = $this->getTable(Mage_Reports_Model_Resource_Report_Product_Viewed::AGGREGATION_DAILY); + $select->from($mainTable, $cols); + } else { + $mainTable = $this->getTable(Mage_Reports_Model_Resource_Report_Product_Viewed::AGGREGATION_YEARLY); + $select->from($mainTable, $cols); + } + + //exclude removed products + $subSelect = $this->getConnection()->select(); + $subSelect->from(array('existed_products' => $this->getTable('catalog/product')), new Zend_Db_Expr('1)')); + + $select->exists($subSelect, $mainTable . '.product_id = existed_products.entity_id') + ->group('product_id') + ->order('views_num ' . Varien_Db_Select::SQL_DESC) + ->limit($this->_ratingLimit); + + return $this; + } + + if ('year' == $this->_period) { + $mainTable = $this->getTable(Mage_Reports_Model_Resource_Report_Product_Viewed::AGGREGATION_YEARLY); + $select->from($mainTable, $this->_getSelectedColumns()); + } elseif ('month' == $this->_period) { + $mainTable = $this->getTable(Mage_Reports_Model_Resource_Report_Product_Viewed::AGGREGATION_MONTHLY); + $select->from($mainTable, $this->_getSelectedColumns()); + } else { + $mainTable = $this->getTable(Mage_Reports_Model_Resource_Report_Product_Viewed::AGGREGATION_DAILY); + $select->from($mainTable, $this->_getSelectedColumns()); + } + if (!$this->isTotals()) { + $select->group(array('period', 'product_id')); + } + $select->where('rating_pos <= ?', $this->_ratingLimit); + + return $this; + } + + /** + * Get SQL for get record count + * + * @return Varien_Db_Select + */ + public function getSelectCountSql() + { + $this->_renderFilters(); + $select = clone $this->getSelect(); + $select->reset(Zend_Db_Select::ORDER); + return $this->getConnection()->select()->from($select, 'COUNT(*)'); + } + + /** + * Set ids for store restrictions + * + * @param array $storeIds + * @return Mage_Reports_Model_Resource_Report_Product_Viewed_Collection + */ + public function addStoreRestrictions($storeIds) + { + if (!is_array($storeIds)) { + $storeIds = array($storeIds); + } + $currentStoreIds = $this->_storesIds; + if (isset($currentStoreIds) && $currentStoreIds != Mage_Core_Model_App::ADMIN_STORE_ID + && $currentStoreIds != array(Mage_Core_Model_App::ADMIN_STORE_ID)) { + if (!is_array($currentStoreIds)) { + $currentStoreIds = array($currentStoreIds); + } + $this->_storesIds = array_intersect($currentStoreIds, $storeIds); + } else { + $this->_storesIds = $storeIds; + } + + return $this; + } + + /** + * Redeclare parent method for applying filters after parent method + * but before adding unions and calculating totals + * + * @return Mage_Reports_Model_Resource_Report_Product_Viewed_Collection + */ + protected function _beforeLoad() + { + parent::_beforeLoad(); + + $this->_applyStoresFilter(); + + if ($this->_period) { + $selectUnions = array(); + + // apply date boundaries (before calling $this->_applyDateRangeFilter()) + $dtFormat = Varien_Date::DATE_INTERNAL_FORMAT; + $periodFrom = (!is_null($this->_from) ? new Zend_Date($this->_from, $dtFormat) : null); + $periodTo = (!is_null($this->_to) ? new Zend_Date($this->_to, $dtFormat) : null); + if ('year' == $this->_period) { + + if ($periodFrom) { + // not the first day of the year + if ($periodFrom->toValue(Zend_Date::MONTH) != 1 || $periodFrom->toValue(Zend_Date::DAY) != 1) { + $dtFrom = $periodFrom->getDate(); + // last day of the year + $dtTo = $periodFrom->getDate()->setMonth(12)->setDay(31); + if (!$periodTo || $dtTo->isEarlier($periodTo)) { + $selectUnions[] = $this->_makeBoundarySelect( + $dtFrom->toString($dtFormat), + $dtTo->toString($dtFormat) + ); + + // first day of the next year + $this->_from = $periodFrom->getDate() + ->addYear(1) + ->setMonth(1) + ->setDay(1) + ->toString($dtFormat); + } + } + } + + if ($periodTo) { + // not the last day of the year + if ($periodTo->toValue(Zend_Date::MONTH) != 12 || $periodTo->toValue(Zend_Date::DAY) != 31) { + $dtFrom = $periodTo->getDate()->setMonth(1)->setDay(1); // first day of the year + $dtTo = $periodTo->getDate(); + if (!$periodFrom || $dtFrom->isLater($periodFrom)) { + $selectUnions[] = $this->_makeBoundarySelect( + $dtFrom->toString($dtFormat), + $dtTo->toString($dtFormat) + ); + + // last day of the previous year + $this->_to = $periodTo->getDate() + ->subYear(1) + ->setMonth(12) + ->setDay(31) + ->toString($dtFormat); + } + } + } + + if ($periodFrom && $periodTo) { + // the same year + if ($periodFrom->toValue(Zend_Date::YEAR) == $periodTo->toValue(Zend_Date::YEAR)) { + $dtFrom = $periodFrom->getDate(); + $dtTo = $periodTo->getDate(); + $selectUnions[] = $this->_makeBoundarySelect( + $dtFrom->toString($dtFormat), + $dtTo->toString($dtFormat) + ); + + $this->getSelect()->where('1<>1'); + } + } + + } + else if ('month' == $this->_period) { + if ($periodFrom) { + // not the first day of the month + if ($periodFrom->toValue(Zend_Date::DAY) != 1) { + $dtFrom = $periodFrom->getDate(); + // last day of the month + $dtTo = $periodFrom->getDate()->addMonth(1)->setDay(1)->subDay(1); + if (!$periodTo || $dtTo->isEarlier($periodTo)) { + $selectUnions[] = $this->_makeBoundarySelect( + $dtFrom->toString($dtFormat), + $dtTo->toString($dtFormat) + ); + + // first day of the next month + $this->_from = $periodFrom->getDate()->addMonth(1)->setDay(1)->toString($dtFormat); + } + } + } + + if ($periodTo) { + // not the last day of the month + if ($periodTo->toValue(Zend_Date::DAY) != $periodTo->toValue(Zend_Date::MONTH_DAYS)) { + $dtFrom = $periodTo->getDate()->setDay(1); // first day of the month + $dtTo = $periodTo->getDate(); + if (!$periodFrom || $dtFrom->isLater($periodFrom)) { + $selectUnions[] = $this->_makeBoundarySelect( + $dtFrom->toString($dtFormat), + $dtTo->toString($dtFormat) + ); + + // last day of the previous month + $this->_to = $periodTo->getDate()->setDay(1)->subDay(1)->toString($dtFormat); + } + } + } + + if ($periodFrom && $periodTo) { + // the same month + if ($periodFrom->toValue(Zend_Date::YEAR) == $periodTo->toValue(Zend_Date::YEAR) + && $periodFrom->toValue(Zend_Date::MONTH) == $periodTo->toValue(Zend_Date::MONTH) + ) { + $dtFrom = $periodFrom->getDate(); + $dtTo = $periodTo->getDate(); + $selectUnions[] = $this->_makeBoundarySelect( + $dtFrom->toString($dtFormat), + $dtTo->toString($dtFormat) + ); + + $this->getSelect()->where('1<>1'); + } + } + + } + + $this->_applyDateRangeFilter(); + + // add unions to select + if ($selectUnions) { + $unionParts = array(); + $cloneSelect = clone $this->getSelect(); + $helper = Mage::getResourceHelper('core'); + $unionParts[] = '(' . $cloneSelect . ')'; + foreach ($selectUnions as $union) { + $query = $helper->getQueryUsingAnalyticFunction($union); + $unionParts[] = '(' . $query . ')'; + } + $this->getSelect()->reset()->union($unionParts, Zend_Db_Select::SQL_UNION_ALL); + } + + if ($this->isTotals()) { + // calculate total + $cloneSelect = clone $this->getSelect(); + $this->getSelect()->reset()->from($cloneSelect, $this->getAggregatedColumns()); + } else { + // add sorting + $this->getSelect()->order(array('period ASC', 'views_num DESC')); + } + } + + return $this; + } +} diff --git a/app/code/core/Mage/Reports/Model/Resource/Tag/Collection.php b/app/code/core/Mage/Reports/Model/Resource/Tag/Collection.php index 7be1b75693..a119a8f54f 100755 --- a/app/code/core/Mage/Reports/Model/Resource/Tag/Collection.php +++ b/app/code/core/Mage/Reports/Model/Resource/Tag/Collection.php @@ -57,12 +57,13 @@ public function addPopularity($storeIds) $select = $this->getSelect() ->joinLeft( array('tr' => $this->getTable('tag/relation')), - 'main_table.tag_id = tr.tag_id', + 'main_table.tag_id = tr.tag_id AND tr.active = 1', array('popularity' => 'COUNT(tr.tag_id)') ); if (!empty($storeIds)) { $select->where('tr.store_id IN(?)', $storeIds); } + $select->group('main_table.tag_id'); /** diff --git a/app/code/core/Mage/Reports/etc/config.xml b/app/code/core/Mage/Reports/etc/config.xml index 5956512fb8..64f21bbd15 100644 --- a/app/code/core/Mage/Reports/etc/config.xml +++ b/app/code/core/Mage/Reports/etc/config.xml @@ -28,7 +28,7 @@ - 1.6.0.0 + 1.6.0.0.1 @@ -53,6 +53,15 @@ report_viewed_product_index
+ + report_viewed_product_aggregated_daily
+
+ + report_viewed_product_aggregated_monthly
+
+ + report_viewed_product_aggregated_yearly
+
diff --git a/app/code/core/Mage/Reports/sql/reports_setup/upgrade-1.6.0.0-1.6.0.0.1.php b/app/code/core/Mage/Reports/sql/reports_setup/upgrade-1.6.0.0-1.6.0.0.1.php new file mode 100644 index 0000000000..be07726725 --- /dev/null +++ b/app/code/core/Mage/Reports/sql/reports_setup/upgrade-1.6.0.0-1.6.0.0.1.php @@ -0,0 +1,99 @@ +startSetup(); + +$aggregationTables = array( + Mage_Reports_Model_Resource_Report_Product_Viewed::AGGREGATION_DAILY, + Mage_Reports_Model_Resource_Report_Product_Viewed::AGGREGATION_MONTHLY, + Mage_Reports_Model_Resource_Report_Product_Viewed::AGGREGATION_YEARLY, +); +$aggregationTableComments = array( + 'Most Viewed Products Aggregated Daily', + 'Most Viewed Products Aggregated Monthly', + 'Most Viewed Products Aggregated Yearly', +); + +for ($i = 0; $i < 3; ++$i) { + $table = $installer->getConnection() + ->newTable($installer->getTable($aggregationTables[$i])) + ->addColumn('id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array( + 'identity' => true, + 'unsigned' => true, + 'nullable' => false, + 'primary' => true, + ), 'Id') + ->addColumn('period', Varien_Db_Ddl_Table::TYPE_DATE, null, array( + ), 'Period') + ->addColumn('store_id', Varien_Db_Ddl_Table::TYPE_SMALLINT, null, array( + 'unsigned' => true, + ), 'Store Id') + ->addColumn('product_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array( + 'unsigned' => true, + ), 'Product Id') + ->addColumn('product_name', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array( + 'nullable' => true, + ), 'Product Name') + ->addColumn('product_price', Varien_Db_Ddl_Table::TYPE_DECIMAL, '12,4', array( + 'nullable' => false, + 'default' => '0.0000', + ), 'Product Price') + ->addColumn('views_num', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array( + 'nullable' => false, + 'default' => '0', + ), 'Number of Views') + ->addColumn('rating_pos', Varien_Db_Ddl_Table::TYPE_SMALLINT, null, array( + 'unsigned' => true, + 'nullable' => false, + 'default' => '0', + ), 'Rating Pos') + ->addIndex( + $installer->getIdxName( + $aggregationTables[$i], + array('period', 'store_id', 'product_id'), + Varien_Db_Adapter_Interface::INDEX_TYPE_UNIQUE + ), + array('period', 'store_id', 'product_id'), array('type' => Varien_Db_Adapter_Interface::INDEX_TYPE_UNIQUE)) + ->addIndex($installer->getIdxName($aggregationTables[$i], array('store_id')), array('store_id')) + ->addIndex($installer->getIdxName($aggregationTables[$i], array('product_id')), array('product_id')) + ->addForeignKey( + $installer->getFkName($aggregationTables[$i], 'store_id', 'core/store', 'store_id'), + 'store_id', $installer->getTable('core/store'), 'store_id', + Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE) + ->addForeignKey( + $installer->getFkName($aggregationTables[$i], 'product_id', 'catalog/product', 'entity_id'), + 'product_id', $installer->getTable('catalog/product'), 'entity_id', + Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE) + ->setComment($aggregationTableComments[$i]); + $installer->getConnection()->createTable($table); +} + +$installer->endSetup(); diff --git a/app/code/core/Mage/Review/Model/Review.php b/app/code/core/Mage/Review/Model/Review.php index 949a29f5d9..116b32bbaa 100644 --- a/app/code/core/Mage/Review/Model/Review.php +++ b/app/code/core/Mage/Review/Model/Review.php @@ -119,18 +119,16 @@ public function validate() { $errors = array(); - $helper = Mage::helper('customer'); - if (!Zend_Validate::is($this->getTitle(), 'NotEmpty')) { - $errors[] = $helper->__('Review summary can\'t be empty'); + $errors[] = Mage::helper('review')->__('Review summary can\'t be empty'); } if (!Zend_Validate::is($this->getNickname(), 'NotEmpty')) { - $errors[] = $helper->__('Nickname can\'t be empty'); + $errors[] = Mage::helper('review')->__('Nickname can\'t be empty'); } if (!Zend_Validate::is($this->getDetail(), 'NotEmpty')) { - $errors[] = $helper->__('Review can\'t be empty'); + $errors[] = Mage::helper('review')->__('Review can\'t be empty'); } if (empty($errors)) { diff --git a/app/code/core/Mage/Review/controllers/ProductController.php b/app/code/core/Mage/Review/controllers/ProductController.php index 4cf922e378..bfa26a6de9 100644 --- a/app/code/core/Mage/Review/controllers/ProductController.php +++ b/app/code/core/Mage/Review/controllers/ProductController.php @@ -75,6 +75,9 @@ protected function _initProduct() $productId = (int) $this->getRequest()->getParam('id'); $product = $this->_loadProduct($productId); + if (!$product) { + return false; + } if ($categoryId) { $category = Mage::getModel('catalog/category')->load($categoryId); @@ -83,7 +86,10 @@ protected function _initProduct() try { Mage::dispatchEvent('review_controller_product_init', array('product'=>$product)); - Mage::dispatchEvent('review_controller_product_init_after', array('product'=>$product, 'controller_action' => $this)); + Mage::dispatchEvent('review_controller_product_init_after', array( + 'product' => $product, + 'controller_action' => $this + )); } catch (Mage_Core_Exception $e) { Mage::logException($e); return false; diff --git a/app/code/core/Mage/Rss/Block/Catalog/New.php b/app/code/core/Mage/Rss/Block/Catalog/New.php index 50c988ff45..10f9abb4f7 100644 --- a/app/code/core/Mage/Rss/Block/Catalog/New.php +++ b/app/code/core/Mage/Rss/Block/Catalog/New.php @@ -65,20 +65,31 @@ protected function _toHtml() */ $product = Mage::getModel('catalog/product'); - $todayDate = $product->getResource()->formatDate(time()); + + $todayStartOfDayDate = Mage::app()->getLocale()->date() + ->setTime('00:00:00') + ->toString(Varien_Date::DATETIME_INTERNAL_FORMAT); + + $todayEndOfDayDate = Mage::app()->getLocale()->date() + ->setTime('23:59:59') + ->toString(Varien_Date::DATETIME_INTERNAL_FORMAT); $products = $product->getCollection() ->setStoreId($storeId) ->addStoreFilter() - - ->addAttributeToFilter('news_from_date', array('date'=>true, 'to'=> $todayDate)) + ->addAttributeToFilter('news_from_date', array('or' => array( + 0 => array('date' => true, 'to' => $todayEndOfDayDate), + 1 => array('is' => new Zend_Db_Expr('null'))) + ), 'left') + ->addAttributeToFilter('news_to_date', array('or' => array( + 0 => array('date' => true, 'from' => $todayStartOfDayDate), + 1 => array('is' => new Zend_Db_Expr('null'))) + ), 'left') ->addAttributeToFilter( array( - array('attribute'=>'news_to_date', 'date'=>true, 'from'=>$todayDate), - array('attribute'=>'news_to_date', 'is' => new Zend_Db_Expr('null')) - ), - '', - 'left' + array('attribute' => 'news_from_date', 'is' => new Zend_Db_Expr('not null')), + array('attribute' => 'news_to_date', 'is' => new Zend_Db_Expr('not null')) + ) ) ->addAttributeToSort('news_from_date','desc') ->addAttributeToSelect(array('name', 'short_description', 'description', 'thumbnail'), 'inner') diff --git a/app/code/core/Mage/Rss/Block/Catalog/NotifyStock.php b/app/code/core/Mage/Rss/Block/Catalog/NotifyStock.php index b2f824d6dc..106e7e5090 100644 --- a/app/code/core/Mage/Rss/Block/Catalog/NotifyStock.php +++ b/app/code/core/Mage/Rss/Block/Catalog/NotifyStock.php @@ -64,9 +64,7 @@ protected function _construct() protected function _toHtml() { $newUrl = Mage::getUrl('rss/catalog/notifystock'); - /* @var $helper Mage_Rss_Helper_Data */ - $helper = Mage::helper('rss'); - $title = $helper->__('Low Stock Products'); + $title = Mage::helper('rss')->__('Low Stock Products'); $rssObj = Mage::getModel('rss/rss'); $data = array( @@ -81,7 +79,7 @@ protected function _toHtml() Mage_CatalogInventory_Model_Stock_Item::XML_PATH_MANAGE_STOCK); $globalNotifyStockQty = (float) Mage::getStoreConfig( Mage_CatalogInventory_Model_Stock_Item::XML_PATH_NOTIFY_STOCK_QTY); - $helper->disableFlat(); + Mage::helper('rss')->disableFlat(); /* @var $product Mage_Catalog_Model_Product */ $product = Mage::getModel('catalog/product'); /* @var $collection Mage_Catalog_Model_Resource_Product_Collection */ diff --git a/app/code/core/Mage/Rss/Block/Catalog/Review.php b/app/code/core/Mage/Rss/Block/Catalog/Review.php index 456cd3cf09..006caad9a0 100644 --- a/app/code/core/Mage/Rss/Block/Catalog/Review.php +++ b/app/code/core/Mage/Rss/Block/Catalog/Review.php @@ -64,10 +64,8 @@ protected function _construct() protected function _toHtml() { $newUrl = Mage::getUrl('rss/catalog/review'); - /* @var $helper Mage_Rss_Helper_Data */ - $helper = Mage::helper('rss'); - $title = $helper->__('Pending product review(s)'); - $helper->disableFlat(); + $title = Mage::helper('rss')->__('Pending product review(s)'); + Mage::helper('rss')->disableFlat(); $rssObj = Mage::getModel('rss/rss'); $data = array( diff --git a/app/code/core/Mage/Rss/Block/Wishlist.php b/app/code/core/Mage/Rss/Block/Wishlist.php index cd93335c25..9f57d8bd51 100644 --- a/app/code/core/Mage/Rss/Block/Wishlist.php +++ b/app/code/core/Mage/Rss/Block/Wishlist.php @@ -110,33 +110,32 @@ protected function _toHtml() 'language' => $lang )); - /** @var $product Mage_Wishlist_Model_Item*/ - foreach ($this->getWishlistItems() as $product) { + /** @var $wishlistItem Mage_Wishlist_Model_Item*/ + foreach ($this->getWishlistItems() as $wishlistItem) { + /* @var $product Mage_Catalog_Model_Product */ + $product = $wishlistItem->getProduct(); $productUrl = $this->getProductUrl($product); - - /* @var $wishlistProduct Mage_Catalog_Model_Product */ - $wishlistProduct = $product->getProduct(); - $wishlistProduct->setAllowedInRss(true); - $wishlistProduct->setAllowedPriceInRss(true); - $wishlistProduct->setProductUrl($productUrl); - $args = array('product'=>$wishlistProduct); + $product->setAllowedInRss(true); + $product->setAllowedPriceInRss(true); + $product->setProductUrl($productUrl); + $args = array('product' => $product); Mage::dispatchEvent('rss_wishlist_xml_callback', $args); - if (!$wishlistProduct->getAllowedInRss()) { + if (!$product->getAllowedInRss()) { continue; } $description = '' . ' + + XOF + KG + CM + Togo + + + + TJS + KG + CM + AP + Tajikistan + + + USD + KG + CM + East Timor + + + TND + KG + CM + Tunisia + + + TOP + KG + CM + Tonga + + + TRY + KG + CM + AP + Turkey + + + TTD + KG + CM + AM + Trinidad and Tobago + + + AUD + KG + CM + Tuvalu + + + TWD + KG + CM + AP + Taiwan + + + TZS + KG + CM + Tanzania + + + UAH + KG + CM + AP + Ukraine + + + UGX + KG + CM + Uganda + + + USD + LB + IN + AM + United States Of America + + + UYU + KG + CM + AM + Uruguay + + + UZS + KG + CM + AP + Uzbekistan + + + XCD + KG + CM + AM + St. Vincent + + + VEF + KG + CM + AM + Venezuela + + + USD + KG + CM + AM + Virgin Islands (British) + + + USD + LB + IN + Virgin Islands (US) + + + VND + KG + CM + AP + Vietnam + + + VUV + KG + CM + Vanuatu + + + WST + KG + CM + Samoa + + + EUR + KG + CM + Bonaire + + + EUR + KG + CM + AM + Curacao + + + ANG + KG + CM + St. Eustatius + + + EUR + KG + CM + AM + St. Maarten + + + XCD + KG + CM + AM + Nevis + + + SIS + KG + CM + Somaliland, Rep of (North Somalia) + + + ANG + KG + CM + AM + St. Barthelemy + + + YER + KG + CM + AP + Yemen, Republic of + + + EUR + KG + CM + Mayotte + + + ZAR + KG + CM + AP + South Africa + + + ZMK + KG + CM + Zambia + + + ZWD + KG + CM + Zimbabwe + + diff --git a/app/code/core/Mage/Usa/etc/system.xml b/app/code/core/Mage/Usa/etc/system.xml index eecf43971d..613205f3f2 100644 --- a/app/code/core/Mage/Usa/etc/system.xml +++ b/app/code/core/Mage/Usa/etc/system.xml @@ -30,7 +30,7 @@ - + text 130 1 @@ -215,7 +215,7 @@ 1 1 0 - 0 + <label>Title</label> <frontend_type>text</frontend_type> @@ -801,7 +801,7 @@ <show_in_website>1</show_in_website> <show_in_store>0</show_in_store> </type> - <unit_of_measure translate="label"> + <unit_of_measure translate="label comment"> <label>Weight Unit</label> <frontend_type>select</frontend_type> <source_model>usa/shipping_carrier_ups_source_unitofmeasure</source_model> @@ -811,7 +811,7 @@ <show_in_store>0</show_in_store> </unit_of_measure> <username translate="label"> - <label>UserId</label> + <label>User ID</label> <frontend_type>obscure</frontend_type> <backend_model>adminhtml/system_config_backend_encrypted</backend_model> <sort_order>30</sort_order> @@ -1146,8 +1146,287 @@ 0</sort_order> </fields> </usps> + <dhlint translate="label" module="usa"> + <label>DHL</label> + <frontend_type>text</frontend_type> + <sort_order>140</sort_order> + <show_in_default>1</show_in_default> + <show_in_website>1</show_in_website> + <show_in_store>1</show_in_store> + <fields> + <active translate="label"> + <label>Enabled for Checkout</label> + <frontend_type>select</frontend_type> + <source_model>adminhtml/system_config_source_yesno</source_model> + <sort_order>10</sort_order> + <show_in_default>1</show_in_default> + <show_in_website>1</show_in_website> + <show_in_store>0</show_in_store> + </active> + <gateway_url translate="label"> + <label>Gateway URL</label> + <frontend_type>text</frontend_type> + <sort_order>20</sort_order> + <show_in_default>1</show_in_default> + <show_in_website>1</show_in_website> + <show_in_store>0</show_in_store> + </gateway_url> + <title translate="label"> + <label>Title</label> + <frontend_type>text</frontend_type> + <sort_order>20</sort_order> + <show_in_default>1</show_in_default> + <show_in_website>1</show_in_website> + <show_in_store>1</show_in_store> + + + + obscure + adminhtml/system_config_backend_encrypted + 50 + 1 + 1 + 0 + + + + obscure + adminhtml/system_config_backend_encrypted + 60 + 1 + 1 + 0 + + + + text + 70 + 1 + 1 + 0 + + + + select + usa/shipping_carrier_dhl_international_source_contenttype + 90 + 1 + 1 + 0 + + + + select + shipping/source_handlingType + 100 + 1 + 1 + 0 + + + + "Per Order" allows single handling fee for entire order. "Per Package" allows individual handling fee for each package. + select + shipping/source_handlingAction + 110 + 1 + 1 + 0 + + + + text + 120 + 1 + 1 + 0 + + + + Allows breaking total order weight into smaller pieces if it exeeds 70 kg to ensure accurate calculation of shipping charges. + select + adminhtml/system_config_source_yesno + 130 + 1 + 1 + 1 + + + + select + usa/shipping_carrier_dhl_international_source_method_unitofmeasure + usa/adminhtml_dhl_unitofmeasure + 140 + 1 + 1 + 1 + + + + select + usa/shipping_carrier_dhl_international_source_method_size + 150 + 1 + 1 + 1 + + + + text + 151 + 1 + 1 + 1 + 1 + + + + text + 152 + 1 + 1 + 1 + 1 + + + + text + 153 + 1 + 1 + 1 + 1 + + + + multiselect + usa/shipping_carrier_dhl_international_source_method_doc + 170 + 1 + 1 + 0 + D + + + + multiselect + usa/shipping_carrier_dhl_international_source_method_nondoc + 170 + 1 + 1 + 0 + N + + + + Package ready time after order submission (in hours) + text + 180 + 1 + 1 + 0 + + + + textarea + 800 + 1 + 1 + 1 + + + + + select + free-method + usa/shipping_carrier_dhl_international_source_method_freedoc + 1200 + 1 + 1 + 0 + D + + + + select + free-method + usa/shipping_carrier_dhl_international_source_method_freenondoc + 1200 + 1 + 1 + 0 + N + + + + select + adminhtml/system_config_source_enabledisable + 1210 + 1 + 1 + 0 + + + + text + 1220 + 1 + 1 + 0 + + + + select + 1900 + shipping-applicable-country + adminhtml/system_config_source_shipping_allspecificcountries + 1 + 1 + 0 + + + + multiselect + 1910 + adminhtml/system_config_source_country + 1 + 1 + 0 + 1 + + + + select + 1940 + shipping-skip-hide + adminhtml/system_config_source_yesno + 1 + 1 + 0 + + + + text + 2000 + 1 + 1 + 0 + + + + select + adminhtml/system_config_source_yesno + 1950 + 1 + 1 + 0 + + + - diff --git a/app/code/core/Mage/Weee/Helper/Data.php b/app/code/core/Mage/Weee/Helper/Data.php index 2e63651966..4f119e59d9 100644 --- a/app/code/core/Mage/Weee/Helper/Data.php +++ b/app/code/core/Mage/Weee/Helper/Data.php @@ -26,6 +26,10 @@ /** * WEEE data helper + * + * @category Mage + * @package Mage_Weee + * @author Magento Core Team */ class Mage_Weee_Helper_Data extends Mage_Core_Helper_Abstract { @@ -130,9 +134,17 @@ public function getAmount($product, $shipping = null, $billing = null, $website return 0; } + /** + * Returns diaplay type for price accordingly to current zone + * + * @param Mage_Catalog_Model_Product $product + * @param array|null $compareTo + * @param string $zone + * @param Mage_Core_Model_Store $store + * @return bool|int + */ public function typeOfDisplay($product, $compareTo = null, $zone = null, $store = null) { - $type = 0; if (!$this->isEnabled($store)) { return false; } @@ -169,6 +181,16 @@ public function typeOfDisplay($product, $compareTo = null, $zone = null, $store } } + /** + * Proxy for Mage_Weee_Model_Tax::getProductWeeeAttributes() + * + * @param Mage_Catalog_Model_Product $product + * @param null|false|Varien_Object $shipping + * @param null|false|Varien_Object $billing + * @param Mage_Core_Model_Website $website + * @param bool $calculateTaxes + * @return array + */ public function getProductWeeeAttributes($product, $shipping = null, $billing = null, $website = null, $calculateTaxes = false) { @@ -176,6 +198,12 @@ public function getProductWeeeAttributes($product, $shipping = null, $billing = ->getProductWeeeAttributes($product, $shipping, $billing, $website, $calculateTaxes); } + /** + * Returns applied weee taxes + * + * @param Mage_Sales_Model_Quote_Item_Abstract $item + * @return array + */ public function getApplied($item) { if ($item instanceof Mage_Sales_Model_Quote_Item_Abstract) { @@ -202,12 +230,25 @@ public function getApplied($item) return unserialize($item->getWeeeTaxApplied()); } + /** + * Sets applied weee taxes + * + * @param Mage_Sales_Model_Quote_Item_Abstract $item + * @param array $value + * @return Mage_Weee_Helper_Data + */ public function setApplied($item, $value) { $item->setWeeeTaxApplied(serialize($value)); return $this; } + /** + * Returns array of weee attributes allowed for display + * + * @param Mage_Catalog_Model_Product $product + * @return array + */ public function getProductWeeeAttributesForDisplay($product) { if ($this->isEnabled()) { @@ -241,6 +282,12 @@ public function getProductWeeeAttributesForRenderer($product, $shipping = null, return array(); } + /** + * Returns amount to display + * + * @param Mage_Catalog_Model_Product $product + * @return int + */ public function getAmountForDisplay($product) { if ($this->isEnabled()) { @@ -250,6 +297,12 @@ public function getAmountForDisplay($product) return 0; } + /** + * Returns original amount + * + * @param Mage_Catalog_Model_Product $product + * @return int + */ public function getOriginalAmount($product) { if ($this->isEnabled()) { @@ -258,15 +311,24 @@ public function getOriginalAmount($product) return 0; } + /** + * Adds HTML containers and formats tier prices accordingly to the currency used + * + * @param Mage_Catalog_Model_Product $product + * @param array $tierPrices + * @return Mage_Weee_Helper_Data + */ public function processTierPrices($product, &$tierPrices) { $weeeAmount = $this->getAmountForDisplay($product); $store = Mage::app()->getStore(); - foreach ($tierPrices as &$tier) { - $tier['formated_price_incl_weee'] = $store->formatPrice($store - ->convertPrice(Mage::helper('tax')->getPrice($product, $tier['website_price'], true)+$weeeAmount)); - $tier['formated_price_incl_weee_only'] = $store->formatPrice($store - ->convertPrice(Mage::helper('tax')->getPrice($product, $tier['website_price'])+$weeeAmount)); + foreach ($tierPrices as $index => &$tier) { + $html = $store->formatPrice($store->convertPrice( + Mage::helper('tax')->getPrice($product, $tier['website_price'], true)+$weeeAmount), false); + $tier['formated_price_incl_weee'] = '' . $html . ''; + $html = $store->formatPrice($store->convertPrice( + Mage::helper('tax')->getPrice($product, $tier['website_price'])+$weeeAmount), false); + $tier['formated_price_incl_weee_only'] = '' . $html . ''; $tier['formated_weee'] = $store->formatPrice($store->convertPrice($weeeAmount)); } return $this; @@ -275,6 +337,7 @@ public function processTierPrices($product, &$tierPrices) /** * Check if fixed taxes are used in system * + * @param Mage_Core_Model_Store $store * @return bool */ public function isEnabled($store = null) diff --git a/app/code/core/Mage/Weee/Model/Observer.php b/app/code/core/Mage/Weee/Model/Observer.php index 1921d0ddd2..d7bbc01843 100644 --- a/app/code/core/Mage/Weee/Model/Observer.php +++ b/app/code/core/Mage/Weee/Model/Observer.php @@ -120,7 +120,7 @@ public function prepareCatalogIndexSelect(Varien_Event_Observer $observer) $fieldAlias = sprintf('weee_%s_table.value', $attribute); $checkAdditionalCalculation = $select->getAdapter()->getCheckSql("{$fieldAlias} IS NULL", 0, $fieldAlias); if (Mage::helper('weee')->isDiscounted()) { - $additionalCalculations[] = sprintf('+(%s*(1-(%s/100))', $checkAdditionalCalculation, $checkDiscountField); + $additionalCalculations[] = sprintf('+(%s*(1-(%s/100)))', $checkAdditionalCalculation, $checkDiscountField); } else { $additionalCalculations[] = "+($checkAdditionalCalculation)"; } diff --git a/app/code/core/Mage/Widget/Block/Adminhtml/Widget/Chooser.php b/app/code/core/Mage/Widget/Block/Adminhtml/Widget/Chooser.php index a84f0d8af6..9b5f279c42 100644 --- a/app/code/core/Mage/Widget/Block/Adminhtml/Widget/Chooser.php +++ b/app/code/core/Mage/Widget/Block/Adminhtml/Widget/Chooser.php @@ -180,13 +180,23 @@ protected function _toHtml() - + '; } } diff --git a/app/code/core/Mage/Widget/Block/Adminhtml/Widget/Instance/Edit/Chooser/Layout.php b/app/code/core/Mage/Widget/Block/Adminhtml/Widget/Instance/Edit/Chooser/Layout.php index ebf98c74f0..60144db9c1 100644 --- a/app/code/core/Mage/Widget/Block/Adminhtml/Widget/Instance/Edit/Chooser/Layout.php +++ b/app/code/core/Mage/Widget/Block/Adminhtml/Widget/Instance/Edit/Chooser/Layout.php @@ -128,7 +128,8 @@ protected function _toHtml() ->setName($this->getSelectName()) ->setId('layout_handle') ->setClass('required-entry select') - ->setExtraParams("onchange=\"WidgetInstance.loadSelectBoxByType(\'block_reference\', this.up(\'div.pages\'), this.value)\"") + ->setExtraParams("onchange=\"WidgetInstance.loadSelectBoxByType(\'block_reference\', " . + "this.up(\'div.pages\'), this.value)\"") ->setOptions($this->getLayoutHandles( $this->getArea(), $this->getPackage(), @@ -166,7 +167,9 @@ protected function _collectLayoutHandles($layoutHandles) foreach ($layoutHandlesArr as $node) { if ($this->_filterLayoutHandle($node->getName())) { $helper = Mage::helper(Mage_Core_Model_Layout::findTranslationModuleName($node)); - $this->_layoutHandles[$node->getName()] = $helper->__((string)$node->label); + $this->_layoutHandles[$node->getName()] = $this->helper('core')->jsQuoteEscape( + $helper->__((string)$node->label) + ); } } asort($this->_layoutHandles, SORT_STRING); diff --git a/app/code/core/Mage/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main/Layout.php b/app/code/core/Mage/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main/Layout.php index 6797219574..9bcfe2f7f6 100644 --- a/app/code/core/Mage/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main/Layout.php +++ b/app/code/core/Mage/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main/Layout.php @@ -150,45 +150,45 @@ protected function _getDisplayOnOptions() $options = array(); $options[] = array( 'value' => '', - 'label' => Mage::helper('widget')->__('-- Please Select --') + 'label' => $this->helper('core')->jsQuoteEscape(Mage::helper('widget')->__('-- Please Select --')) ); $options[] = array( 'label' => Mage::helper('widget')->__('Categories'), 'value' => array( array( 'value' => 'anchor_categories', - 'label' => Mage::helper('widget')->__('Anchor Categories') + 'label' => $this->helper('core')->jsQuoteEscape(Mage::helper('widget')->__('Anchor Categories')) ), array( 'value' => 'notanchor_categories', - 'label' => Mage::helper('widget')->__('Non-Anchor Categories') + 'label' => $this->helper('core')->jsQuoteEscape(Mage::helper('widget')->__('Non-Anchor Categories')) ) ) ); foreach (Mage_Catalog_Model_Product_Type::getTypes() as $typeId => $type) { $productsOptions[] = array( 'value' => $typeId.'_products', - 'label' => $type['label'] + 'label' => $this->helper('core')->jsQuoteEscape($type['label']) ); } array_unshift($productsOptions, array( 'value' => 'all_products', - 'label' => Mage::helper('widget')->__('All Product Types') + 'label' => $this->helper('core')->jsQuoteEscape(Mage::helper('widget')->__('All Product Types')) )); $options[] = array( - 'label' => Mage::helper('widget')->__('Products'), + 'label' => $this->helper('core')->jsQuoteEscape(Mage::helper('widget')->__('Products')), 'value' => $productsOptions ); $options[] = array( - 'label' => Mage::helper('widget')->__('Generic Pages'), + 'label' => $this->helper('core')->jsQuoteEscape(Mage::helper('widget')->__('Generic Pages')), 'value' => array( array( 'value' => 'all_pages', - 'label' => Mage::helper('widget')->__('All Pages') + 'label' => $this->helper('core')->jsQuoteEscape(Mage::helper('widget')->__('All Pages')) ), array( 'value' => 'pages', - 'label' => Mage::helper('widget')->__('Specified Page') + 'label' => $this->helper('core')->jsQuoteEscape(Mage::helper('widget')->__('Specified Page')) ) ) ); @@ -281,7 +281,7 @@ public function getRemoveLayoutButtonHtml() { $button = $this->getLayout()->createBlock('adminhtml/widget_button') ->setData(array( - 'label' => Mage::helper('widget')->__('Remove Layout Update'), + 'label' => $this->helper('core')->jsQuoteEscape(Mage::helper('widget')->__('Remove Layout Update')), 'onclick' => 'WidgetInstance.removePageGroup(this)', 'class' => 'delete' )); diff --git a/app/code/core/Mage/Widget/Model/Resource/Widget/Instance/Collection.php b/app/code/core/Mage/Widget/Model/Resource/Widget/Instance/Collection.php index cc78826856..00d1567a03 100755 --- a/app/code/core/Mage/Widget/Model/Resource/Widget/Instance/Collection.php +++ b/app/code/core/Mage/Widget/Model/Resource/Widget/Instance/Collection.php @@ -34,6 +34,14 @@ */ class Mage_Widget_Model_Resource_Widget_Instance_Collection extends Mage_Core_Model_Resource_Db_Collection_Abstract { + /** + * Fields map for corellation names & real selected fields + * + * @var array + */ + protected $_map = array('fields' => array('type' => 'instance_type')); + + /** * Constructor * diff --git a/app/code/core/Mage/Widget/controllers/Adminhtml/Widget/InstanceController.php b/app/code/core/Mage/Widget/controllers/Adminhtml/Widget/InstanceController.php index f77bd1873e..4f1de1a2aa 100644 --- a/app/code/core/Mage/Widget/controllers/Adminhtml/Widget/InstanceController.php +++ b/app/code/core/Mage/Widget/controllers/Adminhtml/Widget/InstanceController.php @@ -62,7 +62,7 @@ protected function _initAction() /** * Init widget instance object and set it to registry * - * @return age_Widget_Model_Widget_Instance|boolean + * @return Mage_Widget_Model_Widget_Instance|boolean */ protected function _initWidgetInstance() { diff --git a/app/code/core/Mage/Wishlist/Block/Share/Email/Items.php b/app/code/core/Mage/Wishlist/Block/Share/Email/Items.php index 020ff01e6a..1b60b3cf0e 100644 --- a/app/code/core/Mage/Wishlist/Block/Share/Email/Items.php +++ b/app/code/core/Mage/Wishlist/Block/Share/Email/Items.php @@ -70,4 +70,19 @@ public function getAddToCartUrl($product, $additional = array()) $additional['_store_to_url'] = true; return parent::getAddToCartUrl($product, $additional); } + + /** + * Check whether whishlist item has description + * + * @param Mage_Wishlist_Model_Item $item + * @return bool + */ + public function hasDescription($item) + { + $hasDescription = parent::hasDescription($item); + if ($hasDescription) { + return ($item->getDescription() !== Mage::helper('wishlist')->defaultCommentString()); + } + return $hasDescription; + } } diff --git a/app/code/core/Mage/Wishlist/controllers/IndexController.php b/app/code/core/Mage/Wishlist/controllers/IndexController.php index db76caa080..37c97c554e 100644 --- a/app/code/core/Mage/Wishlist/controllers/IndexController.php +++ b/app/code/core/Mage/Wishlist/controllers/IndexController.php @@ -193,7 +193,7 @@ public function addAction() Mage::helper('wishlist')->calculate(); - $message = $this->__('%1$s has been added to your wishlist. Click here to continue shopping', $product->getName(), $referer); + $message = $this->__('%1$s has been added to your wishlist. Click here to continue shopping', $product->getName(), Mage::helper('core')->escapeUrl($referer)); $session->addSuccess($message); } catch (Mage_Core_Exception $e) { @@ -552,6 +552,7 @@ public function sendAction() /* @var $emailModel Mage_Core_Model_Email_Template */ $emailModel = Mage::getModel('core/email_template'); + $sharingCode = $wishlist->getSharingCode(); foreach($emails as $email) { $emailModel->sendTransactional( Mage::getStoreConfig('wishlist/email/email_template'), @@ -562,8 +563,8 @@ public function sendAction() 'customer' => $customer, 'salable' => $wishlist->isSalable() ? 'yes' : '', 'items' => $wishlistBlock, - 'addAllLink' => Mage::getUrl('*/shared/allcart', array('code' => $wishlist->getSharingCode())), - 'viewOnSiteLink'=> Mage::getUrl('*/shared/index', array('code' => $wishlist->getSharingCode())), + 'addAllLink' => Mage::getUrl('*/shared/allcart', array('code' => $sharingCode)), + 'viewOnSiteLink'=> Mage::getUrl('*/shared/index', array('code' => $sharingCode)), 'message' => $message ) ); diff --git a/app/code/core/Mage/XmlConnect/Block/Cart.php b/app/code/core/Mage/XmlConnect/Block/Cart.php index a86f34f78b..b126dc84b4 100644 --- a/app/code/core/Mage/XmlConnect/Block/Cart.php +++ b/app/code/core/Mage/XmlConnect/Block/Cart.php @@ -61,7 +61,7 @@ protected function _toHtml() $itemXml->addChild('entity_id', $item->getProduct()->getId()); $itemXml->addChild('entity_type', $type); $itemXml->addChild('item_id', $item->getId()); - $itemXml->addChild('name', $xmlObject->xmlentities($renderer->getProductName())); + $itemXml->addChild('name', $xmlObject->escapeXml($renderer->getProductName())); $itemXml->addChild('code', 'cart[' . $item->getId() . '][qty]'); $itemXml->addChild('qty', $renderer->getQty()); $icon = $renderer->getProductThumbnail()->resize( @@ -190,27 +190,27 @@ protected function _toHtml() /** * Options list */ - if ($_options = $renderer->getOptionList()) { + $_options = $renderer->getOptionList(); + if ($_options) { $itemOptionsXml = $itemXml->addChild('options'); foreach ($_options as $_option) { $_formattedOptionValue = $renderer->getFormatedOptionValue($_option); $optionXml = $itemOptionsXml->addChild('option'); $optionXml->addAttribute('label', $xmlObject->xmlAttribute($_option['label'])); - $optionXml->addAttribute( - 'text', $xmlObject->xmlAttribute(strip_tags($_formattedOptionValue['value'])) - ); + $optionXml->addAttribute('text', $xmlObject->xmlAttribute($_formattedOptionValue['value'])); } } /** * Item messages */ - if ($messages = $renderer->getMessages()) { + $messages = $renderer->getMessages(); + if ($messages) { $itemMessagesXml = $itemXml->addChild('messages'); foreach ($messages as $message) { $messageXml = $itemMessagesXml->addChild('option'); $messageXml->addChild('type', $message['type']); - $messageXml->addChild('text', $xmlObject->xmlentities($message['text'])); + $messageXml->addChild('text', $xmlObject->escapeXml($message['text'])); } } } diff --git a/app/code/core/Mage/XmlConnect/Block/Cart/Crosssell.php b/app/code/core/Mage/XmlConnect/Block/Cart/Crosssell.php index ed5f40b6aa..ed7bc5e15c 100644 --- a/app/code/core/Mage/XmlConnect/Block/Cart/Crosssell.php +++ b/app/code/core/Mage/XmlConnect/Block/Cart/Crosssell.php @@ -55,7 +55,7 @@ protected function _toHtml() /** @var $product Mage_Catalog_Model_Product */ foreach ($this->getItems() as $product) { $itemXmlObj = $crossSellXmlObj->addChild('item'); - $itemXmlObj->addChild('name', $crossSellXmlObj->xmlentities($product->getName())); + $itemXmlObj->addChild('name', $crossSellXmlObj->escapeXml($product->getName())); $icon = $this->helper('catalog/image')->init($product, 'thumbnail') ->resize(Mage::helper('xmlconnect/image')->getImageSizeForContent('product_small')); @@ -70,12 +70,13 @@ protected function _toHtml() /** * If product type is grouped than it has options as its grouped items */ - if ($product->getTypeId() == Mage_Catalog_Model_Product_Type_Grouped::TYPE_CODE) { + if ($product->getTypeId() == Mage_Catalog_Model_Product_Type_Grouped::TYPE_CODE + || $product->getTypeId() == Mage_Catalog_Model_Product_Type_Configurable::TYPE_CODE) { $product->setHasOptions(true); } $itemXmlObj->addChild('has_options', (int)$product->getHasOptions()); - $itemXmlObj->addChild('in_stock', (int)$product->getIsInStock()); + $itemXmlObj->addChild('in_stock', (int)$product->getStockItem()->getIsInStock()); if ($product->getTypeId() == Mage_Downloadable_Model_Product_Type::TYPE_DOWNLOADABLE) { $itemXmlObj->addChild('is_salable', 0); } else { diff --git a/app/code/core/Mage/XmlConnect/Block/Cart/Item/Renderer.php b/app/code/core/Mage/XmlConnect/Block/Cart/Item/Renderer.php index 3a6f0f2c15..3ab6f66d0a 100644 --- a/app/code/core/Mage/XmlConnect/Block/Cart/Item/Renderer.php +++ b/app/code/core/Mage/XmlConnect/Block/Cart/Item/Renderer.php @@ -92,7 +92,7 @@ protected function _addSubtotalToXmlObj(Mage_XmlConnect_Model_Simplexml_Element $exclPrice = $_item->getRowTotal(); } $exclPrice = $this->_formatPrice($exclPrice); - $subtotalXmlObj->addAttribute('excluding_tax', $subtotalXmlObj->xmlentities($exclPrice)); + $subtotalXmlObj->addAttribute('excluding_tax', $subtotalXmlObj->escapeXml($exclPrice)); } if ($this->helper('tax')->displayCartPriceInclTax() || $this->helper('tax')->displayCartBothPrices()) { @@ -107,7 +107,7 @@ protected function _addSubtotalToXmlObj(Mage_XmlConnect_Model_Simplexml_Element } $inclPrice = $this->_formatPrice($inclPrice); - $subtotalXmlObj->addAttribute('including_tax', $subtotalXmlObj->xmlentities($inclPrice)); + $subtotalXmlObj->addAttribute('including_tax', $subtotalXmlObj->escapeXml($inclPrice)); } if (Mage::helper('weee')->getApplied($_item)) { @@ -152,7 +152,7 @@ protected function _addPriceToXmlObj(Mage_XmlConnect_Model_Simplexml_Element $pr } $exclPrice = $this->_formatPrice($exclPrice); - $priceXmlObj->addAttribute('excluding_tax', $priceXmlObj->xmlentities($exclPrice)); + $priceXmlObj->addAttribute('excluding_tax', $priceXmlObj->escapeXml($exclPrice)); } if ($this->helper('tax')->displayCartPriceInclTax() @@ -169,7 +169,7 @@ protected function _addPriceToXmlObj(Mage_XmlConnect_Model_Simplexml_Element $pr } $inclPrice = $this->_formatPrice($inclPrice); - $priceXmlObj->addAttribute('including_tax', $priceXmlObj->xmlentities($inclPrice)); + $priceXmlObj->addAttribute('including_tax', $priceXmlObj->escapeXml($inclPrice)); } if (Mage::helper('weee')->getApplied($_item)) { @@ -240,10 +240,7 @@ protected function _addWeeeToXmlObj(Mage_XmlConnect_Model_Simplexml_Element $pri } $totalExcl = $this->_formatPrice($totalExcl); - $priceXmlObj->addAttribute( - 'total_excluding_tax', - $priceXmlObj->xmlentities($totalExcl) - ); + $priceXmlObj->addAttribute('total_excluding_tax', $priceXmlObj->escapeXml($totalExcl)); } if ($typeOfDisplay2 && $_item->getWeeeTaxAppliedAmount()) { @@ -254,7 +251,7 @@ protected function _addWeeeToXmlObj(Mage_XmlConnect_Model_Simplexml_Element $pri } $totalIncl = $this->_formatPrice($totalIncl); - $priceXmlObj->addAttribute('total_including_tax', $priceXmlObj->xmlentities($totalIncl)); + $priceXmlObj->addAttribute('total_including_tax', $priceXmlObj->escapeXml($totalIncl)); } return $priceXmlObj; diff --git a/app/code/core/Mage/XmlConnect/Block/Cart/Totals.php b/app/code/core/Mage/XmlConnect/Block/Cart/Totals.php index 19d4819eeb..67c786c5b7 100644 --- a/app/code/core/Mage/XmlConnect/Block/Cart/Totals.php +++ b/app/code/core/Mage/XmlConnect/Block/Cart/Totals.php @@ -112,7 +112,7 @@ protected function _toHtml() $title = $this->__('Gift Card (%s)', $cardCode['c']); $value = $cardCode['c']; $totalXmlObj = $totalsXmlObj->addChild($code); - $totalXmlObj->addChild('title', $totalsXmlObj->xmlentities($title)); + $totalXmlObj->addChild('title', $totalsXmlObj->escapeXml($title)); $totalXmlObj->addChild('value', $value); $value = Mage::helper('xmlconnect')->formatPriceForXml($cardCode['a']); $formattedValue = $this->getQuote()->getStore()->formatPrice($value, false); @@ -149,7 +149,7 @@ protected function _addTotalDataToXmlObj($totalsXmlObj, $code, $title, $value) { $value = Mage::helper('xmlconnect')->formatPriceForXml($value); $totalXmlObj = $totalsXmlObj->addChild($code); - $totalXmlObj->addChild('title', $totalsXmlObj->xmlentities($title)); + $totalXmlObj->addChild('title', $totalsXmlObj->escapeXml($title)); $formattedValue = $this->getQuote()->getStore()->formatPrice($value, false); $totalXmlObj->addChild('value', $value); $totalXmlObj->addChild('formated_value', $formattedValue); diff --git a/app/code/core/Mage/XmlConnect/Block/Catalog.php b/app/code/core/Mage/XmlConnect/Block/Catalog.php index 54e8b01832..91399be2a9 100644 --- a/app/code/core/Mage/XmlConnect/Block/Catalog.php +++ b/app/code/core/Mage/XmlConnect/Block/Catalog.php @@ -67,7 +67,7 @@ public function getProductSortFeildsXmlObject() $item->addAttribute('isDefault', 1); } $item->addChild('code', $code); - $item->addChild('name', $ordersXmlObject->xmlentities(strip_tags($name))); + $item->addChild('name', $ordersXmlObject->escapeXml($name)); } return $ordersXmlObject; diff --git a/app/code/core/Mage/XmlConnect/Block/Catalog/Category.php b/app/code/core/Mage/XmlConnect/Block/Catalog/Category.php index eb5aa2c9cf..28bd9e207a 100644 --- a/app/code/core/Mage/XmlConnect/Block/Catalog/Category.php +++ b/app/code/core/Mage/XmlConnect/Block/Catalog/Category.php @@ -78,7 +78,7 @@ protected function _toHtml() $item = Mage::getModel('catalog/category')->load($item->getId()); $itemXmlObj = $itemsXmlObj->addChild('item'); - $itemXmlObj->addChild('label', $categoryXmlObj->xmlentities($item->getName())); + $itemXmlObj->addChild('label', $categoryXmlObj->escapeXml($item->getName())); $itemXmlObj->addChild('entity_id', $item->getId()); $itemXmlObj->addChild('content_type', $item->hasChildren() ? 'categories' : 'products'); if (!is_null($categoryId)) { diff --git a/app/code/core/Mage/XmlConnect/Block/Catalog/Category/Info.php b/app/code/core/Mage/XmlConnect/Block/Catalog/Category/Info.php index ba74afce1e..9d24ae57be 100644 --- a/app/code/core/Mage/XmlConnect/Block/Catalog/Category/Info.php +++ b/app/code/core/Mage/XmlConnect/Block/Catalog/Category/Info.php @@ -50,7 +50,7 @@ public function getCategoryInfoXmlObject() */ $title = $this->__('Shop'); if ($category->getParentCategory()->getLevel() > 1) { - $title = $infoXmlObj->xmlentities($category->getParentCategory()->getName()); + $title = $infoXmlObj->escapeXml($category->getParentCategory()->getName()); } $infoXmlObj->addChild('parent_title', $title); diff --git a/app/code/core/Mage/XmlConnect/Block/Catalog/Filters.php b/app/code/core/Mage/XmlConnect/Block/Catalog/Filters.php index cb72d40270..dadbce1c4b 100644 --- a/app/code/core/Mage/XmlConnect/Block/Catalog/Filters.php +++ b/app/code/core/Mage/XmlConnect/Block/Catalog/Filters.php @@ -50,14 +50,14 @@ protected function _toHtml() continue; } $itemXmlObj = $filtersXmlObj->addChild('item'); - $itemXmlObj->addChild('name', $categoryXmlObj->xmlentities($item->getName())); - $itemXmlObj->addChild('code', $categoryXmlObj->xmlentities($item->getCode())); + $itemXmlObj->addChild('name', $categoryXmlObj->escapeXml($item->getName())); + $itemXmlObj->addChild('code', $categoryXmlObj->escapeXml($item->getCode())); $valuesXmlObj = $itemXmlObj->addChild('values'); foreach ($item->getValues() as $value) { $valueXmlObj = $valuesXmlObj->addChild('value'); - $valueXmlObj->addChild('id', $categoryXmlObj->xmlentities($value->getValueString())); - $valueXmlObj->addChild('label', $categoryXmlObj->xmlentities(strip_tags($value->getLabel()))); + $valueXmlObj->addChild('id', $categoryXmlObj->escapeXml($value->getValueString())); + $valueXmlObj->addChild('label', $categoryXmlObj->escapeXml($value->getLabel())); $valueXmlObj->addChild('count', (int)$value->getProductsCount()); } } diff --git a/app/code/core/Mage/XmlConnect/Block/Catalog/Product.php b/app/code/core/Mage/XmlConnect/Block/Catalog/Product.php index 701afbdd90..095e01bf86 100644 --- a/app/code/core/Mage/XmlConnect/Block/Catalog/Product.php +++ b/app/code/core/Mage/XmlConnect/Block/Catalog/Product.php @@ -46,9 +46,9 @@ public function productToXmlObject(Mage_Catalog_Model_Product $product, $itemNod $item = Mage::getModel('xmlconnect/simplexml_element', '<' . $itemNodeName . '>'); if ($product && $product->getId()) { $item->addChild('entity_id', $product->getId()); - $item->addChild('name', $item->xmlentities($product->getName())); + $item->addChild('name', $item->escapeXml($product->getName())); $item->addChild('entity_type', $product->getTypeId()); - $item->addChild('short_description', $item->xmlentities($product->getShortDescription())); + $item->addChild('short_description', $item->escapeXml($product->getShortDescription())); $description = Mage::helper('xmlconnect')->htmlize($item->xmlentities($product->getDescription())); $item->addChild('description', $description); $item->addChild('link', $product->getProductUrl()); @@ -81,7 +81,8 @@ public function productToXmlObject(Mage_Catalog_Model_Product $product, $itemNod /** * If product type is grouped than it has options as its grouped items */ - if ($product->getTypeId() == Mage_Catalog_Model_Product_Type_Grouped::TYPE_CODE) { + if ($product->getTypeId() == Mage_Catalog_Model_Product_Type_Grouped::TYPE_CODE + || $product->getTypeId() == Mage_Catalog_Model_Product_Type_Configurable::TYPE_CODE) { $product->setHasOptions(true); } $item->addChild('has_options', (int)$product->getHasOptions()); diff --git a/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Options.php b/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Options.php index 1d159608b7..7d276e29cf 100644 --- a/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Options.php +++ b/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Options.php @@ -87,7 +87,7 @@ public function getProductCustomOptionsXmlObject(Mage_Catalog_Model_Product $pro } $optionNode->addAttribute('code', $code); $optionNode->addAttribute('type', $type); - $optionNode->addAttribute('label', $xmlModel->xmlentities($option->getTitle())); + $optionNode->addAttribute('label', $xmlModel->escapeXml($option->getTitle())); if ($option->getIsRequire()) { $optionNode->addAttribute('is_required', 1); } @@ -105,7 +105,7 @@ public function getProductCustomOptionsXmlObject(Mage_Catalog_Model_Product $pro foreach ($option->getValues() as $value) { $valueNode = $optionNode->addChild('value'); $valueNode->addAttribute('code', $value->getId()); - $valueNode->addAttribute('label', $xmlModel->xmlentities($value->getTitle())); + $valueNode->addAttribute('label', $xmlModel->escapeXml($value->getTitle())); if ($value->getPrice() != 0) { $price = Mage::helper('xmlconnect')->formatPriceForXml($value->getPrice()); diff --git a/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Options/Bundle.php b/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Options/Bundle.php index 94ed27c17c..690021844b 100644 --- a/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Options/Bundle.php +++ b/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Options/Bundle.php @@ -80,7 +80,7 @@ public function getProductOptionsXml(Mage_Catalog_Model_Product $product, $isObj } $optionNode->addAttribute('code', $code); $optionNode->addAttribute('type', $type); - $optionNode->addAttribute('label', $optionsXmlObj->xmlentities($_option->getTitle())); + $optionNode->addAttribute('label', $optionsXmlObj->escapeXml($_option->getTitle())); if ($_option->getRequired()) { $optionNode->addAttribute('is_required', 1); } @@ -93,7 +93,7 @@ public function getProductOptionsXml(Mage_Catalog_Model_Product $product, $isObj $valueNode = $optionNode->addChild('value'); $valueNode->addAttribute('code', $_selection->getSelectionId()); - $valueNode->addAttribute('label', $optionsXmlObj->xmlentities($_selection->getName())); + $valueNode->addAttribute('label', $optionsXmlObj->escapeXml($_selection->getName())); if (!$_option->isMultiSelection()) { if ($_selection->getSelectionCanChangeQty()) { $valueNode->addAttribute('is_qty_editable', 1); diff --git a/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Options/Configurable.php b/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Options/Configurable.php index cbaaa866af..6edc281770 100644 --- a/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Options/Configurable.php +++ b/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Options/Configurable.php @@ -131,13 +131,13 @@ public function getProductOptionsXml(Mage_Catalog_Model_Product $product, $isObj $optionNode = $optionsXmlObj->addChild('option'); $optionNode->addAttribute('code', 'super_attribute[' . $id . ']'); $optionNode->addAttribute('type', 'select'); - $optionNode->addAttribute('label', $optionsXmlObj->xmlentities($attribute['label'])); + $optionNode->addAttribute('label', $optionsXmlObj->escapeXml($attribute['label'])); $optionNode->addAttribute('is_required', 1); if ($isFirst) { foreach ($attribute['options'] as $option) { $valueNode = $optionNode->addChild('value'); $valueNode->addAttribute('code', $option['id']); - $valueNode->addAttribute('label', $optionsXmlObj->xmlentities($option['label'])); + $valueNode->addAttribute('label', $optionsXmlObj->escapeXml($option['label'])); if ((float)$option['price'] != 0.00) { $valueNode->addAttribute('price', $option['price']); $valueNode->addAttribute('formated_price', $option['formated_price']); @@ -187,7 +187,7 @@ protected function _prepareRecursivelyRelatedValues(&$valueNode, $attributes, $p $_valueNode = $relatedNode->addChild('value'); $_valueNode->addAttribute('code', $option['id']); - $_valueNode->addAttribute('label', $_valueNode->xmlentities($option['label'])); + $_valueNode->addAttribute('label', $_valueNode->escapeXml($option['label'])); if ((float)$option['price'] != 0.00) { $_valueNode->addAttribute('price', $option['price']); $_valueNode->addAttribute('formated_price', $option['formated_price']); diff --git a/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Options/Grouped.php b/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Options/Grouped.php index 8c90bb5c32..cdedcea493 100644 --- a/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Options/Grouped.php +++ b/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Options/Grouped.php @@ -68,7 +68,7 @@ public function getProductOptionsXml(Mage_Catalog_Model_Product $product, $isObj $optionNode->addAttribute('code', 'super_group[' . $_item->getId() . ']'); $optionNode->addAttribute('type', 'product'); - $optionNode->addAttribute('label', $xmlModel->xmlentities($_item->getName())); + $optionNode->addAttribute('label', $xmlModel->escapeXml($_item->getName())); $optionNode->addAttribute('is_qty_editable', 1); $optionNode->addAttribute('qty', $_item->getQty()*1); diff --git a/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Price/Bundle.php b/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Price/Bundle.php index d446da2754..7670fd4ea2 100644 --- a/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Price/Bundle.php +++ b/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Price/Bundle.php @@ -104,7 +104,7 @@ public function collectProductPrices( } $weeeItemXmlObj = $weeeXmlObj->addChild('item'); $weeeItemXmlObj->addAttribute( - 'name', $weeeItemXmlObj->xmlentities($_weeeTaxAttribute->getName()) + 'name', $weeeItemXmlObj->escapeXml($_weeeTaxAttribute->getName()) ); $weeeItemXmlObj->addAttribute( 'amount', $_coreHelper->currency($amount, true, false) @@ -132,7 +132,7 @@ public function collectProductPrices( } $weeeItemXmlObj = $weeeXmlObj->addChild('item'); $weeeItemXmlObj->addAttribute( - 'name', $weeeItemXmlObj->xmlentities($_weeeTaxAttribute->getName()) + 'name', $weeeItemXmlObj->escapeXml($_weeeTaxAttribute->getName()) ); $weeeItemXmlObj->addAttribute( 'amount', $_coreHelper->currency($amount, true, false) @@ -168,7 +168,7 @@ public function collectProductPrices( } $weeeItemXmlObj = $weeeXmlObj->addChild('item'); $weeeItemXmlObj->addAttribute( - 'name', $weeeItemXmlObj->xmlentities($_weeeTaxAttribute->getName()) + 'name', $weeeItemXmlObj->escapeXml($_weeeTaxAttribute->getName()) ); $weeeItemXmlObj->addAttribute( 'amount', $_coreHelper->currency($amount, true, false) @@ -194,7 +194,7 @@ public function collectProductPrices( } $weeeItemXmlObj = $weeeXmlObj->addChild('item'); $weeeItemXmlObj->addAttribute( - 'name', $weeeItemXmlObj->xmlentities($_weeeTaxAttribute->getName()) + 'name', $weeeItemXmlObj->escapeXml($_weeeTaxAttribute->getName()) ); $weeeItemXmlObj->addAttribute('amount', $_coreHelper->currency($amount, true, false)); } @@ -237,7 +237,7 @@ public function collectProductPrices( } $weeeItemXmlObj = $weeeXmlObj->addChild('item'); $weeeItemXmlObj->addAttribute( - 'name', $weeeItemXmlObj->xmlentities($_weeeTaxAttribute->getName()) + 'name', $weeeItemXmlObj->escapeXml($_weeeTaxAttribute->getName()) ); $weeeItemXmlObj->addAttribute( 'amount', $_coreHelper->currency($amount, true, false) @@ -265,7 +265,7 @@ public function collectProductPrices( } $weeeItemXmlObj = $weeeXmlObj->addChild('item'); $weeeItemXmlObj->addAttribute( - 'name', $weeeItemXmlObj->xmlentities($_weeeTaxAttribute->getName()) + 'name', $weeeItemXmlObj->escapeXml($_weeeTaxAttribute->getName()) ); $weeeItemXmlObj->addAttribute( 'amount', $_coreHelper->currency($amount, true, false) @@ -300,7 +300,7 @@ public function collectProductPrices( } $weeeItemXmlObj = $weeeXmlObj->addChild('item'); $weeeItemXmlObj->addAttribute( - 'name', $weeeItemXmlObj->xmlentities($_weeeTaxAttribute->getName()) + 'name', $weeeItemXmlObj->escapeXml($_weeeTaxAttribute->getName()) ); $weeeItemXmlObj->addAttribute( 'amount', $_coreHelper->currency($amount, true, false) @@ -328,7 +328,7 @@ public function collectProductPrices( } $weeeItemXmlObj = $weeeXmlObj->addChild('item'); $weeeItemXmlObj->addAttribute( - 'name', $weeeItemXmlObj->xmlentities($_weeeTaxAttribute->getName()) + 'name', $weeeItemXmlObj->escapeXml($_weeeTaxAttribute->getName()) ); $weeeItemXmlObj->addAttribute( 'amount', $_coreHelper->currency($amount, true, false) diff --git a/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Price/Default.php b/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Price/Default.php index 33de63ad5d..c136f84d49 100644 --- a/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Price/Default.php +++ b/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Price/Default.php @@ -102,7 +102,7 @@ public function collectProductPrices( foreach ($_weeeTaxAttributes as $_weeeTaxAttribute) { $weeeItemXmlObj = $weeeXmlObj->addChild('item'); $weeeItemXmlObj->addAttribute( - 'name', $weeeItemXmlObj->xmlentities($_weeeTaxAttribute->getName()) + 'name', $weeeItemXmlObj->escapeXml($_weeeTaxAttribute->getName()) ); $weeeItemXmlObj->addAttribute( 'amount', $_coreHelper->currency($_weeeTaxAttribute->getAmount(), true, false) @@ -124,7 +124,7 @@ public function collectProductPrices( foreach ($_weeeTaxAttributes as $_weeeTaxAttribute) { $weeeItemXmlObj = $weeeXmlObj->addChild('item'); $weeeItemXmlObj->addAttribute( - 'name', $weeeItemXmlObj->xmlentities($_weeeTaxAttribute->getName()) + 'name', $weeeItemXmlObj->escapeXml($_weeeTaxAttribute->getName()) ); $weeeItemXmlObj->addAttribute('amount', $_coreHelper->currency( $_weeeTaxAttribute->getAmount() + $_weeeTaxAttribute->getTaxAmount(), true, false @@ -139,7 +139,7 @@ public function collectProductPrices( foreach ($_weeeTaxAttributes as $_weeeTaxAttribute) { $weeeItemXmlObj = $weeeXmlObj->addChild('item'); $weeeItemXmlObj->addAttribute( - 'name', $weeeItemXmlObj->xmlentities($_weeeTaxAttribute->getName()) + 'name', $weeeItemXmlObj->escapeXml($_weeeTaxAttribute->getName()) ); $weeeItemXmlObj->addAttribute( 'amount', $_coreHelper->currency($_weeeTaxAttribute->getAmount(), true, false) @@ -178,7 +178,7 @@ public function collectProductPrices( foreach ($_weeeTaxAttributes as $_weeeTaxAttribute) { $weeeItemXmlObj = $weeeXmlObj->addChild('item'); $weeeItemXmlObj->addAttribute( - 'name', $weeeItemXmlObj->xmlentities($_weeeTaxAttribute->getName()) + 'name', $weeeItemXmlObj->escapeXml($_weeeTaxAttribute->getName()) ); $weeeItemXmlObj->addAttribute( 'amount', $_coreHelper->currency($_weeeTaxAttribute->getAmount(), true, false) @@ -197,7 +197,7 @@ public function collectProductPrices( foreach ($_weeeTaxAttributes as $_weeeTaxAttribute) { $weeeItemXmlObj = $weeeXmlObj->addChild('item'); $weeeItemXmlObj->addAttribute( - 'name', $weeeItemXmlObj->xmlentities($_weeeTaxAttribute->getName()) + 'name', $weeeItemXmlObj->escapeXml($_weeeTaxAttribute->getName()) ); $weeeItemXmlObj->addAttribute('amount', $_coreHelper->currency( $_weeeTaxAttribute->getAmount() + $_weeeTaxAttribute->getTaxAmount(), true, false @@ -214,7 +214,7 @@ public function collectProductPrices( foreach ($_weeeTaxAttributes as $_weeeTaxAttribute) { $weeeItemXmlObj = $weeeXmlObj->addChild('item'); $weeeItemXmlObj->addAttribute( - 'name', $weeeItemXmlObj->xmlentities($_weeeTaxAttribute->getName()) + 'name', $weeeItemXmlObj->escapeXml($_weeeTaxAttribute->getName()) ); $weeeItemXmlObj->addAttribute( 'amount', $_coreHelper->currency($_weeeTaxAttribute->getAmount(), true, false) @@ -267,7 +267,7 @@ public function collectProductPrices( foreach ($_weeeTaxAttributes as $_weeeTaxAttribute) { $weeeItemXmlObj = $weeeXmlObj->addChild('item'); $weeeItemXmlObj->addAttribute( - 'name', $weeeItemXmlObj->xmlentities($_weeeTaxAttribute->getName()) + 'name', $weeeItemXmlObj->escapeXml($_weeeTaxAttribute->getName()) ); $weeeItemXmlObj->addAttribute('amount', $_coreHelper->currency( $_weeeTaxAttribute->getAmount(), true, false @@ -292,7 +292,7 @@ public function collectProductPrices( foreach ($_weeeTaxAttributes as $_weeeTaxAttribute) { $weeeItemXmlObj = $weeeXmlObj->addChild('item'); $weeeItemXmlObj->addAttribute( - 'name', $weeeItemXmlObj->xmlentities($_weeeTaxAttribute->getName()) + 'name', $weeeItemXmlObj->escapeXml($_weeeTaxAttribute->getName()) ); $weeeItemXmlObj->addAttribute('amount', $_coreHelper->currency( $_weeeTaxAttribute->getAmount() + $_weeeTaxAttribute->getTaxAmount(), true, false @@ -313,7 +313,7 @@ public function collectProductPrices( foreach ($_weeeTaxAttributes as $_weeeTaxAttribute) { $weeeItemXmlObj = $weeeXmlObj->addChild('item'); $weeeItemXmlObj->addAttribute( - 'name', $weeeItemXmlObj->xmlentities($_weeeTaxAttribute->getName()) + 'name', $weeeItemXmlObj->escapeXml($_weeeTaxAttribute->getName()) ); $weeeItemXmlObj->addAttribute('amount', $_coreHelper->currency( $_weeeTaxAttribute->getAmount(), true, false diff --git a/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Review.php b/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Review.php index 8b196ccbfb..3194eb4ac4 100644 --- a/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Review.php +++ b/app/code/core/Mage/XmlConnect/Block/Catalog/Product/Review.php @@ -52,9 +52,9 @@ public function reviewToXmlObject(Mage_Review_Model_Review $review, $itemNodeNam if ($review->getId()) { $item->addChild('review_id', $review->getId()); $item->addChild('created_at', $this->formatDate($review->getCreatedAt())); - $item->addChild('title', $item->xmlentities($review->getTitle())); - $item->addChild('nickname', $item->xmlentities($review->getNickname())); - $detail = $item->xmlentities($review->getDetail()); + $item->addChild('title', $item->escapeXml($review->getTitle())); + $item->addChild('nickname', $item->escapeXml($review->getNickname())); + $detail = $item->escapeXml($review->getDetail()); if ($itemNodeName == 'item') { $remainder = ''; $deviceType = Mage::helper('xmlconnect')->getDeviceType(); diff --git a/app/code/core/Mage/XmlConnect/Block/Catalog/Search.php b/app/code/core/Mage/XmlConnect/Block/Catalog/Search.php index a782092bbd..a2c884779e 100644 --- a/app/code/core/Mage/XmlConnect/Block/Catalog/Search.php +++ b/app/code/core/Mage/XmlConnect/Block/Catalog/Search.php @@ -96,7 +96,7 @@ protected function _toHtml() continue; } $item = $filtersXmlObject->addChild('item'); - $item->addChild('name', $searchXmlObject->xmlentities($filter->getName())); + $item->addChild('name', $searchXmlObject->escapeXml($filter->getName())); $item->addChild('code', $filter->getRequestVar()); $values = $item->addChild('values'); @@ -107,9 +107,7 @@ protected function _toHtml() } $value = $values->addChild('value'); $value->addChild('id', $valueItem->getValueString()); - $value->addChild( - 'label', $searchXmlObject->xmlentities(strip_tags($valueItem->getLabel())) - ); + $value->addChild('label', $searchXmlObject->escapeXml($valueItem->getLabel())); $value->addChild('count', $count); } } diff --git a/app/code/core/Mage/XmlConnect/Block/Catalog/Search/Suggest.php b/app/code/core/Mage/XmlConnect/Block/Catalog/Search/Suggest.php index d35c133214..a3e4ea8cc5 100644 --- a/app/code/core/Mage/XmlConnect/Block/Catalog/Search/Suggest.php +++ b/app/code/core/Mage/XmlConnect/Block/Catalog/Search/Suggest.php @@ -58,7 +58,7 @@ protected function _toHtml() $items = ''; foreach ($suggestData as $item) { - $items .= $suggestXmlObj->xmlentities(strip_tags($item['title'])) . self::SUGGEST_ITEM_SEPARATOR + $items .= $suggestXmlObj->escapeXml($item['title']) . self::SUGGEST_ITEM_SEPARATOR . (int)$item['num_of_results'] . self::SUGGEST_ITEM_SEPARATOR; } diff --git a/app/code/core/Mage/XmlConnect/Block/Checkout/Address/Billing.php b/app/code/core/Mage/XmlConnect/Block/Checkout/Address/Billing.php index 8d5b5c7b7e..891d2b72c2 100644 --- a/app/code/core/Mage/XmlConnect/Block/Checkout/Address/Billing.php +++ b/app/code/core/Mage/XmlConnect/Block/Checkout/Address/Billing.php @@ -55,7 +55,7 @@ protected function _toHtml() } $this->getChild('address_list')->prepareAddressData($address, $item); $item->addChild( - 'address_line', $billingXmlObj->xmlentities($address->format('oneline')) + 'address_line', $billingXmlObj->escapeXml($address->format('oneline')) ); } diff --git a/app/code/core/Mage/XmlConnect/Block/Checkout/Address/Shipping.php b/app/code/core/Mage/XmlConnect/Block/Checkout/Address/Shipping.php index 014a90d918..f29eef62bf 100644 --- a/app/code/core/Mage/XmlConnect/Block/Checkout/Address/Shipping.php +++ b/app/code/core/Mage/XmlConnect/Block/Checkout/Address/Shipping.php @@ -54,7 +54,7 @@ protected function _toHtml() $item->addAttribute('selected', 1); } $this->getChild('address_list')->prepareAddressData($address, $item); - $item->addChild('address_line', $shippingXmlObj->xmlentities($address->format('oneline'))); + $item->addChild('address_line', $shippingXmlObj->escapeXml($address->format('oneline'))); } return $shippingXmlObj->asNiceXml(); diff --git a/app/code/core/Mage/XmlConnect/Block/Checkout/Agreements.php b/app/code/core/Mage/XmlConnect/Block/Checkout/Agreements.php index c5f667e6d9..bb6f3285f2 100644 --- a/app/code/core/Mage/XmlConnect/Block/Checkout/Agreements.php +++ b/app/code/core/Mage/XmlConnect/Block/Checkout/Agreements.php @@ -40,15 +40,18 @@ class Mage_XmlConnect_Block_Checkout_Agreements extends Mage_Checkout_Block_Agre */ protected function _toHtml() { + /** @var $agreementsXmlObj Mage_XmlConnect_Model_Simplexml_Element */ $agreementsXmlObj = Mage::getModel('xmlconnect/simplexml_element', ''); if ($this->getAgreements()) { foreach ($this->getAgreements() as $agreement) { $itemXmlObj = $agreementsXmlObj->addChild('item'); - $content = $agreementsXmlObj->xmlentities($agreement->getContent()); + $content = $agreement->getContent(); if (!$agreement->getIsHtml()) { - $content = nl2br(strip_tags($content)); + $content = nl2br($agreementsXmlObj->escapeXml($content)); + } else { + $agreementsXmlObj->xmlentities($content); } - $agreementText = $agreementsXmlObj->xmlentities($agreement->getCheckboxText()); + $agreementText = $agreementsXmlObj->escapeXml($agreement->getCheckboxText()); $itemXmlObj->addChild('label', $agreementText); $itemXmlObj->addChild('content', $content); $itemXmlObj->addChild('code', 'agreement[' . $agreement->getId() . ']'); diff --git a/app/code/core/Mage/XmlConnect/Block/Checkout/Order/Review/Info.php b/app/code/core/Mage/XmlConnect/Block/Checkout/Order/Review/Info.php index cab02fc13b..3988c74f86 100644 --- a/app/code/core/Mage/XmlConnect/Block/Checkout/Order/Review/Info.php +++ b/app/code/core/Mage/XmlConnect/Block/Checkout/Order/Review/Info.php @@ -55,7 +55,7 @@ protected function _toHtml() $itemXml->addChild('entity_id', $item->getProduct()->getId()); $itemXml->addChild('entity_type', $type); $itemXml->addChild('item_id', $item->getId()); - $itemXml->addChild('name', $itemsXmlObj->xmlentities($renderer->getProductName())); + $itemXml->addChild('name', $itemsXmlObj->escapeXml($renderer->getProductName())); $itemXml->addChild('qty', $renderer->getQty()); $icon = $renderer->getProductThumbnail()->resize( Mage::helper('xmlconnect/image')->getImageSizeForContent('product_small') @@ -169,16 +169,15 @@ protected function _toHtml() /** * Options list */ - if ($_options = $renderer->getOptionList()) { + $_options = $renderer->getOptionList(); + if ($_options) { $itemOptionsXml = $itemXml->addChild('options'); foreach ($_options as $_option) { $_formattedOptionValue = $renderer->getFormatedOptionValue($_option); $optionXml = $itemOptionsXml->addChild('option'); - $labelValue = $itemsXmlObj->xmlentities($_option['label']); + $labelValue = $itemsXmlObj->escapeXml($_option['label']); $optionXml->addAttribute('label', $labelValue); - $textValue = $itemsXmlObj->xmlentities( - strip_tags($_formattedOptionValue['value']) - ); + $textValue = $itemsXmlObj->escapeXml($_formattedOptionValue['value']); $optionXml->addAttribute('text', $textValue); } } diff --git a/app/code/core/Mage/XmlConnect/Block/Checkout/Payment/Method/List.php b/app/code/core/Mage/XmlConnect/Block/Checkout/Payment/Method/List.php index 36c201aa5d..8096a237eb 100644 --- a/app/code/core/Mage/XmlConnect/Block/Checkout/Payment/Method/List.php +++ b/app/code/core/Mage/XmlConnect/Block/Checkout/Payment/Method/List.php @@ -285,7 +285,7 @@ protected function _toHtml() $methodItemXmlObj = $methodsXmlObj->addChild('method'); $methodItemXmlObj->addAttribute('post_name', 'payment[method]'); $methodItemXmlObj->addAttribute('code', $method->getCode()); - $methodItemXmlObj->addAttribute('label', $methodsXmlObj->xmlentities($method->getTitle())); + $methodItemXmlObj->addAttribute('label', $methodsXmlObj->escapeXml($method->getTitle())); if ($this->getQuote()->getPayment()->getMethod() == $method->getCode()) { $methodItemXmlObj->addAttribute('selected', 1); } diff --git a/app/code/core/Mage/XmlConnect/Block/Checkout/Shipping/Method/Available.php b/app/code/core/Mage/XmlConnect/Block/Checkout/Shipping/Method/Available.php index ca6bcb1ef6..a2aa2cfdb8 100644 --- a/app/code/core/Mage/XmlConnect/Block/Checkout/Shipping/Method/Available.php +++ b/app/code/core/Mage/XmlConnect/Block/Checkout/Shipping/Method/Available.php @@ -49,16 +49,16 @@ protected function _toHtml() $_sole = count($_shippingRateGroups) == 1; foreach ($_shippingRateGroups as $code => $_rates) { $methodXmlObj = $methodsXmlObj->addChild('method'); - $methodXmlObj->addAttribute('label', $methodsXmlObj->xmlentities($this->getCarrierName($code))); + $methodXmlObj->addAttribute('label', $methodsXmlObj->escapeXml($this->getCarrierName($code))); $ratesXmlObj = $methodXmlObj->addChild('rates'); $_sole = $_sole && count($_rates) == 1; foreach ($_rates as $_rate) { $rateXmlObj = $ratesXmlObj->addChild('rate'); - $rateXmlObj->addAttribute('label', $methodsXmlObj->xmlentities($_rate->getMethodTitle())); + $rateXmlObj->addAttribute('label', $methodsXmlObj->escapeXml($_rate->getMethodTitle())); $rateXmlObj->addAttribute('code', $_rate->getCode()); if ($_rate->getErrorMessage()) { - $rateXmlObj->addChild('error_message', $methodsXmlObj->xmlentities($_rate->getErrorMessage())); + $rateXmlObj->addChild('error_message', $methodsXmlObj->escapeXml($_rate->getErrorMessage())); } else { $price = Mage::helper('tax')->getShippingPrice( $_rate->getPrice(), diff --git a/app/code/core/Mage/XmlConnect/Block/Customer/Address/Form.php b/app/code/core/Mage/XmlConnect/Block/Customer/Address/Form.php index ab998621c4..4a00292e7f 100644 --- a/app/code/core/Mage/XmlConnect/Block/Customer/Address/Form.php +++ b/app/code/core/Mage/XmlConnect/Block/Customer/Address/Form.php @@ -250,12 +250,12 @@ protected function _addPrefix(Mage_XmlConnect_Model_Simplexml_Form_Element_Field if ($this->getNameWidgetBlock()->getPrefixOptions() === false) { $contactInfoFieldset->addField($this->getNameWidgetBlock()->getFieldId('prefix'), 'text', array( - 'label' => $this->getNameWidgetBlock()->__('Prefix'), + 'label' => $this->__('Prefix'), 'name' => $this->getNameWidgetBlock()->getFieldName('prefix') ) + $attributes); } else { $contactInfoFieldset->addField($this->getNameWidgetBlock()->getFieldId('prefix'), 'select', array( - 'label' => $this->getNameWidgetBlock()->__('Prefix'), + 'label' => $this->__('Prefix'), 'name' => $this->getNameWidgetBlock()->getFieldName('prefix'), 'options' => $this->getNameWidgetBlock()->getPrefixOptions() ) + $attributes); @@ -284,12 +284,12 @@ protected function _addSuffix( if ($this->getNameWidgetBlock()->getSuffixOptions() === false) { $contactInfoFieldset->addField($this->getNameWidgetBlock()->getFieldId('suffix'), 'text', array( - 'label' => $this->getNameWidgetBlock()->__('Suffix'), + 'label' => $this->__('Suffix'), 'name' => $this->getNameWidgetBlock()->getFieldName('suffix') ) + $attributes); } else { $contactInfoFieldset->addField($this->getNameWidgetBlock()->getFieldId('suffix'), 'select', array( - 'label' => $this->getNameWidgetBlock()->__('Suffix'), + 'label' => $this->__('Suffix'), 'name' => $this->getNameWidgetBlock()->getFieldName('suffix'), 'options' => $this->getNameWidgetBlock()->getSuffixOptions() ) + $attributes); @@ -314,7 +314,7 @@ protected function _addMiddleName( ); $contactInfoFieldset->addField($this->getNameWidgetBlock()->getFieldId('middlename'), 'text', array( - 'label' => $this->getNameWidgetBlock()->__('M.I.'), + 'label' => $this->__('M.I.'), 'name' => $this->getNameWidgetBlock()->getFieldName('middlename') ) + $attributes); diff --git a/app/code/core/Mage/XmlConnect/Block/Customer/Address/List.php b/app/code/core/Mage/XmlConnect/Block/Customer/Address/List.php index b0f2381891..d4f0e0534f 100644 --- a/app/code/core/Mage/XmlConnect/Block/Customer/Address/List.php +++ b/app/code/core/Mage/XmlConnect/Block/Customer/Address/List.php @@ -82,10 +82,8 @@ protected function _toHtml() * @return array */ public function prepareAddressData( - Mage_Customer_Model_Address $address, - Mage_XmlConnect_Model_Simplexml_Element $item - ) - { + Mage_Customer_Model_Address $address, Mage_XmlConnect_Model_Simplexml_Element $item + ) { if (!$address) { return array(); } @@ -123,7 +121,7 @@ public function prepareAddressData( if (empty($value)) { continue; } - $item->addChild($key, $item->xmlentities($value)); + $item->addChild($key, $item->escapeXml($value)); } } } diff --git a/app/code/core/Mage/XmlConnect/Block/Customer/Form.php b/app/code/core/Mage/XmlConnect/Block/Customer/Form.php index 24431c1e84..8e1d18bf96 100644 --- a/app/code/core/Mage/XmlConnect/Block/Customer/Form.php +++ b/app/code/core/Mage/XmlConnect/Block/Customer/Form.php @@ -46,32 +46,32 @@ protected function _toHtml() $xmlModel = Mage::getModel('xmlconnect/simplexml_element', ''); //Enterprise_Customer if ($editFlag == 1 && $customer && $customer->getId()) { - $firstname = $xmlModel->xmlentities($customer->getFirstname()); - $lastname = $xmlModel->xmlentities($customer->getLastname()); - $email = $xmlModel->xmlentities($customer->getEmail()); + $firstname = $xmlModel->escapeXml($customer->getFirstname()); + $lastname = $xmlModel->escapeXml($customer->getLastname()); + $email = $xmlModel->escapeXml($customer->getEmail()); } else { $firstname = $lastname = $email = ''; } if ($editFlag) { $passwordManageXml = ' - +
- - - + + + - password + password
'; } else { $passwordManageXml = ' - - + + - password + password '; @@ -80,11 +80,11 @@ protected function _toHtml() $xml = <<
- - - + + + - + $passwordManageXml diff --git a/app/code/core/Mage/XmlConnect/Block/Customer/Order/List.php b/app/code/core/Mage/XmlConnect/Block/Customer/Order/List.php index 90734f5fe9..8e3a4e789b 100644 --- a/app/code/core/Mage/XmlConnect/Block/Customer/Order/List.php +++ b/app/code/core/Mage/XmlConnect/Block/Customer/Order/List.php @@ -65,7 +65,7 @@ protected function _toHtml() $item->addChild('number', $_order->getRealOrderId()); $item->addChild('date', $this->formatDate($_order->getCreatedAtStoreDate())); if ($_order->getShippingAddress()) { - $item->addChild('ship_to', $ordersXmlObj->xmlentities($_order->getShippingAddress()->getName())); + $item->addChild('ship_to', $ordersXmlObj->escapeXml($_order->getShippingAddress()->getName())); } $item->addChild('total', $_order->getOrderCurrency()->formatPrecision( $_order->getGrandTotal(), 2, array(), false, false diff --git a/app/code/core/Mage/XmlConnect/Block/Customer/Storecredit.php b/app/code/core/Mage/XmlConnect/Block/Customer/Storecredit.php index 2412011ac0..59792ac05d 100644 --- a/app/code/core/Mage/XmlConnect/Block/Customer/Storecredit.php +++ b/app/code/core/Mage/XmlConnect/Block/Customer/Storecredit.php @@ -46,10 +46,8 @@ protected function _toHtml() $accountBalance = $this->getLayout() ->addBlock('enterprise_customerbalance/account_balance', 'account_balance'); - $customerBalanceHelper = Mage::helper('enterprise_customerbalance'); - $xmlModel->addCustomChild('balance', null, array( - 'label' => $customerBalanceHelper->__('Your current balance is:'), + 'label' => $this->__('Your current balance is:'), 'value' => $accountBalance->getBalance(), 'formatted_value' => Mage::helper('core')->currency($accountBalance->getBalance(), true, false) )); @@ -59,11 +57,11 @@ protected function _toHtml() if ($accountHistory->canShow() && $accountHistory->getEvents() && count($accountHistory->getEvents())) { $balanceHistory = $xmlModel->addCustomChild('balance_history', null, array( - 'label' => $customerBalanceHelper->__('Balance History'), - 'action_label' => $customerBalanceHelper->__('Action'), - 'balance_change_label' => $customerBalanceHelper->__('Balance Change'), - 'balance_label' => $customerBalanceHelper->__('Balance'), - 'date_label' => $customerBalanceHelper->__('Date') + 'label' => $this->__('Balance History'), + 'action_label' => $this->__('Action'), + 'balance_change_label' => $this->__('Balance Change'), + 'balance_label' => $this->__('Balance'), + 'date_label' => $this->__('Date') )); foreach ($accountHistory->getEvents() as $event) { diff --git a/app/code/core/Mage/XmlConnect/Block/Home.php b/app/code/core/Mage/XmlConnect/Block/Home.php index af9a7d34ae..d058c3eceb 100644 --- a/app/code/core/Mage/XmlConnect/Block/Home.php +++ b/app/code/core/Mage/XmlConnect/Block/Home.php @@ -69,7 +69,7 @@ protected function _toHtml() /** @var $item Mage_Catalog_Model_Category */ $item = Mage::getModel('catalog/category')->load($item->getId()); $itemXmlObj = $itemsXmlObj->addChild('item'); - $itemXmlObj->addChild('label', $homeXmlObj->xmlentities($item->getName())); + $itemXmlObj->addChild('label', $homeXmlObj->escapeXml($item->getName())); $itemXmlObj->addChild('entity_id', $item->getId()); $itemXmlObj->addChild('content_type', $item->hasChildren() ? 'categories' : 'products'); $icon = Mage::helper('xmlconnect/catalog_category_image')->initialize($item, 'thumbnail') diff --git a/app/code/core/Mage/XmlConnect/Block/Review/Form.php b/app/code/core/Mage/XmlConnect/Block/Review/Form.php index 5e55d76826..28dbf76031 100644 --- a/app/code/core/Mage/XmlConnect/Block/Review/Form.php +++ b/app/code/core/Mage/XmlConnect/Block/Review/Form.php @@ -55,7 +55,7 @@ protected function _toHtml() $nickname = ''; if ($customer->getId()) { - $nickname = $xmlReview->xmlentities($customer->getFirstname()); + $nickname = $xmlReview->escapeXml($customer->getFirstname()); } if ($this->getRatings()) { diff --git a/app/code/core/Mage/XmlConnect/Block/Wishlist.php b/app/code/core/Mage/XmlConnect/Block/Wishlist.php index f2ca53bf46..e2fb0f7ae4 100644 --- a/app/code/core/Mage/XmlConnect/Block/Wishlist.php +++ b/app/code/core/Mage/XmlConnect/Block/Wishlist.php @@ -70,13 +70,14 @@ protected function _toHtml() $itemXmlObj->addChild('item_id', $item->getWishlistItemId()); $itemXmlObj->addChild('entity_id', $item->getProductId()); $itemXmlObj->addChild('entity_type_id', $item->getProduct()->getTypeId()); - $itemXmlObj->addChild('name', $wishlistXmlObj->xmlentities($item->getName())); - $itemXmlObj->addChild('in_stock', (int)$item->getProduct()->getIsInStock()); + $itemXmlObj->addChild('name', $wishlistXmlObj->escapeXml($item->getName())); + $itemXmlObj->addChild('in_stock', (int)$item->getProduct()->getStockItem()->getIsInStock()); $itemXmlObj->addChild('is_salable', (int)$item->getProduct()->isSalable()); /** * If product type is grouped than it has options as its grouped items */ - if ($item->getProduct()->getTypeId() == Mage_Catalog_Model_Product_Type_Grouped::TYPE_CODE) { + if ($item->getProduct()->getTypeId() == Mage_Catalog_Model_Product_Type_Grouped::TYPE_CODE + || $item->getProduct()->getTypeId() == Mage_Catalog_Model_Product_Type_Configurable::TYPE_CODE) { $item->getProduct()->setHasOptions(true); } $itemXmlObj->addChild('has_options', (int)$item->getProduct()->getHasOptions()); @@ -89,10 +90,10 @@ protected function _toHtml() $file = Mage::helper('xmlconnect')->urlToPath($icon); $iconXml->addAttribute('modification_time', filemtime($file)); - $description = $wishlistXmlObj->xmlentities(strip_tags($item->getDescription())); + $description = $wishlistXmlObj->escapeXml($item->getDescription()); $itemXmlObj->addChild('description', $description); - $addedDate = $wishlistXmlObj->xmlentities($this->getFormatedDate($item->getAddedAt())); + $addedDate = $wishlistXmlObj->escapeXml($this->getFormatedDate($item->getAddedAt())); $itemXmlObj->addChild('added_date', $addedDate); if ($this->getChild('product_price')) { diff --git a/app/code/core/Mage/XmlConnect/Helper/Customer/Form/Renderer.php b/app/code/core/Mage/XmlConnect/Helper/Customer/Form/Renderer.php index ea40a65c31..b7adcfc13a 100644 --- a/app/code/core/Mage/XmlConnect/Helper/Customer/Form/Renderer.php +++ b/app/code/core/Mage/XmlConnect/Helper/Customer/Form/Renderer.php @@ -37,11 +37,11 @@ class Mage_XmlConnect_Helper_Customer_Form_Renderer extends Mage_Core_Helper_Abs * Get title and required attributes for a field * * @param Mage_XmlConnect_Model_Simplexml_Form_Abstract $fieldsetXmlObj - * @param Enterprise_Customer_Block_Form_Renderer_Abstract $blockObject + * @param Enterprise_Eav_Block_Form_Renderer_Abstract $blockObject * @return array */ public function addTitleAndRequiredAttr(Mage_XmlConnect_Model_Simplexml_Form_Abstract $fieldsetXmlObj, - Enterprise_Customer_Block_Form_Renderer_Abstract $blockObject + Enterprise_Eav_Block_Form_Renderer_Abstract $blockObject ) { $attributes = array(); diff --git a/app/code/core/Mage/XmlConnect/Helper/Customer/Order.php b/app/code/core/Mage/XmlConnect/Helper/Customer/Order.php index b588441280..fe877b1135 100644 --- a/app/code/core/Mage/XmlConnect/Helper/Customer/Order.php +++ b/app/code/core/Mage/XmlConnect/Helper/Customer/Order.php @@ -62,7 +62,7 @@ public function addPriceAndSubtotalToXml(Mage_Core_Block_Template $renderer, Mag $typesOfDisplay = $renderer->getTypesOfDisplay(); if ($isIncludeTax) { $nodeName = 'including_tax'; - $nodeLabel = $renderer->__('Incl. Tax'); + $nodeLabel = Mage::helper('tax')->__('Incl. Tax'); $inclPrice = $renderer->helper('checkout')->getPriceInclTax($item); $inclSubtotal = $renderer->helper('checkout')->getSubtotalInclTax($item); @@ -77,7 +77,7 @@ public function addPriceAndSubtotalToXml(Mage_Core_Block_Template $renderer, Mag $weeeParams['include'] = $inclPrice; } else { $nodeName = 'excluding_tax'; - $nodeLabel = $renderer->__('Excl. Tax'); + $nodeLabel = Mage::helper('tax')->__('Excl. Tax'); if ($typesOfDisplay[self::PRICE_DISPLAY_TYPE_14]) { $price = $item->getPrice() + $renderer->getWeeeTaxAppliedAmount() @@ -221,19 +221,19 @@ public function addQuantityToXml(Mage_Core_Block_Template $renderer, ) { $qty = 1 * $item->getQtyOrdered(); if ($qty > 0) { - $quantityXml->addCustomChild('value', $qty, array('label' => $renderer->__('Ordered'))); + $quantityXml->addCustomChild('value', $qty, array('label' => Mage::helper('xmlconnect')->__('Ordered'))); } $qty = 1 * $item->getQtyShipped(); if ($qty > 0) { - $quantityXml->addCustomChild('value', $qty, array('label' => $renderer->__('Shipped'))); + $quantityXml->addCustomChild('value', $qty, array('label' => Mage::helper('xmlconnect')->__('Shipped'))); } $qty = 1 * $item->getQtyCanceled(); if ($qty > 0) { - $quantityXml->addCustomChild('value', $qty, array('label' => $renderer->__('Canceled'))); + $quantityXml->addCustomChild('value', $qty, array('label' => Mage::helper('xmlconnect')->__('Canceled'))); } $qty = 1 * $item->getQtyRefunded(); if ($qty > 0) { - $quantityXml->addCustomChild('value', $qty, array('label' => $renderer->__('Refunded'))); + $quantityXml->addCustomChild('value', $qty, array('label' => Mage::helper('xmlconnect')->__('Refunded'))); } } diff --git a/app/code/core/Mage/XmlConnect/Helper/Data.php b/app/code/core/Mage/XmlConnect/Helper/Data.php index daf287f808..1413a32a9f 100644 --- a/app/code/core/Mage/XmlConnect/Helper/Data.php +++ b/app/code/core/Mage/XmlConnect/Helper/Data.php @@ -43,6 +43,11 @@ class Mage_XmlConnect_Helper_Data extends Mage_Core_Helper_Abstract */ const MESSAGE_TITLE_LENGTH = 255; + /** + * Curl default timeout + */ + const CURLOPT_DEFAULT_TIMEOUT = 60; + /** * List of the keys for xml config that have to be excluded form application config * @@ -422,7 +427,6 @@ public function htmlize($body) { $w3cUrl = 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'; return <<getAppCode(); + $applicationId = Mage::getModel('xmlconnect/template')->load($queue->getTemplateId())->getApplicationId(); /** @var $app Mage_XmlConnect_Model_Application */ - $app = Mage::getModel('xmlconnect/application')->loadByCode($appCode); + $app = Mage::getModel('xmlconnect/application')->load($applicationId); if (!$app->getId()) { Mage::throwException( - Mage::helper('xmlconnect')->__('Can\'t load application with code "%s"', $appCode) + Mage::helper('xmlconnect')->__('Can\'t load application with id "%s"', $applicationId) ); } @@ -636,8 +640,6 @@ public function sendBroadcastMessage(Mage_XmlConnect_Model_Queue $queue) return; } - $userpwd = $app->getUserpwd(); - $sendType = $queue->getData('type'); switch ($sendType) { case Mage_XmlConnect_Model_Queue::MESSAGE_TYPE_AIRMAIL: @@ -652,29 +654,21 @@ public function sendBroadcastMessage(Mage_XmlConnect_Model_Queue $queue) break; } - $curlHandler = curl_init(Mage::getStoreConfig($configPath)); + $curl = new Varien_Http_Adapter_Curl(); + $curl->setConfig($this->_getCurlConfig($app->getUserpwd())); - $httpHeaders = $this->getHttpHeaders(); + $urbanUrl = Mage::getStoreConfig($configPath); + $curl->write( + Zend_Http_Client::POST, $urbanUrl, HTTP_REQUEST_HTTP_VER_1_1, $this->getHttpHeaders(), $params + ); - curl_setopt($curlHandler, CURLOPT_POST, 1); - curl_setopt($curlHandler, CURLOPT_HTTPHEADER, $httpHeaders); - curl_setopt($curlHandler, CURLOPT_POSTFIELDS, $params); - curl_setopt($curlHandler, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($curlHandler, CURLOPT_USERPWD, $userpwd); - curl_setopt($curlHandler, CURLOPT_TIMEOUT, 60); - - // Execute the request. - $result = curl_exec($curlHandler); - $succeeded = curl_errno($curlHandler) == 0 ? true : false; - - // close cURL resource, and free up system resources - curl_close($curlHandler); - - if ($succeeded && (is_null($result) || strtolower($result) == 'null')) { + if ($curl->read() && $curl->getInfo(CURLINFO_HTTP_CODE) == 200) { $queue->setStatus(Mage_XmlConnect_Model_Queue::STATUS_COMPLETED); } - $queue->setIsSent(true); + $curl->close(); + $queue->setIsSent(true); + $queue->save(); return; } catch (Exception $e) { Mage::logException($e); @@ -685,12 +679,23 @@ public function sendBroadcastMessage(Mage_XmlConnect_Model_Queue $queue) /** * Get headers for broadcast message * - * @return string + * @return array */ public function getHttpHeaders() { - $httpHeaders = array('Content-Type: application/json'); - return $httpHeaders; + return array('Content-Type: application/json'); + } + + /** + * Get urban airship curl request configuration + * + * @param string $userPwd + * @param int $timeout + * @return array + */ + protected function _getCurlConfig($userPwd, $timeout = self::CURLOPT_DEFAULT_TIMEOUT) + { + return array ('timeout' => $timeout, 'userpwd' => $userPwd); } /** diff --git a/app/code/core/Mage/XmlConnect/Model/Observer.php b/app/code/core/Mage/XmlConnect/Model/Observer.php index 5bf0b4de8f..398d059143 100644 --- a/app/code/core/Mage/XmlConnect/Model/Observer.php +++ b/app/code/core/Mage/XmlConnect/Model/Observer.php @@ -112,7 +112,6 @@ public function scheduledSend() foreach ($collection as $message) { if ($message->getId()) { Mage::helper('xmlconnect')->sendBroadcastMessage($message); - $message->save(); } } } diff --git a/app/code/core/Mage/XmlConnect/Model/Simplexml/Element.php b/app/code/core/Mage/XmlConnect/Model/Simplexml/Element.php index 67aea94c7a..2f60848fef 100644 --- a/app/code/core/Mage/XmlConnect/Model/Simplexml/Element.php +++ b/app/code/core/Mage/XmlConnect/Model/Simplexml/Element.php @@ -60,18 +60,55 @@ public function appendChild($source) return $this; } + /** + * Escape xml entities + * + * @param mixed $data + * @param bool $stripTags + * @param array $allowedTags + * @return mixed + */ + public function escapeXml($data, $stripTags = true, $allowedTags = null) + { + if (is_array($data)) { + $result = array(); + foreach ($data as $item) { + if ($stripTags) { + $item = Mage::helper('core')->stripTags($item, $allowedTags); + } + $result[] = $this->xmlentities($item); + } + } else { + if (is_null($data)) { + $data = $this; + } + $data = (string)$data; + + if ($stripTags) { + $data = Mage::helper('core')->stripTags($data, $allowedTags); + } + $result = $this->xmlentities($data); + } + return $result; + } + /** * Converts meaningful xml character (") to xml attribute specification * * @param string $value + * @param bool $stripTags * @return string|this */ - public function xmlAttribute($value = null) + public function xmlAttribute($value = null, $stripTags = true) { if (is_null($value)) { $value = $this; } $value = (string)$value; + + if ($stripTags) { + $value = Mage::helper('core')->stripTags($value); + } $value = str_replace(array('&', '"', '<', '>'), array('&', '"', '<', '>'), $value); return $value; } diff --git a/app/code/core/Mage/XmlConnect/Model/Simplexml/Form/Abstract.php b/app/code/core/Mage/XmlConnect/Model/Simplexml/Form/Abstract.php index 7608e9068d..78ce6bc426 100644 --- a/app/code/core/Mage/XmlConnect/Model/Simplexml/Form/Abstract.php +++ b/app/code/core/Mage/XmlConnect/Model/Simplexml/Form/Abstract.php @@ -182,7 +182,7 @@ public function addType($type, $className) public function getElements() { if (empty($this->_elements)) { - $this->_elements = Mage::getModel('xmlconnect/simplexml_form_element_collection'); + $this->_elements = Mage::getModel('xmlconnect/simplexml_form_element_collection', $this); } return $this->_elements; } diff --git a/app/code/core/Mage/XmlConnect/Model/Simplexml/Form/Element/Validator/Abstract.php b/app/code/core/Mage/XmlConnect/Model/Simplexml/Form/Element/Validator/Abstract.php index 91eb1c1e4d..348e76a2f3 100644 --- a/app/code/core/Mage/XmlConnect/Model/Simplexml/Form/Element/Validator/Abstract.php +++ b/app/code/core/Mage/XmlConnect/Model/Simplexml/Form/Element/Validator/Abstract.php @@ -71,22 +71,21 @@ public function __construct($attributes = array()) */ protected function _setDefaultValidatorTypeMessages() { - $helper = Mage::helper('xmlconnect'); $this->_validatorTypeMessages = array( - 'min_length' => $helper->__('Text length does not satisfy specified min text range.'), - 'max_length' => $helper->__('Text length does not satisfy specified max text range.'), - 'alphanumeric' => $helper->__('Please use only letters (a-z or A-Z) or numbers (0-9) only in this field. No spaces or other characters are allowed.'), - 'email' => $helper->__('Please enter a valid email address. For example johndoe@domain.com.'), - 'required' => $helper->__('This is a required field.'), - 'required_select' => $helper->__('Please select an option.'), - 'numeric' => $helper->__('Please use numbers only in this field. Please avoid spaces or other characters such as dots or commas.'), - 'alpha' => $helper->__('Please use letters only (a-z or A-Z) in this field.'), - 'url' => $helper->__('Please enter a valid URL. Protocol is required (http://, https:// or ftp://)'), - 'date' => $helper->__('Please enter a valid date.'), - 'max_file_size' => $helper->__('\'%s\' exceeds the allowed file size: %d (bytes)', $this->getFieldLabel(), $this->getValue()), - 'file_extensions' => $helper->__('\'%s\' is not a valid file extension. Allowed extensions: %s', $this->getFieldLabel(), $this->getValue()), - 'max_image_width' => $helper->__('\'%s\' width exceeds allowed value of %d px', $this->getFieldLabel(), $this->getValue()), - 'max_image_height' => $helper->__('\'%s\' height exceeds allowed value of %d px', $this->getFieldLabel(), $this->getValue()) + 'min_length' => Mage::helper('xmlconnect')->__('Text length does not satisfy specified min text range.'), + 'max_length' => Mage::helper('xmlconnect')->__('Text length does not satisfy specified max text range.'), + 'alphanumeric' => Mage::helper('xmlconnect')->__('Please use only letters (a-z or A-Z) or numbers (0-9) only in this field. No spaces or other characters are allowed.'), + 'email' => Mage::helper('xmlconnect')->__('Please enter a valid email address. For example johndoe@domain.com.'), + 'required' => Mage::helper('xmlconnect')->__('This is a required field.'), + 'required_select' => Mage::helper('xmlconnect')->__('Please select an option.'), + 'numeric' => Mage::helper('xmlconnect')->__('Please use numbers only in this field. Please avoid spaces or other characters such as dots or commas.'), + 'alpha' => Mage::helper('xmlconnect')->__('Please use letters only (a-z or A-Z) in this field.'), + 'url' => Mage::helper('xmlconnect')->__('Please enter a valid URL. Protocol is required (http://, https:// or ftp://)'), + 'date' => Mage::helper('xmlconnect')->__('Please enter a valid date.'), + 'max_file_size' => Mage::helper('xmlconnect')->__('\'%s\' exceeds the allowed file size: %d (bytes)', $this->getFieldLabel(), $this->getValue()), + 'file_extensions' => Mage::helper('xmlconnect')->__('\'%s\' is not a valid file extension. Allowed extensions: %s', $this->getFieldLabel(), $this->getValue()), + 'max_image_width' => Mage::helper('xmlconnect')->__('\'%s\' width exceeds allowed value of %d px', $this->getFieldLabel(), $this->getValue()), + 'max_image_height' => Mage::helper('xmlconnect')->__('\'%s\' height exceeds allowed value of %d px', $this->getFieldLabel(), $this->getValue()) ); return $this; } diff --git a/app/code/core/Mage/XmlConnect/controllers/Adminhtml/MobileController.php b/app/code/core/Mage/XmlConnect/controllers/Adminhtml/MobileController.php index 8b81bca344..273e0ce38e 100644 --- a/app/code/core/Mage/XmlConnect/controllers/Adminhtml/MobileController.php +++ b/app/code/core/Mage/XmlConnect/controllers/Adminhtml/MobileController.php @@ -317,25 +317,29 @@ protected function _clearSessionData() protected function _processPostRequest() { try { + /** @var $app Mage_XmlConnect_Model_Application */ $app = Mage::helper('xmlconnect')->getApplication(); $params = $app->getSubmitParams(); $appConnectorUrl = Mage::getStoreConfig('xmlconnect/mobile_application/magentocommerce_url'); - $curlHandler = curl_init($appConnectorUrl . $params['key']); - - // set URL and other appropriate options - curl_setopt($curlHandler, CURLOPT_POST, 1); - curl_setopt($curlHandler, CURLOPT_POSTFIELDS, $params); - curl_setopt($curlHandler, CURLOPT_SSL_VERIFYHOST, 2); - curl_setopt($curlHandler, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($curlHandler, CURLOPT_SSL_VERIFYPEER, FALSE); - curl_setopt($curlHandler, CURLOPT_TIMEOUT, 60); - // Execute the request. - $result = curl_exec($curlHandler); - - // close cURL resource, and free up system resources - curl_close($curlHandler); + $curl = new Varien_Http_Adapter_Curl(); + $verifyPeerValue = Mage::getStoreConfig('xmlconnect/mobile_application/curl_ssl_verifypeer'); + $curl->setConfig(array( + 'timeout' => Mage_XmlConnect_Helper_Data::CURLOPT_DEFAULT_TIMEOUT, + 'verifypeer' => $verifyPeerValue, 'verifyhost' => 2, 'header' => false + )); + + $mCommerceUrl = $appConnectorUrl . $params['key']; + $curl->write(Zend_Http_Client::POST, $mCommerceUrl, CURL_HTTP_VERSION_1_1, array(), $params); + + $result = $curl->read(); + if(false === $result) { + Mage::log('Curl error: ' . $curl->getError()); + $curl->close(); + Mage::throwException($this->__('Request internal error.')); + } + $curl->close(); // Assert that we received an expected message in reponse. $resultArray = json_decode($result, true); @@ -1313,13 +1317,13 @@ public function queueMessageAction() } if (isset($template)) { - $appCode = $template->getAppCode(); + $applicationId = $template->getApplicationId(); } else { - $appCode = Mage::getModel('xmlconnect/template')->load($message->getTemplateId())->getAppCode(); + $applicationId = Mage::getModel('xmlconnect/template')->load($message->getTemplateId())->getApplicationId(); } /** @var $app Mage_XmlConnect_Model_Application */ - $app = Mage::getModel('xmlconnect/application')->loadByCode($appCode); + $app = Mage::getModel('xmlconnect/application')->load($applicationId); if(!$app->isNotificationsActive()) { $this->_getSession()->addError( diff --git a/app/code/core/Mage/XmlConnect/controllers/CatalogController.php b/app/code/core/Mage/XmlConnect/controllers/CatalogController.php index 9b1381c2e8..a8775f1aff 100644 --- a/app/code/core/Mage/XmlConnect/controllers/CatalogController.php +++ b/app/code/core/Mage/XmlConnect/controllers/CatalogController.php @@ -295,14 +295,7 @@ public function sendEmailAction() $model->setWebsiteId(Mage::app()->getStore()->getWebsiteId()); Mage::register('send_to_friend_model', $model); -/* - if ($model->getMaxSendsToFriend()) { - $this->_message($this->__('Messages cannot be sent more than %d times in an hour.', - $model->getMaxSendsToFriend()), - self::MESSAGE_STATUS_WARNING); - return $this; - } -*/ + $data = $this->getRequest()->getPost(); if (!$data) { diff --git a/app/code/core/Mage/XmlConnect/controllers/CheckoutController.php b/app/code/core/Mage/XmlConnect/controllers/CheckoutController.php index 4cc97dbcb5..c7a0161136 100644 --- a/app/code/core/Mage/XmlConnect/controllers/CheckoutController.php +++ b/app/code/core/Mage/XmlConnect/controllers/CheckoutController.php @@ -332,8 +332,7 @@ public function saveShippingMethodAction() if (!$result) { Mage::dispatchEvent('checkout_controller_onepage_save_shipping_method', array( - 'request' => $this->getRequest(), - 'quote' => $this->getOnepage()->getQuote() + 'request' => $this->getRequest(), 'quote' => $this->getOnepage()->getQuote() )); $this->getOnepage()->getQuote()->collectTotals()->save(); @@ -343,8 +342,7 @@ public function saveShippingMethodAction() $result['message'] = array($result['message']); } Mage::dispatchEvent('checkout_controller_onepage_save_shipping_method', array( - 'request' => $this->getRequest(), - 'quote' => $this->getOnepage()->getQuote() + 'request' => $this->getRequest(), 'quote' => $this->getOnepage()->getQuote() )); $this->getOnepage()->getQuote()->collectTotals()->save(); $this->_message(implode('. ', $result['message']), self::MESSAGE_STATUS_ERROR); diff --git a/app/code/core/Mage/XmlConnect/controllers/CustomerController.php b/app/code/core/Mage/XmlConnect/controllers/CustomerController.php index 35ce112d60..d28f38207d 100644 --- a/app/code/core/Mage/XmlConnect/controllers/CustomerController.php +++ b/app/code/core/Mage/XmlConnect/controllers/CustomerController.php @@ -367,7 +367,6 @@ public function forgotPasswordAction() public function addressAction() { if (!$this->_getSession()->isLoggedIn()) { - Mage::log('address:'.$this->_getSession()->getSessionId()); $this->_message( $this->__('Customer not logged in.'), self::MESSAGE_STATUS_ERROR, array('logged_in' => '0') ); diff --git a/app/code/core/Mage/XmlConnect/controllers/PbridgeController.php b/app/code/core/Mage/XmlConnect/controllers/PbridgeController.php index 8a33ad5a1f..c9ee4ea260 100755 --- a/app/code/core/Mage/XmlConnect/controllers/PbridgeController.php +++ b/app/code/core/Mage/XmlConnect/controllers/PbridgeController.php @@ -60,9 +60,7 @@ protected function _initActionLayout() protected function _checkPbridge() { if (!is_object(Mage::getConfig()->getNode('modules/Enterprise_Pbridge'))) { - $this->getResponse()->setBody( - $this->__('Payment Bridge module unavailable.') - ); + $this->getResponse()->setBody($this->__('Payment Bridge module unavailable.')); return false; } return true; diff --git a/app/code/core/Mage/XmlConnect/etc/config.xml b/app/code/core/Mage/XmlConnect/etc/config.xml index 339824ed87..5e9b71ab25 100644 --- a/app/code/core/Mage/XmlConnect/etc/config.xml +++ b/app/code/core/Mage/XmlConnect/etc/config.xml @@ -29,7 +29,7 @@ 1.6.0.0 - 22.0 + 22.1 @@ -387,6 +387,7 @@ + 1 www.magentocommerce.com/products/index.php/mobile/activate/index/key/ http://www.magentocommerce.com/product/mobile http://www.magentocommerce.com/product/mobile#resubmission diff --git a/app/code/core/Zend/Date.php b/app/code/core/Zend/Date.php new file mode 100644 index 0000000000..1992f51bdb --- /dev/null +++ b/app/code/core/Zend/Date.php @@ -0,0 +1,5008 @@ + 'iso', // format for date strings 'iso' or 'php' + 'fix_dst' => true, // fix dst on summer/winter time change + 'extend_month' => false, // false - addMonth like SQL, true like excel + 'cache' => null, // cache to set + 'timesync' => null // timesync server to set + ); + + // Class wide Date Constants + const DAY = 'dd'; + const DAY_SHORT = 'd'; + const DAY_SUFFIX = 'SS'; + const DAY_OF_YEAR = 'D'; + const WEEKDAY = 'EEEE'; + const WEEKDAY_SHORT = 'EEE'; + const WEEKDAY_NARROW = 'E'; + const WEEKDAY_NAME = 'EE'; + const WEEKDAY_8601 = 'eee'; + const WEEKDAY_DIGIT = 'e'; + const WEEK = 'ww'; + const MONTH = 'MM'; + const MONTH_SHORT = 'M'; + const MONTH_DAYS = 'ddd'; + const MONTH_NAME = 'MMMM'; + const MONTH_NAME_SHORT = 'MMM'; + const MONTH_NAME_NARROW = 'MMMMM'; + const YEAR = 'y'; + const YEAR_SHORT = 'yy'; + const YEAR_8601 = 'Y'; + const YEAR_SHORT_8601 = 'YY'; + const LEAPYEAR = 'l'; + const MERIDIEM = 'a'; + const SWATCH = 'B'; + const HOUR = 'HH'; + const HOUR_SHORT = 'H'; + const HOUR_AM = 'hh'; + const HOUR_SHORT_AM = 'h'; + const MINUTE = 'mm'; + const MINUTE_SHORT = 'm'; + const SECOND = 'ss'; + const SECOND_SHORT = 's'; + const MILLISECOND = 'S'; + const TIMEZONE_NAME = 'zzzz'; + const DAYLIGHT = 'I'; + const GMT_DIFF = 'Z'; + const GMT_DIFF_SEP = 'ZZZZ'; + const TIMEZONE = 'z'; + const TIMEZONE_SECS = 'X'; + const ISO_8601 = 'c'; + const RFC_2822 = 'r'; + const TIMESTAMP = 'U'; + const ERA = 'G'; + const ERA_NAME = 'GGGG'; + const ERA_NARROW = 'GGGGG'; + const DATES = 'F'; + const DATE_FULL = 'FFFFF'; + const DATE_LONG = 'FFFF'; + const DATE_MEDIUM = 'FFF'; + const DATE_SHORT = 'FF'; + const TIMES = 'WW'; + const TIME_FULL = 'TTTTT'; + const TIME_LONG = 'TTTT'; + const TIME_MEDIUM = 'TTT'; + const TIME_SHORT = 'TT'; + const DATETIME = 'K'; + const DATETIME_FULL = 'KKKKK'; + const DATETIME_LONG = 'KKKK'; + const DATETIME_MEDIUM = 'KKK'; + const DATETIME_SHORT = 'KK'; + const ATOM = 'OOO'; + const COOKIE = 'CCC'; + const RFC_822 = 'R'; + const RFC_850 = 'RR'; + const RFC_1036 = 'RRR'; + const RFC_1123 = 'RRRR'; + const RFC_3339 = 'RRRRR'; + const RSS = 'SSS'; + const W3C = 'WWW'; + + /** + * Minimum allowed year value + */ + const YEAR_MIN_VALUE = -10000; + + /** + * Maximum allowed year value + */ + const YEAR_MAX_VALUE = 10000; + + /** + * Generates the standard date object, could be a unix timestamp, localized date, + * string, integer, array and so on. Also parts of dates or time are supported + * Always set the default timezone: http://php.net/date_default_timezone_set + * For example, in your bootstrap: date_default_timezone_set('America/Los_Angeles'); + * For detailed instructions please look in the docu. + * + * @param string|integer|Zend_Date|array $date OPTIONAL Date value or value of date part to set + * ,depending on $part. If null the actual time is set + * @param string $part OPTIONAL Defines the input format of $date + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date + * @throws Zend_Date_Exception + */ + public function __construct($date = null, $part = null, $locale = null) + { + if (is_object($date) and !($date instanceof Zend_TimeSync_Protocol) and + !($date instanceof Zend_Date)) { + if ($locale instanceof Zend_Locale) { + $locale = $date; + $date = null; + $part = null; + } else { + $date = (string) $date; + } + } + + if (($date !== null) and !is_array($date) and !($date instanceof Zend_TimeSync_Protocol) and + !($date instanceof Zend_Date) and !defined($date) and Zend_Locale::isLocale($date, true, false)) { + $locale = $date; + $date = null; + $part = null; + } else if (($part !== null) and !defined($part) and Zend_Locale::isLocale($part, true, false)) { + $locale = $part; + $part = null; + } + + $this->setLocale($locale); + if (is_string($date) && ($part === null) && (strlen($date) <= 5)) { + $part = $date; + $date = null; + } + + if ($date === null) { + if ($part === null) { + $date = time(); + } else if ($part !== self::TIMESTAMP) { + $date = self::now($locale); + $date = $date->get($part); + } + } + + if ($date instanceof Zend_TimeSync_Protocol) { + $date = $date->getInfo(); + $date = $this->_getTime($date['offset']); + $part = null; + } else if (parent::$_defaultOffset != 0) { + $date = $this->_getTime(parent::$_defaultOffset); + } + + // set the timezone and offset for $this + $zone = @date_default_timezone_get(); + $this->setTimezone($zone); + + // try to get timezone from date-string + if (!is_int($date)) { + $zone = $this->getTimezoneFromString($date); + $this->setTimezone($zone); + } + + // set datepart + if (($part !== null && $part !== self::TIMESTAMP) or (!is_numeric($date))) { + // switch off dst handling for value setting + $this->setUnixTimestamp($this->getGmtOffset()); + $this->set($date, $part, $this->_locale); + + // DST fix + if (is_array($date) === true) { + if (!isset($date['hour'])) { + $date['hour'] = 0; + } + + $hour = $this->toString('H', 'iso', true); + $hour = $date['hour'] - $hour; + switch ($hour) { + case 1 : + case -23 : + $this->addTimestamp(3600); + break; + case -1 : + case 23 : + $this->subTimestamp(3600); + break; + case 2 : + case -22 : + $this->addTimestamp(7200); + break; + case -2 : + case 22 : + $this->subTimestamp(7200); + break; + } + } + } else { + $this->setUnixTimestamp($date); + } + } + + /** + * Sets class wide options, if no option was given, the actual set options will be returned + * + * @param array $options Options to set + * @throws Zend_Date_Exception + * @return Options array if no option was given + */ + public static function setOptions(array $options = array()) + { + if (empty($options)) { + return self::$_options; + } + + foreach ($options as $name => $value) { + $name = strtolower($name); + + if (array_key_exists($name, self::$_options)) { + switch($name) { + case 'format_type' : + if ((strtolower($value) != 'php') && (strtolower($value) != 'iso')) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("Unknown format type ($value) for dates, only 'iso' and 'php' supported", 0, null, $value); + } + break; + case 'fix_dst' : + if (!is_bool($value)) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("'fix_dst' has to be boolean", 0, null, $value); + } + break; + case 'extend_month' : + if (!is_bool($value)) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("'extend_month' has to be boolean", 0, null, $value); + } + break; + case 'cache' : + if ($value === null) { + parent::$_cache = null; + } else { + if (!$value instanceof Zend_Cache_Core) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("Instance of Zend_Cache expected"); + } + + parent::$_cache = $value; + parent::$_cacheTags = Zend_Date_DateObject::_getTagSupportForCache(); + Zend_Locale_Data::setCache($value); + } + break; + case 'timesync' : + if ($value === null) { + parent::$_defaultOffset = 0; + } else { + if (!$value instanceof Zend_TimeSync_Protocol) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("Instance of Zend_TimeSync expected"); + } + + $date = $value->getInfo(); + parent::$_defaultOffset = $date['offset']; + } + break; + } + self::$_options[$name] = $value; + } + else { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("Unknown option: $name = $value"); + } + } + } + + /** + * Returns this object's internal UNIX timestamp (equivalent to Zend_Date::TIMESTAMP). + * If the timestamp is too large for integers, then the return value will be a string. + * This function does not return the timestamp as an object. + * Use clone() or copyPart() instead. + * + * @return integer|string UNIX timestamp + */ + public function getTimestamp() + { + return $this->getUnixTimestamp(); + } + + /** + * Returns the calculated timestamp + * HINT: timestamps are always GMT + * + * @param string $calc Type of calculation to make + * @param string|integer|array|Zend_Date $stamp Timestamp to calculate, when null the actual timestamp is calculated + * @return Zend_Date|integer + * @throws Zend_Date_Exception + */ + private function _timestamp($calc, $stamp) + { + if ($stamp instanceof Zend_Date) { + // extract timestamp from object + $stamp = $stamp->getTimestamp(); + } + + if (is_array($stamp)) { + if (isset($stamp['timestamp']) === true) { + $stamp = $stamp['timestamp']; + } else { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception('no timestamp given in array'); + } + } + + if ($calc === 'set') { + $return = $this->setUnixTimestamp($stamp); + } else { + $return = $this->_calcdetail($calc, $stamp, self::TIMESTAMP, null); + } + if ($calc != 'cmp') { + return $this; + } + return $return; + } + + /** + * Sets a new timestamp + * + * @param integer|string|array|Zend_Date $timestamp Timestamp to set + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function setTimestamp($timestamp) + { + return $this->_timestamp('set', $timestamp); + } + + /** + * Adds a timestamp + * + * @param integer|string|array|Zend_Date $timestamp Timestamp to add + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function addTimestamp($timestamp) + { + return $this->_timestamp('add', $timestamp); + } + + /** + * Subtracts a timestamp + * + * @param integer|string|array|Zend_Date $timestamp Timestamp to sub + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function subTimestamp($timestamp) + { + return $this->_timestamp('sub', $timestamp); + } + + /** + * Compares two timestamps, returning the difference as integer + * + * @param integer|string|array|Zend_Date $timestamp Timestamp to compare + * @return integer 0 = equal, 1 = later, -1 = earlier + * @throws Zend_Date_Exception + */ + public function compareTimestamp($timestamp) + { + return $this->_timestamp('cmp', $timestamp); + } + + /** + * Returns a string representation of the object + * Supported format tokens are: + * G - era, y - year, Y - ISO year, M - month, w - week of year, D - day of year, d - day of month + * E - day of week, e - number of weekday (1-7), h - hour 1-12, H - hour 0-23, m - minute, s - second + * A - milliseconds of day, z - timezone, Z - timezone offset, S - fractional second, a - period of day + * + * Additionally format tokens but non ISO conform are: + * SS - day suffix, eee - php number of weekday(0-6), ddd - number of days per month + * l - Leap year, B - swatch internet time, I - daylight saving time, X - timezone offset in seconds + * r - RFC2822 format, U - unix timestamp + * + * Not supported ISO tokens are + * u - extended year, Q - quarter, q - quarter, L - stand alone month, W - week of month + * F - day of week of month, g - modified julian, c - stand alone weekday, k - hour 0-11, K - hour 1-24 + * v - wall zone + * + * @param string $format OPTIONAL Rule for formatting output. If null the default date format is used + * @param string $type OPTIONAL Type for the format string which overrides the standard setting + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return string + */ + public function toString($format = null, $type = null, $locale = null) + { + if (is_object($format)) { + if ($format instanceof Zend_Locale) { + $locale = $format; + $format = null; + } else { + $format = (string) $format; + } + } + + if (is_object($type)) { + if ($type instanceof Zend_Locale) { + $locale = $type; + $type = null; + } else { + $type = (string) $type; + } + } + + if (($format !== null) && !defined($format) + && ($format != 'ee') && ($format != 'ss') && ($format != 'GG') && ($format != 'MM') && ($format != 'EE') && ($format != 'TT') + && Zend_Locale::isLocale($format, null, false)) { + $locale = $format; + $format = null; + } + + if (($type !== null) and ($type != 'php') and ($type != 'iso') and + Zend_Locale::isLocale($type, null, false)) { + $locale = $type; + $type = null; + } + + if ($locale === null) { + $locale = $this->getLocale(); + } + + if ($format === null) { + $format = Zend_Locale_Format::getDateFormat($locale) . ' ' . Zend_Locale_Format::getTimeFormat($locale); + } else if (((self::$_options['format_type'] == 'php') && ($type === null)) or ($type == 'php')) { + $format = Zend_Locale_Format::convertPhpToIsoFormat($format); + } + + return $this->date($this->_toToken($format, $locale), $this->getUnixTimestamp(), false); + } + + /** + * Returns a string representation of the date which is equal with the timestamp + * + * @return string + */ + public function __toString() + { + return $this->toString(null, $this->_locale); + } + + /** + * Returns a integer representation of the object + * But returns false when the given part is no value f.e. Month-Name + * + * @param string|integer|Zend_Date $part OPTIONAL Defines the date or datepart to return as integer + * @return integer|false + */ + public function toValue($part = null) + { + $result = $this->get($part); + if (is_numeric($result)) { + return intval("$result"); + } else { + return false; + } + } + + /** + * Returns an array representation of the object + * + * @return array + */ + public function toArray() + { + return array('day' => $this->toString(self::DAY_SHORT, 'iso'), + 'month' => $this->toString(self::MONTH_SHORT, 'iso'), + 'year' => $this->toString(self::YEAR, 'iso'), + 'hour' => $this->toString(self::HOUR_SHORT, 'iso'), + 'minute' => $this->toString(self::MINUTE_SHORT, 'iso'), + 'second' => $this->toString(self::SECOND_SHORT, 'iso'), + 'timezone' => $this->toString(self::TIMEZONE, 'iso'), + 'timestamp' => $this->toString(self::TIMESTAMP, 'iso'), + 'weekday' => $this->toString(self::WEEKDAY_8601, 'iso'), + 'dayofyear' => $this->toString(self::DAY_OF_YEAR, 'iso'), + 'week' => $this->toString(self::WEEK, 'iso'), + 'gmtsecs' => $this->toString(self::TIMEZONE_SECS, 'iso')); + } + + /** + * Returns a representation of a date or datepart + * This could be for example a localized monthname, the time without date, + * the era or only the fractional seconds. There are about 50 different supported date parts. + * For a complete list of supported datepart values look into the docu + * + * @param string $part OPTIONAL Part of the date to return, if null the timestamp is returned + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return string date or datepart + */ + public function get($part = null, $locale = null) + { + if ($locale === null) { + $locale = $this->getLocale(); + } + + if (($part !== null) && !defined($part) + && ($part != 'ee') && ($part != 'ss') && ($part != 'GG') && ($part != 'MM') && ($part != 'EE') && ($part != 'TT') + && Zend_Locale::isLocale($part, null, false)) { + $locale = $part; + $part = null; + } + + if ($part === null) { + $part = self::TIMESTAMP; + } else if (self::$_options['format_type'] == 'php') { + $part = Zend_Locale_Format::convertPhpToIsoFormat($part); + } + + return $this->date($this->_toToken($part, $locale), $this->getUnixTimestamp(), false); + } + + /** + * Internal method to apply tokens + * + * @param string $part + * @param string $locale + * @return string + */ + private function _toToken($part, $locale) { + // get format tokens + $comment = false; + $format = ''; + $orig = ''; + for ($i = 0; isset($part[$i]); ++$i) { + if ($part[$i] == "'") { + $comment = $comment ? false : true; + if (isset($part[$i+1]) && ($part[$i+1] == "'")) { + $comment = $comment ? false : true; + $format .= "\\'"; + ++$i; + } + + $orig = ''; + continue; + } + + if ($comment) { + $format .= '\\' . $part[$i]; + $orig = ''; + } else { + $orig .= $part[$i]; + if (!isset($part[$i+1]) || (isset($orig[0]) && ($orig[0] != $part[$i+1]))) { + $format .= $this->_parseIsoToDate($orig, $locale); + $orig = ''; + } + } + } + + return $format; + } + + /** + * Internal parsing method + * + * @param string $token + * @param string $locale + * @return string + */ + private function _parseIsoToDate($token, $locale) { + switch($token) { + case self::DAY : + return 'd'; + break; + + case self::WEEKDAY_SHORT : + $weekday = strtolower($this->date('D', $this->getUnixTimestamp(), false)); + $day = Zend_Locale_Data::getContent($locale, 'day', array('gregorian', 'format', 'wide', $weekday)); + return $this->_toComment(iconv_substr($day, 0, 3, 'UTF-8')); + break; + + case self::DAY_SHORT : + return 'j'; + break; + + case self::WEEKDAY : + $weekday = strtolower($this->date('D', $this->getUnixTimestamp(), false)); + return $this->_toComment(Zend_Locale_Data::getContent($locale, 'day', array('gregorian', 'format', 'wide', $weekday))); + break; + + case self::WEEKDAY_8601 : + return 'N'; + break; + + case 'ee' : + return $this->_toComment(str_pad($this->date('N', $this->getUnixTimestamp(), false), 2, '0', STR_PAD_LEFT)); + break; + + case self::DAY_SUFFIX : + return 'S'; + break; + + case self::WEEKDAY_DIGIT : + return 'w'; + break; + + case self::DAY_OF_YEAR : + return 'z'; + break; + + case 'DDD' : + return $this->_toComment(str_pad($this->date('z', $this->getUnixTimestamp(), false), 3, '0', STR_PAD_LEFT)); + break; + + case 'DD' : + return $this->_toComment(str_pad($this->date('z', $this->getUnixTimestamp(), false), 2, '0', STR_PAD_LEFT)); + break; + + case self::WEEKDAY_NARROW : + case 'EEEEE' : + $weekday = strtolower($this->date('D', $this->getUnixTimestamp(), false)); + $day = Zend_Locale_Data::getContent($locale, 'day', array('gregorian', 'format', 'abbreviated', $weekday)); + return $this->_toComment(iconv_substr($day, 0, 1, 'UTF-8')); + break; + + case self::WEEKDAY_NAME : + $weekday = strtolower($this->date('D', $this->getUnixTimestamp(), false)); + return $this->_toComment(Zend_Locale_Data::getContent($locale, 'day', array('gregorian', 'format', 'abbreviated', $weekday))); + break; + + case 'w' : + $week = $this->date('W', $this->getUnixTimestamp(), false); + return $this->_toComment(($week[0] == '0') ? $week[1] : $week); + break; + + case self::WEEK : + return 'W'; + break; + + case self::MONTH_NAME : + $month = $this->date('n', $this->getUnixTimestamp(), false); + return $this->_toComment(Zend_Locale_Data::getContent($locale, 'month', array('gregorian', 'format', 'wide', $month))); + break; + + case self::MONTH : + return 'm'; + break; + + case self::MONTH_NAME_SHORT : + $month = $this->date('n', $this->getUnixTimestamp(), false); + return $this->_toComment(Zend_Locale_Data::getContent($locale, 'month', array('gregorian', 'format', 'abbreviated', $month))); + break; + + case self::MONTH_SHORT : + return 'n'; + break; + + case self::MONTH_DAYS : + return 't'; + break; + + case self::MONTH_NAME_NARROW : + $month = $this->date('n', $this->getUnixTimestamp(), false); + $mon = Zend_Locale_Data::getContent($locale, 'month', array('gregorian', 'format', 'abbreviated', $month)); + return $this->_toComment(iconv_substr($mon, 0, 1, 'UTF-8')); + break; + + case self::LEAPYEAR : + return 'L'; + break; + + case self::YEAR_8601 : + return 'o'; + break; + + case self::YEAR : + return 'Y'; + break; + + case self::YEAR_SHORT : + return 'y'; + break; + + case self::YEAR_SHORT_8601 : + return $this->_toComment(substr($this->date('o', $this->getUnixTimestamp(), false), -2, 2)); + break; + + case self::MERIDIEM : + $am = $this->date('a', $this->getUnixTimestamp(), false); + if ($am == 'am') { + return $this->_toComment(Zend_Locale_Data::getContent($locale, 'am')); + } + + return $this->_toComment(Zend_Locale_Data::getContent($locale, 'pm')); + break; + + case self::SWATCH : + return 'B'; + break; + + case self::HOUR_SHORT_AM : + return 'g'; + break; + + case self::HOUR_SHORT : + return 'G'; + break; + + case self::HOUR_AM : + return 'h'; + break; + + case self::HOUR : + return 'H'; + break; + + case self::MINUTE : + return $this->_toComment(str_pad($this->date('i', $this->getUnixTimestamp(), false), 2, '0', STR_PAD_LEFT)); + break; + + case self::SECOND : + return $this->_toComment(str_pad($this->date('s', $this->getUnixTimestamp(), false), 2, '0', STR_PAD_LEFT)); + break; + + case self::MINUTE_SHORT : + return 'i'; + break; + + case self::SECOND_SHORT : + return 's'; + break; + + case self::MILLISECOND : + return $this->_toComment($this->getMilliSecond()); + break; + + case self::TIMEZONE_NAME : + case 'vvvv' : + return 'e'; + break; + + case self::DAYLIGHT : + return 'I'; + break; + + case self::GMT_DIFF : + case 'ZZ' : + case 'ZZZ' : + return 'O'; + break; + + case self::GMT_DIFF_SEP : + return 'P'; + break; + + case self::TIMEZONE : + case 'v' : + case 'zz' : + case 'zzz' : + return 'T'; + break; + + case self::TIMEZONE_SECS : + return 'Z'; + break; + + case self::ISO_8601 : + return 'c'; + break; + + case self::RFC_2822 : + return 'r'; + break; + + case self::TIMESTAMP : + return 'U'; + break; + + case self::ERA : + case 'GG' : + case 'GGG' : + $year = $this->date('Y', $this->getUnixTimestamp(), false); + if ($year < 0) { + return $this->_toComment(Zend_Locale_Data::getContent($locale, 'era', array('gregorian', 'Abbr', '0'))); + } + + return $this->_toComment(Zend_Locale_Data::getContent($locale, 'era', array('gregorian', 'Abbr', '1'))); + break; + + case self::ERA_NARROW : + $year = $this->date('Y', $this->getUnixTimestamp(), false); + if ($year < 0) { + return $this->_toComment(iconv_substr(Zend_Locale_Data::getContent($locale, 'era', array('gregorian', 'Abbr', '0')), 0, 1, 'UTF-8')) . '.'; + } + + return $this->_toComment(iconv_substr(Zend_Locale_Data::getContent($locale, 'era', array('gregorian', 'Abbr', '1')), 0, 1, 'UTF-8')) . '.'; + break; + + case self::ERA_NAME : + $year = $this->date('Y', $this->getUnixTimestamp(), false); + if ($year < 0) { + return $this->_toComment(Zend_Locale_Data::getContent($locale, 'era', array('gregorian', 'Names', '0'))); + } + + return $this->_toComment(Zend_Locale_Data::getContent($locale, 'era', array('gregorian', 'Names', '1'))); + break; + + case self::DATES : + return $this->_toToken(Zend_Locale_Format::getDateFormat($locale), $locale); + break; + + case self::DATE_FULL : + return $this->_toToken(Zend_Locale_Data::getContent($locale, 'date', array('gregorian', 'full')), $locale); + break; + + case self::DATE_LONG : + return $this->_toToken(Zend_Locale_Data::getContent($locale, 'date', array('gregorian', 'long')), $locale); + break; + + case self::DATE_MEDIUM : + return $this->_toToken(Zend_Locale_Data::getContent($locale, 'date', array('gregorian', 'medium')), $locale); + break; + + case self::DATE_SHORT : + return $this->_toToken(Zend_Locale_Data::getContent($locale, 'date', array('gregorian', 'short')), $locale); + break; + + case self::TIMES : + return $this->_toToken(Zend_Locale_Format::getTimeFormat($locale), $locale); + break; + + case self::TIME_FULL : + return $this->_toToken(Zend_Locale_Data::getContent($locale, 'time', 'full'), $locale); + break; + + case self::TIME_LONG : + return $this->_toToken(Zend_Locale_Data::getContent($locale, 'time', 'long'), $locale); + break; + + case self::TIME_MEDIUM : + return $this->_toToken(Zend_Locale_Data::getContent($locale, 'time', 'medium'), $locale); + break; + + case self::TIME_SHORT : + return $this->_toToken(Zend_Locale_Data::getContent($locale, 'time', 'short'), $locale); + break; + + case self::DATETIME : + return $this->_toToken(Zend_Locale_Format::getDateTimeFormat($locale), $locale); + break; + + case self::DATETIME_FULL : + return $this->_toToken(Zend_Locale_Data::getContent($locale, 'datetime', array('gregorian', 'full')), $locale); + break; + + case self::DATETIME_LONG : + return $this->_toToken(Zend_Locale_Data::getContent($locale, 'datetime', array('gregorian', 'long')), $locale); + break; + + case self::DATETIME_MEDIUM : + return $this->_toToken(Zend_Locale_Data::getContent($locale, 'datetime', array('gregorian', 'medium')), $locale); + break; + + case self::DATETIME_SHORT : + return $this->_toToken(Zend_Locale_Data::getContent($locale, 'datetime', array('gregorian', 'short')), $locale); + break; + + case self::ATOM : + return 'Y\-m\-d\TH\:i\:sP'; + break; + + case self::COOKIE : + return 'l\, d\-M\-y H\:i\:s e'; + break; + + case self::RFC_822 : + return 'D\, d M y H\:i\:s O'; + break; + + case self::RFC_850 : + return 'l\, d\-M\-y H\:i\:s e'; + break; + + case self::RFC_1036 : + return 'D\, d M y H\:i\:s O'; + break; + + case self::RFC_1123 : + return 'D\, d M Y H\:i\:s O'; + break; + + case self::RFC_3339 : + return 'Y\-m\-d\TH\:i\:sP'; + break; + + case self::RSS : + return 'D\, d M Y H\:i\:s O'; + break; + + case self::W3C : + return 'Y\-m\-d\TH\:i\:sP'; + break; + } + + if ($token == '') { + return ''; + } + + switch ($token[0]) { + case 'y' : + if ((strlen($token) == 4) && (abs($this->getUnixTimestamp()) <= 0x7FFFFFFF)) { + return 'Y'; + } + + $length = iconv_strlen($token, 'UTF-8'); + return $this->_toComment(str_pad($this->date('Y', $this->getUnixTimestamp(), false), $length, '0', STR_PAD_LEFT)); + break; + + case 'Y' : + if ((strlen($token) == 4) && (abs($this->getUnixTimestamp()) <= 0x7FFFFFFF)) { + return 'o'; + } + + $length = iconv_strlen($token, 'UTF-8'); + return $this->_toComment(str_pad($this->date('o', $this->getUnixTimestamp(), false), $length, '0', STR_PAD_LEFT)); + break; + + case 'A' : + $length = iconv_strlen($token, 'UTF-8'); + $result = substr($this->getMilliSecond(), 0, 3); + $result += $this->date('s', $this->getUnixTimestamp(), false) * 1000; + $result += $this->date('i', $this->getUnixTimestamp(), false) * 60000; + $result += $this->date('H', $this->getUnixTimestamp(), false) * 3600000; + + return $this->_toComment(str_pad($result, $length, '0', STR_PAD_LEFT)); + break; + } + + return $this->_toComment($token); + } + + /** + * Private function to make a comment of a token + * + * @param string $token + * @return string + */ + private function _toComment($token) + { + $token = str_split($token); + $result = ''; + foreach ($token as $tok) { + $result .= '\\' . $tok; + } + + return $result; + } + + /** + * Return digit from standard names (english) + * Faster implementation than locale aware searching + * + * @param string $name + * @return integer Number of this month + * @throws Zend_Date_Exception + */ + private function _getDigitFromName($name) + { + switch($name) { + case "Jan": + return 1; + + case "Feb": + return 2; + + case "Mar": + return 3; + + case "Apr": + return 4; + + case "May": + return 5; + + case "Jun": + return 6; + + case "Jul": + return 7; + + case "Aug": + return 8; + + case "Sep": + return 9; + + case "Oct": + return 10; + + case "Nov": + return 11; + + case "Dec": + return 12; + + default: + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception('Month ($name) is not a known month'); + } + } + + /** + * Counts the exact year number + * < 70 - 2000 added, >70 < 100 - 1900, others just returned + * + * @param integer $value year number + * @return integer Number of year + */ + public static function getFullYear($value) + { + if ($value >= 0) { + if ($value < 70) { + $value += 2000; + } else if ($value < 100) { + $value += 1900; + } + } + return $value; + } + + /** + * Sets the given date as new date or a given datepart as new datepart returning the new datepart + * This could be for example a localized dayname, the date without time, + * the month or only the seconds. There are about 50 different supported date parts. + * For a complete list of supported datepart values look into the docu + * + * @param string|integer|array|Zend_Date $date Date or datepart to set + * @param string $part OPTIONAL Part of the date to set, if null the timestamp is set + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function set($date, $part = null, $locale = null) + { + if (self::$_options['format_type'] == 'php') { + $part = Zend_Locale_Format::convertPhpToIsoFormat($part); + } + + $zone = $this->getTimezoneFromString($date); + $this->setTimezone($zone); + + $this->_calculate('set', $date, $part, $locale); + return $this; + } + + /** + * Adds a date or datepart to the existing date, by extracting $part from $date, + * and modifying this object by adding that part. The $part is then extracted from + * this object and returned as an integer or numeric string (for large values, or $part's + * corresponding to pre-defined formatted date strings). + * This could be for example a ISO 8601 date, the hour the monthname or only the minute. + * There are about 50 different supported date parts. + * For a complete list of supported datepart values look into the docu. + * + * @param string|integer|array|Zend_Date $date Date or datepart to add + * @param string $part OPTIONAL Part of the date to add, if null the timestamp is added + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function add($date, $part = self::TIMESTAMP, $locale = null) + { + if (self::$_options['format_type'] == 'php') { + $part = Zend_Locale_Format::convertPhpToIsoFormat($part); + } + + $this->_calculate('add', $date, $part, $locale); + return $this; + } + + /** + * Subtracts a date from another date. + * This could be for example a RFC2822 date, the time, + * the year or only the timestamp. There are about 50 different supported date parts. + * For a complete list of supported datepart values look into the docu + * Be aware: Adding -2 Months is not equal to Subtracting 2 Months !!! + * + * @param string|integer|array|Zend_Date $date Date or datepart to subtract + * @param string $part OPTIONAL Part of the date to sub, if null the timestamp is subtracted + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function sub($date, $part = self::TIMESTAMP, $locale = null) + { + if (self::$_options['format_type'] == 'php') { + $part = Zend_Locale_Format::convertPhpToIsoFormat($part); + } + + $this->_calculate('sub', $date, $part, $locale); + return $this; + } + + /** + * Compares a date or datepart with the existing one. + * Returns -1 if earlier, 0 if equal and 1 if later. + * + * @param string|integer|array|Zend_Date $date Date or datepart to compare with the date object + * @param string $part OPTIONAL Part of the date to compare, if null the timestamp is subtracted + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return integer 0 = equal, 1 = later, -1 = earlier + * @throws Zend_Date_Exception + */ + public function compare($date, $part = self::TIMESTAMP, $locale = null) + { + if (self::$_options['format_type'] == 'php') { + $part = Zend_Locale_Format::convertPhpToIsoFormat($part); + } + + $compare = $this->_calculate('cmp', $date, $part, $locale); + + if ($compare > 0) { + return 1; + } else if ($compare < 0) { + return -1; + } + return 0; + } + + /** + * Returns a new instance of Zend_Date with the selected part copied. + * To make an exact copy, use PHP's clone keyword. + * For a complete list of supported date part values look into the docu. + * If a date part is copied, all other date parts are set to standard values. + * For example: If only YEAR is copied, the returned date object is equal to + * 01-01-YEAR 00:00:00 (01-01-1970 00:00:00 is equal to timestamp 0) + * If only HOUR is copied, the returned date object is equal to + * 01-01-1970 HOUR:00:00 (so $this contains a timestamp equal to a timestamp of 0 plus HOUR). + * + * @param string $part Part of the date to compare, if null the timestamp is subtracted + * @param string|Zend_Locale $locale OPTIONAL New object's locale. No adjustments to timezone are made. + * @return Zend_Date New clone with requested part + */ + public function copyPart($part, $locale = null) + { + $clone = clone $this; // copy all instance variables + $clone->setUnixTimestamp(0); // except the timestamp + if ($locale != null) { + $clone->setLocale($locale); // set an other locale if selected + } + $clone->set($this, $part); + return $clone; + } + + /** + * Internal function, returns the offset of a given timezone + * + * @param string $zone + * @return integer + */ + public function getTimezoneFromString($zone) + { + if (is_array($zone)) { + return $this->getTimezone(); + } + + if ($zone instanceof Zend_Date) { + return $zone->getTimezone(); + } + + $match = array(); + preg_match('/\dZ$/', $zone, $match); + if (!empty($match)) { + return "Etc/UTC"; + } + + preg_match('/([+-]\d{2}):{0,1}\d{2}/', $zone, $match); + if (!empty($match) and ($match[count($match) - 1] <= 12) and ($match[count($match) - 1] >= -12)) { + $zone = "Etc/GMT"; + $zone .= ($match[count($match) - 1] < 0) ? "+" : "-"; + $zone .= (int) abs($match[count($match) - 1]); + return $zone; + } + + preg_match('/([[:alpha:]\/]{3,30})(?!.*([[:alpha:]\/]{3,30}))/', $zone, $match); + try { + if (!empty($match) and (!is_int($match[count($match) - 1]))) { + $oldzone = $this->getTimezone(); + $this->setTimezone($match[count($match) - 1]); + $result = $this->getTimezone(); + $this->setTimezone($oldzone); + if ($result !== $oldzone) { + return $match[count($match) - 1]; + } + } + } catch (Exception $e) { + // fall through + } + + return $this->getTimezone(); + } + + /** + * Calculates the date or object + * + * @param string $calc Calculation to make + * @param string|integer $date Date for calculation + * @param string|integer $comp Second date for calculation + * @param boolean|integer $dst Use dst correction if option is set + * @return integer|string|Zend_Date new timestamp or Zend_Date depending on calculation + */ + private function _assign($calc, $date, $comp = 0, $dst = false) + { + switch ($calc) { + case 'set' : + if (!empty($comp)) { + $this->setUnixTimestamp(call_user_func(Zend_Locale_Math::$sub, $this->getUnixTimestamp(), $comp)); + } + $this->setUnixTimestamp(call_user_func(Zend_Locale_Math::$add, $this->getUnixTimestamp(), $date)); + $value = $this->getUnixTimestamp(); + break; + case 'add' : + $this->setUnixTimestamp(call_user_func(Zend_Locale_Math::$add, $this->getUnixTimestamp(), $date)); + $value = $this->getUnixTimestamp(); + break; + case 'sub' : + $this->setUnixTimestamp(call_user_func(Zend_Locale_Math::$sub, $this->getUnixTimestamp(), $date)); + $value = $this->getUnixTimestamp(); + break; + default : + // cmp - compare + return call_user_func(Zend_Locale_Math::$comp, $comp, $date); + break; + } + + // dst-correction if 'fix_dst' = true and dst !== false but only for non UTC and non GMT + if ((self::$_options['fix_dst'] === true) and ($dst !== false) and ($this->_dst === true)) { + $hour = $this->toString(self::HOUR, 'iso'); + if ($hour != $dst) { + if (($dst == ($hour + 1)) or ($dst == ($hour - 23))) { + $value += 3600; + } else if (($dst == ($hour - 1)) or ($dst == ($hour + 23))) { + $value -= 3600; + } + $this->setUnixTimestamp($value); + } + } + return $this->getUnixTimestamp(); + } + + + /** + * Calculates the date or object + * + * @param string $calc Calculation to make, one of: 'add'|'sub'|'cmp'|'copy'|'set' + * @param string|integer|array|Zend_Date $date Date or datepart to calculate with + * @param string $part Part of the date to calculate, if null the timestamp is used + * @param string|Zend_Locale $locale Locale for parsing input + * @return integer|string|Zend_Date new timestamp + * @throws Zend_Date_Exception + */ + private function _calculate($calc, $date, $part, $locale) + { + if ($date === null) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception('parameter $date must be set, null is not allowed'); + } + + if (($part !== null) && (strlen($part) !== 2) && (Zend_Locale::isLocale($part, null, false))) { + $locale = $part; + $part = null; + } + + if ($locale === null) { + $locale = $this->getLocale(); + } + + $locale = (string) $locale; + + // Create date parts + $year = $this->toString(self::YEAR, 'iso'); + $month = $this->toString(self::MONTH_SHORT, 'iso'); + $day = $this->toString(self::DAY_SHORT, 'iso'); + $hour = $this->toString(self::HOUR_SHORT, 'iso'); + $minute = $this->toString(self::MINUTE_SHORT, 'iso'); + $second = $this->toString(self::SECOND_SHORT, 'iso'); + // If object extract value + if ($date instanceof Zend_Date) { + $date = $date->toString($part, 'iso', $locale); + } + + if (is_array($date) === true) { + if (empty($part) === false) { + switch($part) { + // Fall through + case self::DAY: + case self::DAY_SHORT: + if (isset($date['day']) === true) { + $date = $date['day']; + } + break; + // Fall through + case self::WEEKDAY_SHORT: + case self::WEEKDAY: + case self::WEEKDAY_8601: + case self::WEEKDAY_DIGIT: + case self::WEEKDAY_NARROW: + case self::WEEKDAY_NAME: + if (isset($date['weekday']) === true) { + $date = $date['weekday']; + $part = self::WEEKDAY_DIGIT; + } + break; + case self::DAY_OF_YEAR: + if (isset($date['day_of_year']) === true) { + $date = $date['day_of_year']; + } + break; + // Fall through + case self::MONTH: + case self::MONTH_SHORT: + case self::MONTH_NAME: + case self::MONTH_NAME_SHORT: + case self::MONTH_NAME_NARROW: + if (isset($date['month']) === true) { + $date = $date['month']; + } + break; + // Fall through + case self::YEAR: + case self::YEAR_SHORT: + case self::YEAR_8601: + case self::YEAR_SHORT_8601: + if (isset($date['year']) === true) { + $date = $date['year']; + } + break; + // Fall through + case self::HOUR: + case self::HOUR_AM: + case self::HOUR_SHORT: + case self::HOUR_SHORT_AM: + if (isset($date['hour']) === true) { + $date = $date['hour']; + } + break; + // Fall through + case self::MINUTE: + case self::MINUTE_SHORT: + if (isset($date['minute']) === true) { + $date = $date['minute']; + } + break; + // Fall through + case self::SECOND: + case self::SECOND_SHORT: + if (isset($date['second']) === true) { + $date = $date['second']; + } + break; + // Fall through + case self::TIMEZONE: + case self::TIMEZONE_NAME: + if (isset($date['timezone']) === true) { + $date = $date['timezone']; + } + break; + case self::TIMESTAMP: + if (isset($date['timestamp']) === true) { + $date = $date['timestamp']; + } + break; + case self::WEEK: + if (isset($date['week']) === true) { + $date = $date['week']; + } + break; + case self::TIMEZONE_SECS: + if (isset($date['gmtsecs']) === true) { + $date = $date['gmtsecs']; + } + break; + default: + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("datepart for part ($part) not found in array"); + break; + } + } else { + $hours = 0; + if (isset($date['hour']) === true) { + $hours = $date['hour']; + } + $minutes = 0; + if (isset($date['minute']) === true) { + $minutes = $date['minute']; + } + $seconds = 0; + if (isset($date['second']) === true) { + $seconds = $date['second']; + } + $months = 0; + if (isset($date['month']) === true) { + $months = $date['month']; + } + $days = 0; + if (isset($date['day']) === true) { + $days = $date['day']; + } + $years = 0; + if (isset($date['year']) === true) { + $years = $date['year']; + } + return $this->_assign($calc, $this->mktime($hours, $minutes, $seconds, $months, $days, $years, true), + $this->mktime($hour, $minute, $second, $month, $day, $year, true), $hour); + } + } + + // $date as object, part of foreign date as own date + switch($part) { + + // day formats + case self::DAY: + if (is_numeric($date)) { + return $this->_assign($calc, $this->mktime(0, 0, 0, 1, 1 + intval($date), 1970, true), + $this->mktime(0, 0, 0, 1, 1 + intval($day), 1970, true), $hour); + } + + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, day expected", 0, null, $date); + break; + + case self::WEEKDAY_SHORT: + $daylist = Zend_Locale_Data::getList($locale, 'day'); + $weekday = (int) $this->toString(self::WEEKDAY_DIGIT, 'iso', $locale); + $cnt = 0; + + foreach ($daylist as $key => $value) { + if (strtoupper(iconv_substr($value, 0, 3, 'UTF-8')) == strtoupper($date)) { + $found = $cnt; + break; + } + ++$cnt; + } + + // Weekday found + if ($cnt < 7) { + return $this->_assign($calc, $this->mktime(0, 0, 0, 1, 1 + $found, 1970, true), + $this->mktime(0, 0, 0, 1, 1 + $weekday, 1970, true), $hour); + } + + // Weekday not found + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, weekday expected", 0, null, $date); + break; + + case self::DAY_SHORT: + if (is_numeric($date)) { + return $this->_assign($calc, $this->mktime(0, 0, 0, 1, 1 + intval($date), 1970, true), + $this->mktime(0, 0, 0, 1, 1 + intval($day), 1970, true), $hour); + } + + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, day expected", 0, null, $date); + break; + + case self::WEEKDAY: + $daylist = Zend_Locale_Data::getList($locale, 'day'); + $weekday = (int) $this->toString(self::WEEKDAY_DIGIT, 'iso', $locale); + $cnt = 0; + + foreach ($daylist as $key => $value) { + if (strtoupper($value) == strtoupper($date)) { + $found = $cnt; + break; + } + ++$cnt; + } + + // Weekday found + if ($cnt < 7) { + return $this->_assign($calc, $this->mktime(0, 0, 0, 1, 1 + $found, 1970, true), + $this->mktime(0, 0, 0, 1, 1 + $weekday, 1970, true), $hour); + } + + // Weekday not found + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, weekday expected", 0, null, $date); + break; + + case self::WEEKDAY_8601: + $weekday = (int) $this->toString(self::WEEKDAY_8601, 'iso', $locale); + if ((intval($date) > 0) and (intval($date) < 8)) { + return $this->_assign($calc, $this->mktime(0, 0, 0, 1, 1 + intval($date), 1970, true), + $this->mktime(0, 0, 0, 1, 1 + $weekday, 1970, true), $hour); + } + + // Weekday not found + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, weekday expected", 0, null, $date); + break; + + case self::DAY_SUFFIX: + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception('day suffix not supported', 0, null, $date); + break; + + case self::WEEKDAY_DIGIT: + $weekday = (int) $this->toString(self::WEEKDAY_DIGIT, 'iso', $locale); + if (is_numeric($date) and (intval($date) >= 0) and (intval($date) < 7)) { + return $this->_assign($calc, $this->mktime(0, 0, 0, 1, 1 + $date, 1970, true), + $this->mktime(0, 0, 0, 1, 1 + $weekday, 1970, true), $hour); + } + + // Weekday not found + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, weekday expected", 0, null, $date); + break; + + case self::DAY_OF_YEAR: + if (is_numeric($date)) { + if (($calc == 'add') || ($calc == 'sub')) { + $year = 1970; + ++$date; + ++$day; + } + + return $this->_assign($calc, $this->mktime(0, 0, 0, 1, $date, $year, true), + $this->mktime(0, 0, 0, $month, $day, $year, true), $hour); + } + + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, day expected", 0, null, $date); + break; + + case self::WEEKDAY_NARROW: + $daylist = Zend_Locale_Data::getList($locale, 'day', array('gregorian', 'format', 'abbreviated')); + $weekday = (int) $this->toString(self::WEEKDAY_DIGIT, 'iso', $locale); + $cnt = 0; + foreach ($daylist as $key => $value) { + if (strtoupper(iconv_substr($value, 0, 1, 'UTF-8')) == strtoupper($date)) { + $found = $cnt; + break; + } + ++$cnt; + } + + // Weekday found + if ($cnt < 7) { + return $this->_assign($calc, $this->mktime(0, 0, 0, 1, 1 + $found, 1970, true), + $this->mktime(0, 0, 0, 1, 1 + $weekday, 1970, true), $hour); + } + + // Weekday not found + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, weekday expected", 0, null, $date); + break; + + case self::WEEKDAY_NAME: + $daylist = Zend_Locale_Data::getList($locale, 'day', array('gregorian', 'format', 'abbreviated')); + $weekday = (int) $this->toString(self::WEEKDAY_DIGIT, 'iso', $locale); + $cnt = 0; + foreach ($daylist as $key => $value) { + if (strtoupper($value) == strtoupper($date)) { + $found = $cnt; + break; + } + ++$cnt; + } + + // Weekday found + if ($cnt < 7) { + return $this->_assign($calc, $this->mktime(0, 0, 0, 1, 1 + $found, 1970, true), + $this->mktime(0, 0, 0, 1, 1 + $weekday, 1970, true), $hour); + } + + // Weekday not found + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, weekday expected", 0, null, $date); + break; + + // week formats + case self::WEEK: + if (is_numeric($date)) { + $week = (int) $this->toString(self::WEEK, 'iso', $locale); + return $this->_assign($calc, parent::mktime(0, 0, 0, 1, 1 + ($date * 7), 1970, true), + parent::mktime(0, 0, 0, 1, 1 + ($week * 7), 1970, true), $hour); + } + + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, week expected", 0, null, $date); + break; + + // month formats + case self::MONTH_NAME: + $monthlist = Zend_Locale_Data::getList($locale, 'month'); + $cnt = 0; + foreach ($monthlist as $key => $value) { + if (strtoupper($value) == strtoupper($date)) { + $found = $key; + break; + } + ++$cnt; + } + $date = array_search($date, $monthlist); + + // Monthname found + if ($cnt < 12) { + $fixday = 0; + if ($calc == 'add') { + $date += $found; + $calc = 'set'; + if (self::$_options['extend_month'] == false) { + $parts = $this->getDateParts($this->mktime($hour, $minute, $second, $date, $day, $year, false)); + if ($parts['mday'] != $day) { + $fixday = ($parts['mday'] < $day) ? -$parts['mday'] : ($parts['mday'] - $day); + } + } + } else if ($calc == 'sub') { + $date = $month - $found; + $calc = 'set'; + if (self::$_options['extend_month'] == false) { + $parts = $this->getDateParts($this->mktime($hour, $minute, $second, $date, $day, $year, false)); + if ($parts['mday'] != $day) { + $fixday = ($parts['mday'] < $day) ? -$parts['mday'] : ($parts['mday'] - $day); + } + } + } + return $this->_assign($calc, $this->mktime(0, 0, 0, $date, $day + $fixday, $year, true), + $this->mktime(0, 0, 0, $month, $day, $year, true), $hour); + } + + // Monthname not found + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, month expected", 0, null, $date); + break; + + case self::MONTH: + if (is_numeric($date)) { + $fixday = 0; + if ($calc == 'add') { + $date += $month; + $calc = 'set'; + if (self::$_options['extend_month'] == false) { + $parts = $this->getDateParts($this->mktime($hour, $minute, $second, $date, $day, $year, false)); + if ($parts['mday'] != $day) { + $fixday = ($parts['mday'] < $day) ? -$parts['mday'] : ($parts['mday'] - $day); + } + } + } else if ($calc == 'sub') { + $date = $month - $date; + $calc = 'set'; + if (self::$_options['extend_month'] == false) { + $parts = $this->getDateParts($this->mktime($hour, $minute, $second, $date, $day, $year, false)); + if ($parts['mday'] != $day) { + $fixday = ($parts['mday'] < $day) ? -$parts['mday'] : ($parts['mday'] - $day); + } + } + } + return $this->_assign($calc, $this->mktime(0, 0, 0, $date, $day + $fixday, $year, true), + $this->mktime(0, 0, 0, $month, $day, $year, true), $hour); + } + + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, month expected", 0, null, $date); + break; + + case self::MONTH_NAME_SHORT: + $monthlist = Zend_Locale_Data::getList($locale, 'month', array('gregorian', 'format', 'abbreviated')); + $cnt = 0; + foreach ($monthlist as $key => $value) { + if (strtoupper($value) == strtoupper($date)) { + $found = $key; + break; + } + ++$cnt; + } + $date = array_search($date, $monthlist); + + // Monthname found + if ($cnt < 12) { + $fixday = 0; + if ($calc == 'add') { + $date += $found; + $calc = 'set'; + if (self::$_options['extend_month'] === false) { + $parts = $this->getDateParts($this->mktime($hour, $minute, $second, $date, $day, $year, false)); + if ($parts['mday'] != $day) { + $fixday = ($parts['mday'] < $day) ? -$parts['mday'] : ($parts['mday'] - $day); + } + } + } else if ($calc == 'sub') { + $date = $month - $found; + $calc = 'set'; + if (self::$_options['extend_month'] === false) { + $parts = $this->getDateParts($this->mktime($hour, $minute, $second, $date, $day, $year, false)); + if ($parts['mday'] != $day) { + $fixday = ($parts['mday'] < $day) ? -$parts['mday'] : ($parts['mday'] - $day); + } + } + } + return $this->_assign($calc, $this->mktime(0, 0, 0, $date, $day + $fixday, $year, true), + $this->mktime(0, 0, 0, $month, $day, $year, true), $hour); + } + + // Monthname not found + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, month expected", 0, null, $date); + break; + + case self::MONTH_SHORT: + if (is_numeric($date) === true) { + $fixday = 0; + if ($calc === 'add') { + $date += $month; + $calc = 'set'; + if (self::$_options['extend_month'] === false) { + $parts = $this->getDateParts($this->mktime($hour, $minute, $second, $date, $day, $year, false)); + if ($parts['mday'] != $day) { + $fixday = ($parts['mday'] < $day) ? -$parts['mday'] : ($parts['mday'] - $day); + } + } + } else if ($calc === 'sub') { + $date = $month - $date; + $calc = 'set'; + if (self::$_options['extend_month'] === false) { + $parts = $this->getDateParts($this->mktime($hour, $minute, $second, $date, $day, $year, false)); + if ($parts['mday'] != $day) { + $fixday = ($parts['mday'] < $day) ? -$parts['mday'] : ($parts['mday'] - $day); + } + } + } + + return $this->_assign($calc, $this->mktime(0, 0, 0, $date, $day + $fixday, $year, true), + $this->mktime(0, 0, 0, $month, $day, $year, true), $hour); + } + + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, month expected", 0, null, $date); + break; + + case self::MONTH_DAYS: + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception('month days not supported', 0, null, $date); + break; + + case self::MONTH_NAME_NARROW: + $monthlist = Zend_Locale_Data::getList($locale, 'month', array('gregorian', 'stand-alone', 'narrow')); + $cnt = 0; + foreach ($monthlist as $key => $value) { + if (strtoupper($value) === strtoupper($date)) { + $found = $key; + break; + } + ++$cnt; + } + $date = array_search($date, $monthlist); + + // Monthname found + if ($cnt < 12) { + $fixday = 0; + if ($calc === 'add') { + $date += $found; + $calc = 'set'; + if (self::$_options['extend_month'] === false) { + $parts = $this->getDateParts($this->mktime($hour, $minute, $second, $date, $day, $year, false)); + if ($parts['mday'] != $day) { + $fixday = ($parts['mday'] < $day) ? -$parts['mday'] : ($parts['mday'] - $day); + } + } + } else if ($calc === 'sub') { + $date = $month - $found; + $calc = 'set'; + if (self::$_options['extend_month'] === false) { + $parts = $this->getDateParts($this->mktime($hour, $minute, $second, $date, $day, $year, false)); + if ($parts['mday'] != $day) { + $fixday = ($parts['mday'] < $day) ? -$parts['mday'] : ($parts['mday'] - $day); + } + } + } + return $this->_assign($calc, $this->mktime(0, 0, 0, $date, $day + $fixday, $year, true), + $this->mktime(0, 0, 0, $month, $day, $year, true), $hour); + } + + // Monthname not found + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, month expected", 0, null, $date); + break; + + // year formats + case self::LEAPYEAR: + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception('leap year not supported', 0, null, $date); + break; + + case self::YEAR_8601: + if (is_numeric($date)) { + if ($calc === 'add') { + $date += $year; + $calc = 'set'; + } else if ($calc === 'sub') { + $date = $year - $date; + $calc = 'set'; + } + + return $this->_assign($calc, $this->mktime(0, 0, 0, $month, $day, intval($date), true), + $this->mktime(0, 0, 0, $month, $day, $year, true), false); + } + + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, year expected", 0, null, $date); + break; + + case self::YEAR: + if (is_numeric($date)) { + if ($calc === 'add') { + $date += $year; + $calc = 'set'; + } else if ($calc === 'sub') { + $date = $year - $date; + $calc = 'set'; + } + + return $this->_assign($calc, $this->mktime(0, 0, 0, $month, $day, intval($date), true), + $this->mktime(0, 0, 0, $month, $day, $year, true), false); + } + + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, year expected", 0, null, $date); + break; + + case self::YEAR_SHORT: + if (is_numeric($date)) { + $date = intval($date); + if (($calc == 'set') || ($calc == 'cmp')) { + $date = self::getFullYear($date); + } + if ($calc === 'add') { + $date += $year; + $calc = 'set'; + } else if ($calc === 'sub') { + $date = $year - $date; + $calc = 'set'; + } + + return $this->_assign($calc, $this->mktime(0, 0, 0, $month, $day, $date, true), + $this->mktime(0, 0, 0, $month, $day, $year, true), false); + } + + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, year expected", 0, null, $date); + break; + + case self::YEAR_SHORT_8601: + if (is_numeric($date)) { + $date = intval($date); + if (($calc === 'set') || ($calc === 'cmp')) { + $date = self::getFullYear($date); + } + if ($calc === 'add') { + $date += $year; + $calc = 'set'; + } else if ($calc === 'sub') { + $date = $year - $date; + $calc = 'set'; + } + + return $this->_assign($calc, $this->mktime(0, 0, 0, $month, $day, $date, true), + $this->mktime(0, 0, 0, $month, $day, $year, true), false); + } + + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, year expected", 0, null, $date); + break; + + // time formats + case self::MERIDIEM: + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception('meridiem not supported', 0, null, $date); + break; + + case self::SWATCH: + if (is_numeric($date)) { + $rest = intval($date); + $hours = floor($rest * 24 / 1000); + $rest = $rest - ($hours * 1000 / 24); + $minutes = floor($rest * 1440 / 1000); + $rest = $rest - ($minutes * 1000 / 1440); + $seconds = floor($rest * 86400 / 1000); + return $this->_assign($calc, $this->mktime($hours, $minutes, $seconds, 1, 1, 1970, true), + $this->mktime($hour, $minute, $second, 1, 1, 1970, true), false); + } + + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, swatchstamp expected", 0, null, $date); + break; + + case self::HOUR_SHORT_AM: + if (is_numeric($date)) { + return $this->_assign($calc, $this->mktime(intval($date), 0, 0, 1, 1, 1970, true), + $this->mktime($hour, 0, 0, 1, 1, 1970, true), false); + } + + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, hour expected", 0, null, $date); + break; + + case self::HOUR_SHORT: + if (is_numeric($date)) { + return $this->_assign($calc, $this->mktime(intval($date), 0, 0, 1, 1, 1970, true), + $this->mktime($hour, 0, 0, 1, 1, 1970, true), false); + } + + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, hour expected", 0, null, $date); + break; + + case self::HOUR_AM: + if (is_numeric($date)) { + return $this->_assign($calc, $this->mktime(intval($date), 0, 0, 1, 1, 1970, true), + $this->mktime($hour, 0, 0, 1, 1, 1970, true), false); + } + + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, hour expected", 0, null, $date); + break; + + case self::HOUR: + if (is_numeric($date)) { + return $this->_assign($calc, $this->mktime(intval($date), 0, 0, 1, 1, 1970, true), + $this->mktime($hour, 0, 0, 1, 1, 1970, true), false); + } + + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, hour expected", 0, null, $date); + break; + + case self::MINUTE: + if (is_numeric($date)) { + return $this->_assign($calc, $this->mktime(0, intval($date), 0, 1, 1, 1970, true), + $this->mktime(0, $minute, 0, 1, 1, 1970, true), false); + } + + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, minute expected", 0, null, $date); + break; + + case self::SECOND: + if (is_numeric($date)) { + return $this->_assign($calc, $this->mktime(0, 0, intval($date), 1, 1, 1970, true), + $this->mktime(0, 0, $second, 1, 1, 1970, true), false); + } + + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, second expected", 0, null, $date); + break; + + case self::MILLISECOND: + if (is_numeric($date)) { + switch($calc) { + case 'set' : + return $this->setMillisecond($date); + break; + case 'add' : + return $this->addMillisecond($date); + break; + case 'sub' : + return $this->subMillisecond($date); + break; + } + + return $this->compareMillisecond($date); + } + + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, milliseconds expected", 0, null, $date); + break; + + case self::MINUTE_SHORT: + if (is_numeric($date)) { + return $this->_assign($calc, $this->mktime(0, intval($date), 0, 1, 1, 1970, true), + $this->mktime(0, $minute, 0, 1, 1, 1970, true), false); + } + + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, minute expected", 0, null, $date); + break; + + case self::SECOND_SHORT: + if (is_numeric($date)) { + return $this->_assign($calc, $this->mktime(0, 0, intval($date), 1, 1, 1970, true), + $this->mktime(0, 0, $second, 1, 1, 1970, true), false); + } + + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, second expected", 0, null, $date); + break; + + // timezone formats + // break intentionally omitted + case self::TIMEZONE_NAME: + case self::TIMEZONE: + case self::TIMEZONE_SECS: + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception('timezone not supported', 0, null, $date); + break; + + case self::DAYLIGHT: + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception('daylight not supported', 0, null, $date); + break; + + case self::GMT_DIFF: + case self::GMT_DIFF_SEP: + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception('gmtdiff not supported', 0, null, $date); + break; + + // date strings + case self::ISO_8601: + // (-)YYYY-MM-dd + preg_match('/^(-{0,1}\d{4})-(\d{2})-(\d{2})/', $date, $datematch); + // (-)YY-MM-dd + if (empty($datematch)) { + preg_match('/^(-{0,1}\d{2})-(\d{2})-(\d{2})/', $date, $datematch); + } + // (-)YYYYMMdd + if (empty($datematch)) { + preg_match('/^(-{0,1}\d{4})(\d{2})(\d{2})/', $date, $datematch); + } + // (-)YYMMdd + if (empty($datematch)) { + preg_match('/^(-{0,1}\d{2})(\d{2})(\d{2})/', $date, $datematch); + } + $tmpdate = $date; + if (!empty($datematch)) { + $dateMatchCharCount = iconv_strlen($datematch[0], 'UTF-8'); + $tmpdate = iconv_substr($date, + $dateMatchCharCount, + iconv_strlen($date, 'UTF-8') - $dateMatchCharCount, + 'UTF-8'); + } + // (T)hh:mm:ss + preg_match('/[T,\s]{0,1}(\d{2}):(\d{2}):(\d{2})/', $tmpdate, $timematch); + if (empty($timematch)) { + preg_match('/[T,\s]{0,1}(\d{2})(\d{2})(\d{2})/', $tmpdate, $timematch); + } + if (empty($datematch) and empty($timematch)) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("unsupported ISO8601 format ($date)", 0, null, $date); + } + if (!empty($timematch)) { + $timeMatchCharCount = iconv_strlen($timematch[0], 'UTF-8'); + $tmpdate = iconv_substr($tmpdate, + $timeMatchCharCount, + iconv_strlen($tmpdate, 'UTF-8') - $timeMatchCharCount, + 'UTF-8'); + } + if (empty($datematch)) { + $datematch[1] = 1970; + $datematch[2] = 1; + $datematch[3] = 1; + } else if (iconv_strlen($datematch[1], 'UTF-8') == 2) { + $datematch[1] = self::getFullYear($datematch[1]); + } + if (empty($timematch)) { + $timematch[1] = 0; + $timematch[2] = 0; + $timematch[3] = 0; + } + + if (($calc == 'set') || ($calc == 'cmp')) { + --$datematch[2]; + --$month; + --$datematch[3]; + --$day; + $datematch[1] -= 1970; + $year -= 1970; + } + return $this->_assign($calc, $this->mktime($timematch[1], $timematch[2], $timematch[3], 1 + $datematch[2], 1 + $datematch[3], 1970 + $datematch[1], false), + $this->mktime($hour, $minute, $second, 1 + $month, 1 + $day, 1970 + $year, false), false); + break; + + case self::RFC_2822: + $result = preg_match('/^\w{3},\s(\d{1,2})\s(\w{3})\s(\d{4})\s(\d{2}):(\d{2}):{0,1}(\d{0,2})\s([+-]{1}\d{4})$/', $date, $match); + if (!$result) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("no RFC 2822 format ($date)", 0, null, $date); + } + + $months = $this->_getDigitFromName($match[2]); + + if (($calc == 'set') || ($calc == 'cmp')) { + --$months; + --$month; + --$match[1]; + --$day; + $match[3] -= 1970; + $year -= 1970; + } + return $this->_assign($calc, $this->mktime($match[4], $match[5], $match[6], 1 + $months, 1 + $match[1], 1970 + $match[3], false), + $this->mktime($hour, $minute, $second, 1 + $month, 1 + $day, 1970 + $year, false), false); + break; + + case self::TIMESTAMP: + if (is_numeric($date)) { + return $this->_assign($calc, $date, $this->getUnixTimestamp()); + } + + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, timestamp expected", 0, null, $date); + break; + + // additional formats + // break intentionally omitted + case self::ERA: + case self::ERA_NAME: + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception('era not supported', 0, null, $date); + break; + + case self::DATES: + try { + $parsed = Zend_Locale_Format::getDate($date, array('locale' => $locale, 'format_type' => 'iso', 'fix_date' => true)); + + if (($calc == 'set') || ($calc == 'cmp')) { + --$parsed['month']; + --$month; + --$parsed['day']; + --$day; + $parsed['year'] -= 1970; + $year -= 1970; + } + + return $this->_assign($calc, $this->mktime(0, 0, 0, 1 + $parsed['month'], 1 + $parsed['day'], 1970 + $parsed['year'], true), + $this->mktime(0, 0, 0, 1 + $month, 1 + $day, 1970 + $year, true), $hour); + } catch (Zend_Locale_Exception $e) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception($e->getMessage(), 0, $e, $date); + } + break; + + case self::DATE_FULL: + try { + $format = Zend_Locale_Data::getContent($locale, 'date', array('gregorian', 'full')); + $parsed = Zend_Locale_Format::getDate($date, array('date_format' => $format, 'format_type' => 'iso', 'locale' => $locale)); + + if (($calc == 'set') || ($calc == 'cmp')) { + --$parsed['month']; + --$month; + --$parsed['day']; + --$day; + $parsed['year'] -= 1970; + $year -= 1970; + } + return $this->_assign($calc, $this->mktime(0, 0, 0, 1 + $parsed['month'], 1 + $parsed['day'], 1970 + $parsed['year'], true), + $this->mktime(0, 0, 0, 1 + $month, 1 + $day, 1970 + $year, true), $hour); + } catch (Zend_Locale_Exception $e) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception($e->getMessage(), 0, $e, $date); + } + break; + + case self::DATE_LONG: + try { + $format = Zend_Locale_Data::getContent($locale, 'date', array('gregorian', 'long')); + $parsed = Zend_Locale_Format::getDate($date, array('date_format' => $format, 'format_type' => 'iso', 'locale' => $locale)); + + if (($calc == 'set') || ($calc == 'cmp')){ + --$parsed['month']; + --$month; + --$parsed['day']; + --$day; + $parsed['year'] -= 1970; + $year -= 1970; + } + return $this->_assign($calc, $this->mktime(0, 0, 0, 1 + $parsed['month'], 1 + $parsed['day'], 1970 + $parsed['year'], true), + $this->mktime(0, 0, 0, 1 + $month, 1 + $day, 1970 + $year, true), $hour); + } catch (Zend_Locale_Exception $e) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception($e->getMessage(), 0, $e, $date); + } + break; + + case self::DATE_MEDIUM: + try { + $format = Zend_Locale_Data::getContent($locale, 'date', array('gregorian', 'medium')); + $parsed = Zend_Locale_Format::getDate($date, array('date_format' => $format, 'format_type' => 'iso', 'locale' => $locale)); + + if (($calc == 'set') || ($calc == 'cmp')) { + --$parsed['month']; + --$month; + --$parsed['day']; + --$day; + $parsed['year'] -= 1970; + $year -= 1970; + } + return $this->_assign($calc, $this->mktime(0, 0, 0, 1 + $parsed['month'], 1 + $parsed['day'], 1970 + $parsed['year'], true), + $this->mktime(0, 0, 0, 1 + $month, 1 + $day, 1970 + $year, true), $hour); + } catch (Zend_Locale_Exception $e) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception($e->getMessage(), 0, $e, $date); + } + break; + + case self::DATE_SHORT: + try { + $format = Zend_Locale_Data::getContent($locale, 'date', array('gregorian', 'short')); + $parsed = Zend_Locale_Format::getDate($date, array('date_format' => $format, 'format_type' => 'iso', 'locale' => $locale)); + + $parsed['year'] = self::getFullYear($parsed['year']); + + if (($calc == 'set') || ($calc == 'cmp')) { + --$parsed['month']; + --$month; + --$parsed['day']; + --$day; + $parsed['year'] -= 1970; + $year -= 1970; + } + return $this->_assign($calc, $this->mktime(0, 0, 0, 1 + $parsed['month'], 1 + $parsed['day'], 1970 + $parsed['year'], true), + $this->mktime(0, 0, 0, 1 + $month, 1 + $day, 1970 + $year, true), $hour); + } catch (Zend_Locale_Exception $e) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception($e->getMessage(), 0, $e, $date); + } + break; + + case self::TIMES: + try { + if ($calc != 'set') { + $month = 1; + $day = 1; + $year = 1970; + } + $parsed = Zend_Locale_Format::getTime($date, array('locale' => $locale, 'format_type' => 'iso', 'fix_date' => true)); + return $this->_assign($calc, $this->mktime($parsed['hour'], $parsed['minute'], $parsed['second'], $month, $day, $year, true), + $this->mktime($hour, $minute, $second, $month, $day, $year, true), false); + } catch (Zend_Locale_Exception $e) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception($e->getMessage(), 0, $e, $date); + } + break; + + case self::TIME_FULL: + try { + $format = Zend_Locale_Data::getContent($locale, 'time', array('gregorian', 'full')); + $parsed = Zend_Locale_Format::getTime($date, array('date_format' => $format, 'format_type' => 'iso', 'locale' => $locale)); + if ($calc != 'set') { + $month = 1; + $day = 1; + $year = 1970; + } + + if (!isset($parsed['second'])) { + $parsed['second'] = 0; + } + + return $this->_assign($calc, $this->mktime($parsed['hour'], $parsed['minute'], $parsed['second'], $month, $day, $year, true), + $this->mktime($hour, $minute, $second, $month, $day, $year, true), false); + } catch (Zend_Locale_Exception $e) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception($e->getMessage(), 0, $e, $date); + } + break; + + case self::TIME_LONG: + try { + $format = Zend_Locale_Data::getContent($locale, 'time', array('gregorian', 'long')); + $parsed = Zend_Locale_Format::getTime($date, array('date_format' => $format, 'format_type' => 'iso', 'locale' => $locale)); + if ($calc != 'set') { + $month = 1; + $day = 1; + $year = 1970; + } + return $this->_assign($calc, $this->mktime($parsed['hour'], $parsed['minute'], $parsed['second'], $month, $day, $year, true), + $this->mktime($hour, $minute, $second, $month, $day, $year, true), false); + } catch (Zend_Locale_Exception $e) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception($e->getMessage(), 0, $e, $date); + } + break; + + case self::TIME_MEDIUM: + try { + $format = Zend_Locale_Data::getContent($locale, 'time', array('gregorian', 'medium')); + $parsed = Zend_Locale_Format::getTime($date, array('date_format' => $format, 'format_type' => 'iso', 'locale' => $locale)); + if ($calc != 'set') { + $month = 1; + $day = 1; + $year = 1970; + } + return $this->_assign($calc, $this->mktime($parsed['hour'], $parsed['minute'], $parsed['second'], $month, $day, $year, true), + $this->mktime($hour, $minute, $second, $month, $day, $year, true), false); + } catch (Zend_Locale_Exception $e) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception($e->getMessage(), 0, $e, $date); + } + break; + + case self::TIME_SHORT: + try { + $format = Zend_Locale_Data::getContent($locale, 'time', array('gregorian', 'short')); + $parsed = Zend_Locale_Format::getTime($date, array('date_format' => $format, 'format_type' => 'iso', 'locale' => $locale)); + if ($calc != 'set') { + $month = 1; + $day = 1; + $year = 1970; + } + + if (!isset($parsed['second'])) { + $parsed['second'] = 0; + } + + return $this->_assign($calc, $this->mktime($parsed['hour'], $parsed['minute'], $parsed['second'], $month, $day, $year, true), + $this->mktime($hour, $minute, $second, $month, $day, $year, true), false); + } catch (Zend_Locale_Exception $e) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception($e->getMessage(), 0, $e, $date); + } + break; + + case self::DATETIME: + try { + $parsed = Zend_Locale_Format::getDateTime($date, array('locale' => $locale, 'format_type' => 'iso', 'fix_date' => true)); + if (($calc == 'set') || ($calc == 'cmp')) { + --$parsed['month']; + --$month; + --$parsed['day']; + --$day; + $parsed['year'] -= 1970; + $year -= 1970; + } + return $this->_assign($calc, $this->mktime($parsed['hour'], $parsed['minute'], $parsed['second'], 1 + $parsed['month'], 1 + $parsed['day'], 1970 + $parsed['year'], true), + $this->mktime($hour, $minute, $second, 1 + $month, 1 + $day, 1970 + $year, true), $hour); + } catch (Zend_Locale_Exception $e) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception($e->getMessage(), 0, $e, $date); + } + break; + + case self::DATETIME_FULL: + try { + $format = Zend_Locale_Data::getContent($locale, 'datetime', array('gregorian', 'full')); + $parsed = Zend_Locale_Format::getDateTime($date, array('date_format' => $format, 'format_type' => 'iso', 'locale' => $locale)); + + if (($calc == 'set') || ($calc == 'cmp')) { + --$parsed['month']; + --$month; + --$parsed['day']; + --$day; + $parsed['year'] -= 1970; + $year -= 1970; + } + + if (!isset($parsed['second'])) { + $parsed['second'] = 0; + } + + return $this->_assign($calc, $this->mktime($parsed['hour'], $parsed['minute'], $parsed['second'], 1 + $parsed['month'], 1 + $parsed['day'], 1970 + $parsed['year'], true), + $this->mktime($hour, $minute, $second, 1 + $month, 1 + $day, 1970 + $year, true), $hour); + } catch (Zend_Locale_Exception $e) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception($e->getMessage(), 0, $e, $date); + } + break; + + case self::DATETIME_LONG: + try { + $format = Zend_Locale_Data::getContent($locale, 'datetime', array('gregorian', 'long')); + $parsed = Zend_Locale_Format::getDateTime($date, array('date_format' => $format, 'format_type' => 'iso', 'locale' => $locale)); + + if (($calc == 'set') || ($calc == 'cmp')){ + --$parsed['month']; + --$month; + --$parsed['day']; + --$day; + $parsed['year'] -= 1970; + $year -= 1970; + } + return $this->_assign($calc, $this->mktime($parsed['hour'], $parsed['minute'], $parsed['second'], 1 + $parsed['month'], 1 + $parsed['day'], 1970 + $parsed['year'], true), + $this->mktime($hour, $minute, $second, 1 + $month, 1 + $day, 1970 + $year, true), $hour); + } catch (Zend_Locale_Exception $e) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception($e->getMessage(), 0, $e, $date); + } + break; + + case self::DATETIME_MEDIUM: + try { + $format = Zend_Locale_Data::getContent($locale, 'datetime', array('gregorian', 'medium')); + $parsed = Zend_Locale_Format::getDateTime($date, array('date_format' => $format, 'format_type' => 'iso', 'locale' => $locale)); + if (($calc == 'set') || ($calc == 'cmp')) { + --$parsed['month']; + --$month; + --$parsed['day']; + --$day; + $parsed['year'] -= 1970; + $year -= 1970; + } + return $this->_assign($calc, $this->mktime($parsed['hour'], $parsed['minute'], $parsed['second'], 1 + $parsed['month'], 1 + $parsed['day'], 1970 + $parsed['year'], true), + $this->mktime($hour, $minute, $second, 1 + $month, 1 + $day, 1970 + $year, true), $hour); + } catch (Zend_Locale_Exception $e) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception($e->getMessage(), 0, $e, $date); + } + break; + + case self::DATETIME_SHORT: + try { + $format = Zend_Locale_Data::getContent($locale, 'datetime', array('gregorian', 'short')); + $parsed = Zend_Locale_Format::getDateTime($date, array('date_format' => $format, 'format_type' => 'iso', 'locale' => $locale)); + + $parsed['year'] = self::getFullYear($parsed['year']); + + if (($calc == 'set') || ($calc == 'cmp')) { + --$parsed['month']; + --$month; + --$parsed['day']; + --$day; + $parsed['year'] -= 1970; + $year -= 1970; + } + + if (!isset($parsed['second'])) { + $parsed['second'] = 0; + } + + return $this->_assign($calc, $this->mktime($parsed['hour'], $parsed['minute'], $parsed['second'], 1 + $parsed['month'], 1 + $parsed['day'], 1970 + $parsed['year'], true), + $this->mktime($hour, $minute, $second, 1 + $month, 1 + $day, 1970 + $year, true), $hour); + } catch (Zend_Locale_Exception $e) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception($e->getMessage(), 0, $e, $date); + } + break; + + // ATOM and RFC_3339 are identical + case self::ATOM: + case self::RFC_3339: + $result = preg_match('/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\d{0,4}([+-]{1}\d{2}:\d{2}|Z)$/', $date, $match); + if (!$result) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, ATOM format expected", 0, null, $date); + } + + if (($calc == 'set') || ($calc == 'cmp')) { + --$match[2]; + --$month; + --$match[3]; + --$day; + $match[1] -= 1970; + $year -= 1970; + } + return $this->_assign($calc, $this->mktime($match[4], $match[5], $match[6], 1 + $match[2], 1 + $match[3], 1970 + $match[1], true), + $this->mktime($hour, $minute, $second, 1 + $month, 1 + $day, 1970 + $year, true), false); + break; + + case self::COOKIE: + $result = preg_match("/^\w{6,9},\s(\d{2})-(\w{3})-(\d{2})\s(\d{2}):(\d{2}):(\d{2})\s.{3,20}$/", $date, $match); + if (!$result) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, COOKIE format expected", 0, null, $date); + } + $matchStartPos = iconv_strpos($match[0], ' ', 0, 'UTF-8') + 1; + $match[0] = iconv_substr($match[0], + $matchStartPos, + iconv_strlen($match[0], 'UTF-8') - $matchStartPos, + 'UTF-8'); + + $months = $this->_getDigitFromName($match[2]); + $match[3] = self::getFullYear($match[3]); + + if (($calc == 'set') || ($calc == 'cmp')) { + --$months; + --$month; + --$match[1]; + --$day; + $match[3] -= 1970; + $year -= 1970; + } + return $this->_assign($calc, $this->mktime($match[4], $match[5], $match[6], 1 + $months, 1 + $match[1], 1970 + $match[3], true), + $this->mktime($hour, $minute, $second, 1 + $month, 1 + $day, 1970 + $year, true), false); + break; + + case self::RFC_822: + case self::RFC_1036: + // new RFC 822 format, identical to RFC 1036 standard + $result = preg_match('/^\w{0,3},{0,1}\s{0,1}(\d{1,2})\s(\w{3})\s(\d{2})\s(\d{2}):(\d{2}):{0,1}(\d{0,2})\s([+-]{1}\d{4}|\w{1,20})$/', $date, $match); + if (!$result) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, RFC 822 date format expected", 0, null, $date); + } + + $months = $this->_getDigitFromName($match[2]); + $match[3] = self::getFullYear($match[3]); + + if (($calc == 'set') || ($calc == 'cmp')) { + --$months; + --$month; + --$match[1]; + --$day; + $match[3] -= 1970; + $year -= 1970; + } + return $this->_assign($calc, $this->mktime($match[4], $match[5], $match[6], 1 + $months, 1 + $match[1], 1970 + $match[3], false), + $this->mktime($hour, $minute, $second, 1 + $month, 1 + $day, 1970 + $year, false), false); + break; + + case self::RFC_850: + $result = preg_match('/^\w{6,9},\s(\d{2})-(\w{3})-(\d{2})\s(\d{2}):(\d{2}):(\d{2})\s.{3,21}$/', $date, $match); + if (!$result) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, RFC 850 date format expected", 0, null, $date); + } + + $months = $this->_getDigitFromName($match[2]); + $match[3] = self::getFullYear($match[3]); + + if (($calc == 'set') || ($calc == 'cmp')) { + --$months; + --$month; + --$match[1]; + --$day; + $match[3] -= 1970; + $year -= 1970; + } + return $this->_assign($calc, $this->mktime($match[4], $match[5], $match[6], 1 + $months, 1 + $match[1], 1970 + $match[3], true), + $this->mktime($hour, $minute, $second, 1 + $month, 1 + $day, 1970 + $year, true), false); + break; + + case self::RFC_1123: + $result = preg_match('/^\w{0,3},{0,1}\s{0,1}(\d{1,2})\s(\w{3})\s(\d{2,4})\s(\d{2}):(\d{2}):{0,1}(\d{0,2})\s([+-]{1}\d{4}|\w{1,20})$/', $date, $match); + if (!$result) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, RFC 1123 date format expected", 0, null, $date); + } + + $months = $this->_getDigitFromName($match[2]); + + if (($calc == 'set') || ($calc == 'cmp')) { + --$months; + --$month; + --$match[1]; + --$day; + $match[3] -= 1970; + $year -= 1970; + } + return $this->_assign($calc, $this->mktime($match[4], $match[5], $match[6], 1 + $months, 1 + $match[1], 1970 + $match[3], true), + $this->mktime($hour, $minute, $second, 1 + $month, 1 + $day, 1970 + $year, true), false); + break; + + case self::RSS: + $result = preg_match('/^\w{3},\s(\d{2})\s(\w{3})\s(\d{2,4})\s(\d{1,2}):(\d{2}):(\d{2})\s.{1,21}$/', $date, $match); + if (!$result) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, RSS date format expected", 0, null, $date); + } + + $months = $this->_getDigitFromName($match[2]); + $match[3] = self::getFullYear($match[3]); + + if (($calc == 'set') || ($calc == 'cmp')) { + --$months; + --$month; + --$match[1]; + --$day; + $match[3] -= 1970; + $year -= 1970; + } + return $this->_assign($calc, $this->mktime($match[4], $match[5], $match[6], 1 + $months, 1 + $match[1], 1970 + $match[3], true), + $this->mktime($hour, $minute, $second, 1 + $month, 1 + $day, 1970 + $year, true), false); + break; + + case self::W3C: + $result = preg_match('/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})[+-]{1}\d{2}:\d{2}$/', $date, $match); + if (!$result) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid date ($date) operand, W3C date format expected", 0, null, $date); + } + + if (($calc == 'set') || ($calc == 'cmp')) { + --$match[2]; + --$month; + --$match[3]; + --$day; + $match[1] -= 1970; + $year -= 1970; + } + return $this->_assign($calc, $this->mktime($match[4], $match[5], $match[6], 1 + $match[2], 1 + $match[3], 1970 + $match[1], true), + $this->mktime($hour, $minute, $second, 1 + $month, 1 + $day, 1970 + $year, true), false); + break; + + default: + if (!is_numeric($date) || !empty($part)) { + try { + if (empty($part)) { + $part = Zend_Locale_Format::getDateFormat($locale) . " "; + $part .= Zend_Locale_Format::getTimeFormat($locale); + } + + $parsed = Zend_Locale_Format::getDate($date, array('date_format' => $part, 'locale' => $locale, 'fix_date' => true, 'format_type' => 'iso')); + if ((strpos(strtoupper($part), 'YY') !== false) and (strpos(strtoupper($part), 'YYYY') === false)) { + $parsed['year'] = self::getFullYear($parsed['year']); + } + + if (($calc == 'set') || ($calc == 'cmp')) { + if (isset($parsed['month'])) { + --$parsed['month']; + } else { + $parsed['month'] = 0; + } + + if (isset($parsed['day'])) { + --$parsed['day']; + } else { + $parsed['day'] = 0; + } + + if (isset($parsed['year'])) { + $parsed['year'] -= 1970; + } else { + $parsed['year'] = 0; + } + } + + return $this->_assign($calc, $this->mktime( + isset($parsed['hour']) ? $parsed['hour'] : 0, + isset($parsed['minute']) ? $parsed['minute'] : 0, + isset($parsed['second']) ? $parsed['second'] : 0, + isset($parsed['month']) ? (1 + $parsed['month']) : 1, + isset($parsed['day']) ? (1 + $parsed['day']) : 1, + isset($parsed['year']) ? (1970 + $parsed['year']) : 1970, + false), $this->getUnixTimestamp(), false); + } catch (Zend_Locale_Exception $e) { + if (!is_numeric($date)) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception($e->getMessage(), 0, $e, $date); + } + } + } + + return $this->_assign($calc, $date, $this->getUnixTimestamp(), false); + break; + } + } + + /** + * Returns true when both date objects or date parts are equal. + * For example: + * 15.May.2000 <-> 15.June.2000 Equals only for Day or Year... all other will return false + * + * @param string|integer|array|Zend_Date $date Date or datepart to equal with + * @param string $part OPTIONAL Part of the date to compare, if null the timestamp is used + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return boolean + * @throws Zend_Date_Exception + */ + public function equals($date, $part = self::TIMESTAMP, $locale = null) + { + $result = $this->compare($date, $part, $locale); + + if ($result == 0) { + return true; + } + + return false; + } + + /** + * Returns if the given date or datepart is earlier + * For example: + * 15.May.2000 <-> 13.June.1999 will return true for day, year and date, but not for month + * + * @param string|integer|array|Zend_Date $date Date or datepart to compare with + * @param string $part OPTIONAL Part of the date to compare, if null the timestamp is used + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return boolean + * @throws Zend_Date_Exception + */ + public function isEarlier($date, $part = null, $locale = null) + { + $result = $this->compare($date, $part, $locale); + + if ($result == -1) { + return true; + } + + return false; + } + + /** + * Returns if the given date or datepart is later + * For example: + * 15.May.2000 <-> 13.June.1999 will return true for month but false for day, year and date + * Returns if the given date is later + * + * @param string|integer|array|Zend_Date $date Date or datepart to compare with + * @param string $part OPTIONAL Part of the date to compare, if null the timestamp is used + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return boolean + * @throws Zend_Date_Exception + */ + public function isLater($date, $part = null, $locale = null) + { + $result = $this->compare($date, $part, $locale); + + if ($result == 1) { + return true; + } + + return false; + } + + /** + * Returns only the time of the date as new Zend_Date object + * For example: + * 15.May.2000 10:11:23 will return a dateobject equal to 01.Jan.1970 10:11:23 + * + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date + */ + public function getTime($locale = null) + { + if (self::$_options['format_type'] == 'php') { + $format = 'H:i:s'; + } else { + $format = self::TIME_MEDIUM; + } + + return $this->copyPart($format, $locale); + } + + /** + * Returns the calculated time + * + * @param string $calc Calculation to make + * @param string|integer|array|Zend_Date $time Time to calculate with, if null the actual time is taken + * @param string $format Timeformat for parsing input + * @param string|Zend_Locale $locale Locale for parsing input + * @return integer|Zend_Date new time + * @throws Zend_Date_Exception + */ + private function _time($calc, $time, $format, $locale) + { + if ($time === null) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception('parameter $time must be set, null is not allowed'); + } + + if ($time instanceof Zend_Date) { + // extract time from object + $time = $time->toString('HH:mm:ss', 'iso'); + } else { + if (is_array($time)) { + if ((isset($time['hour']) === true) or (isset($time['minute']) === true) or + (isset($time['second']) === true)) { + $parsed = $time; + } else { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("no hour, minute or second given in array"); + } + } else { + if (self::$_options['format_type'] == 'php') { + $format = Zend_Locale_Format::convertPhpToIsoFormat($format); + } + try { + if ($locale === null) { + $locale = $this->getLocale(); + } + + $parsed = Zend_Locale_Format::getTime($time, array('date_format' => $format, 'locale' => $locale, 'format_type' => 'iso')); + } catch (Zend_Locale_Exception $e) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception($e->getMessage(), 0, $e); + } + } + + if (!array_key_exists('hour', $parsed)) { + $parsed['hour'] = 0; + } + + if (!array_key_exists('minute', $parsed)) { + $parsed['minute'] = 0; + } + + if (!array_key_exists('second', $parsed)) { + $parsed['second'] = 0; + } + + $time = str_pad($parsed['hour'], 2, '0', STR_PAD_LEFT) . ":"; + $time .= str_pad($parsed['minute'], 2, '0', STR_PAD_LEFT) . ":"; + $time .= str_pad($parsed['second'], 2, '0', STR_PAD_LEFT); + } + + $return = $this->_calcdetail($calc, $time, self::TIMES, 'de'); + if ($calc != 'cmp') { + return $this; + } + + return $return; + } + + + /** + * Sets a new time for the date object. Format defines how to parse the time string. + * Also a complete date can be given, but only the time is used for setting. + * For example: dd.MMMM.yyTHH:mm' and 'ss sec'-> 10.May.07T25:11 and 44 sec => 1h11min44sec + 1 day + * Returned is the new date object and the existing date is left as it was before + * + * @param string|integer|array|Zend_Date $time Time to set + * @param string $format OPTIONAL Timeformat for parsing input + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function setTime($time, $format = null, $locale = null) + { + return $this->_time('set', $time, $format, $locale); + } + + + /** + * Adds a time to the existing date. Format defines how to parse the time string. + * If only parts are given the other parts are set to 0. + * If no format is given, the standardformat of this locale is used. + * For example: HH:mm:ss -> 10 -> +10 hours + * + * @param string|integer|array|Zend_Date $time Time to add + * @param string $format OPTIONAL Timeformat for parsing input + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function addTime($time, $format = null, $locale = null) + { + return $this->_time('add', $time, $format, $locale); + } + + + /** + * Subtracts a time from the existing date. Format defines how to parse the time string. + * If only parts are given the other parts are set to 0. + * If no format is given, the standardformat of this locale is used. + * For example: HH:mm:ss -> 10 -> -10 hours + * + * @param string|integer|array|Zend_Date $time Time to sub + * @param string $format OPTIONAL Timeformat for parsing input + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid inteface + * @throws Zend_Date_Exception + */ + public function subTime($time, $format = null, $locale = null) + { + return $this->_time('sub', $time, $format, $locale); + } + + + /** + * Compares the time from the existing date. Format defines how to parse the time string. + * If only parts are given the other parts are set to default. + * If no format us given, the standardformat of this locale is used. + * For example: HH:mm:ss -> 10 -> 10 hours + * + * @param string|integer|array|Zend_Date $time Time to compare + * @param string $format OPTIONAL Timeformat for parsing input + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return integer 0 = equal, 1 = later, -1 = earlier + * @throws Zend_Date_Exception + */ + public function compareTime($time, $format = null, $locale = null) + { + return $this->_time('cmp', $time, $format, $locale); + } + + /** + * Returns a clone of $this, with the time part set to 00:00:00. + * + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date + */ + public function getDate($locale = null) + { + $orig = self::$_options['format_type']; + if (self::$_options['format_type'] == 'php') { + self::$_options['format_type'] = 'iso'; + } + + $date = $this->copyPart(self::DATE_MEDIUM, $locale); + $date->addTimestamp($this->getGmtOffset()); + self::$_options['format_type'] = $orig; + + return $date; + } + + /** + * Returns the calculated date + * + * @param string $calc Calculation to make + * @param string|integer|array|Zend_Date $date Date to calculate with, if null the actual date is taken + * @param string $format Date format for parsing + * @param string|Zend_Locale $locale Locale for parsing input + * @return integer|Zend_Date new date + * @throws Zend_Date_Exception + */ + private function _date($calc, $date, $format, $locale) + { + if ($date === null) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception('parameter $date must be set, null is not allowed'); + } + + if ($date instanceof Zend_Date) { + // extract date from object + $date = $date->toString('d.M.y', 'iso'); + } else { + if (is_array($date)) { + if ((isset($date['year']) === true) or (isset($date['month']) === true) or + (isset($date['day']) === true)) { + $parsed = $date; + } else { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("no day,month or year given in array"); + } + } else { + if ((self::$_options['format_type'] == 'php') && !defined($format)) { + $format = Zend_Locale_Format::convertPhpToIsoFormat($format); + } + try { + if ($locale === null) { + $locale = $this->getLocale(); + } + + $parsed = Zend_Locale_Format::getDate($date, array('date_format' => $format, 'locale' => $locale, 'format_type' => 'iso')); + if ((strpos(strtoupper($format), 'YY') !== false) and (strpos(strtoupper($format), 'YYYY') === false)) { + $parsed['year'] = self::getFullYear($parsed['year']); + } + } catch (Zend_Locale_Exception $e) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception($e->getMessage(), 0, $e); + } + } + + if (!array_key_exists('day', $parsed)) { + $parsed['day'] = 1; + } + + if (!array_key_exists('month', $parsed)) { + $parsed['month'] = 1; + } + + if (!array_key_exists('year', $parsed)) { + $parsed['year'] = 0; + } + + $date = $parsed['day'] . "." . $parsed['month'] . "." . $parsed['year']; + } + + $return = $this->_calcdetail($calc, $date, self::DATE_MEDIUM, 'de'); + if ($calc != 'cmp') { + return $this; + } + return $return; + } + + + /** + * Sets a new date for the date object. Format defines how to parse the date string. + * Also a complete date with time can be given, but only the date is used for setting. + * For example: MMMM.yy HH:mm-> May.07 22:11 => 01.May.07 00:00 + * Returned is the new date object and the existing time is left as it was before + * + * @param string|integer|array|Zend_Date $date Date to set + * @param string $format OPTIONAL Date format for parsing + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function setDate($date, $format = null, $locale = null) + { + return $this->_date('set', $date, $format, $locale); + } + + + /** + * Adds a date to the existing date object. Format defines how to parse the date string. + * If only parts are given the other parts are set to 0. + * If no format is given, the standardformat of this locale is used. + * For example: MM.dd.YYYY -> 10 -> +10 months + * + * @param string|integer|array|Zend_Date $date Date to add + * @param string $format OPTIONAL Date format for parsing input + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function addDate($date, $format = null, $locale = null) + { + return $this->_date('add', $date, $format, $locale); + } + + + /** + * Subtracts a date from the existing date object. Format defines how to parse the date string. + * If only parts are given the other parts are set to 0. + * If no format is given, the standardformat of this locale is used. + * For example: MM.dd.YYYY -> 10 -> -10 months + * Be aware: Subtracting 2 months is not equal to Adding -2 months !!! + * + * @param string|integer|array|Zend_Date $date Date to sub + * @param string $format OPTIONAL Date format for parsing input + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function subDate($date, $format = null, $locale = null) + { + return $this->_date('sub', $date, $format, $locale); + } + + + /** + * Compares the date from the existing date object, ignoring the time. + * Format defines how to parse the date string. + * If only parts are given the other parts are set to 0. + * If no format is given, the standardformat of this locale is used. + * For example: 10.01.2000 => 10.02.1999 -> false + * + * @param string|integer|array|Zend_Date $date Date to compare + * @param string $format OPTIONAL Date format for parsing input + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return integer 0 = equal, 1 = later, -1 = earlier + * @throws Zend_Date_Exception + */ + public function compareDate($date, $format = null, $locale = null) + { + return $this->_date('cmp', $date, $format, $locale); + } + + + /** + * Returns the full ISO 8601 date from the date object. + * Always the complete ISO 8601 specifiction is used. If an other ISO date is needed + * (ISO 8601 defines several formats) use toString() instead. + * This function does not return the ISO date as object. Use copy() instead. + * + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return string + */ + public function getIso($locale = null) + { + return $this->toString(self::ISO_8601, 'iso', $locale); + } + + + /** + * Sets a new date for the date object. Not given parts are set to default. + * Only supported ISO 8601 formats are accepted. + * For example: 050901 -> 01.Sept.2005 00:00:00, 20050201T10:00:30 -> 01.Feb.2005 10h00m30s + * Returned is the new date object + * + * @param string|integer|Zend_Date $date ISO Date to set + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function setIso($date, $locale = null) + { + return $this->_calcvalue('set', $date, 'iso', self::ISO_8601, $locale); + } + + + /** + * Adds a ISO date to the date object. Not given parts are set to default. + * Only supported ISO 8601 formats are accepted. + * For example: 050901 -> + 01.Sept.2005 00:00:00, 10:00:00 -> +10h + * Returned is the new date object + * + * @param string|integer|Zend_Date $date ISO Date to add + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function addIso($date, $locale = null) + { + return $this->_calcvalue('add', $date, 'iso', self::ISO_8601, $locale); + } + + + /** + * Subtracts a ISO date from the date object. Not given parts are set to default. + * Only supported ISO 8601 formats are accepted. + * For example: 050901 -> - 01.Sept.2005 00:00:00, 10:00:00 -> -10h + * Returned is the new date object + * + * @param string|integer|Zend_Date $date ISO Date to sub + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function subIso($date, $locale = null) + { + return $this->_calcvalue('sub', $date, 'iso', self::ISO_8601, $locale); + } + + + /** + * Compares a ISO date with the date object. Not given parts are set to default. + * Only supported ISO 8601 formats are accepted. + * For example: 050901 -> - 01.Sept.2005 00:00:00, 10:00:00 -> -10h + * Returns if equal, earlier or later + * + * @param string|integer|Zend_Date $date ISO Date to sub + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return integer 0 = equal, 1 = later, -1 = earlier + * @throws Zend_Date_Exception + */ + public function compareIso($date, $locale = null) + { + return $this->_calcvalue('cmp', $date, 'iso', self::ISO_8601, $locale); + } + + + /** + * Returns a RFC 822 compilant datestring from the date object. + * This function does not return the RFC date as object. Use copy() instead. + * + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return string + */ + public function getArpa($locale = null) + { + if (self::$_options['format_type'] == 'php') { + $format = 'D\, d M y H\:i\:s O'; + } else { + $format = self::RFC_822; + } + + return $this->toString($format, 'iso', $locale); + } + + + /** + * Sets a RFC 822 date as new date for the date object. + * Only RFC 822 compilant date strings are accepted. + * For example: Sat, 14 Feb 09 00:31:30 +0100 + * Returned is the new date object + * + * @param string|integer|Zend_Date $date RFC 822 to set + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function setArpa($date, $locale = null) + { + return $this->_calcvalue('set', $date, 'arpa', self::RFC_822, $locale); + } + + + /** + * Adds a RFC 822 date to the date object. + * ARPA messages are used in emails or HTTP Headers. + * Only RFC 822 compilant date strings are accepted. + * For example: Sat, 14 Feb 09 00:31:30 +0100 + * Returned is the new date object + * + * @param string|integer|Zend_Date $date RFC 822 Date to add + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function addArpa($date, $locale = null) + { + return $this->_calcvalue('add', $date, 'arpa', self::RFC_822, $locale); + } + + + /** + * Subtracts a RFC 822 date from the date object. + * ARPA messages are used in emails or HTTP Headers. + * Only RFC 822 compilant date strings are accepted. + * For example: Sat, 14 Feb 09 00:31:30 +0100 + * Returned is the new date object + * + * @param string|integer|Zend_Date $date RFC 822 Date to sub + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function subArpa($date, $locale = null) + { + return $this->_calcvalue('sub', $date, 'arpa', self::RFC_822, $locale); + } + + + /** + * Compares a RFC 822 compilant date with the date object. + * ARPA messages are used in emails or HTTP Headers. + * Only RFC 822 compilant date strings are accepted. + * For example: Sat, 14 Feb 09 00:31:30 +0100 + * Returns if equal, earlier or later + * + * @param string|integer|Zend_Date $date RFC 822 Date to sub + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return integer 0 = equal, 1 = later, -1 = earlier + * @throws Zend_Date_Exception + */ + public function compareArpa($date, $locale = null) + { + return $this->_calcvalue('cmp', $date, 'arpa', self::RFC_822, $locale); + } + + + /** + * Check if location is supported + * + * @param $location array - locations array + * @return $horizon float + */ + private function _checkLocation($location) + { + if (!isset($location['longitude']) or !isset($location['latitude'])) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception('Location must include \'longitude\' and \'latitude\'', 0, null, $location); + } + if (($location['longitude'] > 180) or ($location['longitude'] < -180)) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception('Longitude must be between -180 and 180', 0, null, $location); + } + if (($location['latitude'] > 90) or ($location['latitude'] < -90)) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception('Latitude must be between -90 and 90', 0, null, $location); + } + + if (!isset($location['horizon'])){ + $location['horizon'] = 'effective'; + } + + switch ($location['horizon']) { + case 'civil' : + return -0.104528; + break; + case 'nautic' : + return -0.207912; + break; + case 'astronomic' : + return -0.309017; + break; + default : + return -0.0145439; + break; + } + } + + + /** + * Returns the time of sunrise for this date and a given location as new date object + * For a list of cities and correct locations use the class Zend_Date_Cities + * + * @param $location array - location of sunrise + * ['horizon'] -> civil, nautic, astronomical, effective (default) + * ['longitude'] -> longitude of location + * ['latitude'] -> latitude of location + * @return Zend_Date + * @throws Zend_Date_Exception + */ + public function getSunrise($location) + { + $horizon = $this->_checkLocation($location); + $result = clone $this; + $result->set($this->calcSun($location, $horizon, true), self::TIMESTAMP); + return $result; + } + + + /** + * Returns the time of sunset for this date and a given location as new date object + * For a list of cities and correct locations use the class Zend_Date_Cities + * + * @param $location array - location of sunset + * ['horizon'] -> civil, nautic, astronomical, effective (default) + * ['longitude'] -> longitude of location + * ['latitude'] -> latitude of location + * @return Zend_Date + * @throws Zend_Date_Exception + */ + public function getSunset($location) + { + $horizon = $this->_checkLocation($location); + $result = clone $this; + $result->set($this->calcSun($location, $horizon, false), self::TIMESTAMP); + return $result; + } + + + /** + * Returns an array with the sunset and sunrise dates for all horizon types + * For a list of cities and correct locations use the class Zend_Date_Cities + * + * @param $location array - location of suninfo + * ['horizon'] -> civil, nautic, astronomical, effective (default) + * ['longitude'] -> longitude of location + * ['latitude'] -> latitude of location + * @return array - [sunset|sunrise][effective|civil|nautic|astronomic] + * @throws Zend_Date_Exception + */ + public function getSunInfo($location) + { + $suninfo = array(); + for ($i = 0; $i < 4; ++$i) { + switch ($i) { + case 0 : + $location['horizon'] = 'effective'; + break; + case 1 : + $location['horizon'] = 'civil'; + break; + case 2 : + $location['horizon'] = 'nautic'; + break; + case 3 : + $location['horizon'] = 'astronomic'; + break; + } + $horizon = $this->_checkLocation($location); + $result = clone $this; + $result->set($this->calcSun($location, $horizon, true), self::TIMESTAMP); + $suninfo['sunrise'][$location['horizon']] = $result; + $result = clone $this; + $result->set($this->calcSun($location, $horizon, false), self::TIMESTAMP); + $suninfo['sunset'][$location['horizon']] = $result; + } + return $suninfo; + } + + + /** + * Check a given year for leap year. + * + * @param integer|array|Zend_Date $year Year to check + * @return boolean + */ + public static function checkLeapYear($year) + { + if ($year instanceof Zend_Date) { + $year = (int) $year->toString(self::YEAR, 'iso'); + } + + if (is_array($year)) { + if (isset($year['year']) === true) { + $year = $year['year']; + } else { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("no year given in array"); + } + } + + if (!is_numeric($year)) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("year ($year) has to be integer for checkLeapYear()", 0, null, $year); + } + + return (bool) parent::isYearLeapYear($year); + } + + + /** + * Returns true, if the year is a leap year. + * + * @return boolean + */ + public function isLeapYear() + { + return self::checkLeapYear($this); + } + + + /** + * Returns if the set date is todays date + * + * @return boolean + */ + public function isToday() + { + $today = $this->date('Ymd', $this->_getTime()); + $day = $this->date('Ymd', $this->getUnixTimestamp()); + return ($today == $day); + } + + + /** + * Returns if the set date is yesterdays date + * + * @return boolean + */ + public function isYesterday() + { + list($year, $month, $day) = explode('-', $this->date('Y-m-d', $this->_getTime())); + // adjusts for leap days and DST changes that are timezone specific + $yesterday = $this->date('Ymd', $this->mktime(0, 0, 0, $month, $day -1, $year)); + $day = $this->date('Ymd', $this->getUnixTimestamp()); + return $day == $yesterday; + } + + + /** + * Returns if the set date is tomorrows date + * + * @return boolean + */ + public function isTomorrow() + { + list($year, $month, $day) = explode('-', $this->date('Y-m-d', $this->_getTime())); + // adjusts for leap days and DST changes that are timezone specific + $tomorrow = $this->date('Ymd', $this->mktime(0, 0, 0, $month, $day +1, $year)); + $day = $this->date('Ymd', $this->getUnixTimestamp()); + return $day == $tomorrow; + } + + /** + * Returns the actual date as new date object + * + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date + */ + public static function now($locale = null) + { + return new Zend_Date(time(), self::TIMESTAMP, $locale); + } + + /** + * Calculate date details + * + * @param string $calc Calculation to make + * @param string|integer|array|Zend_Date $date Date or Part to calculate + * @param string $part Datepart for Calculation + * @param string|Zend_Locale $locale Locale for parsing input + * @return integer|string new date + * @throws Zend_Date_Exception + */ + private function _calcdetail($calc, $date, $type, $locale) + { + $old = false; + if (self::$_options['format_type'] == 'php') { + self::$_options['format_type'] = 'iso'; + $old = true; + } + + switch($calc) { + case 'set' : + $return = $this->set($date, $type, $locale); + break; + case 'add' : + $return = $this->add($date, $type, $locale); + break; + case 'sub' : + $return = $this->sub($date, $type, $locale); + break; + default : + $return = $this->compare($date, $type, $locale); + break; + } + + if ($old) { + self::$_options['format_type'] = 'php'; + } + + return $return; + } + + /** + * Internal calculation, returns the requested date type + * + * @param string $calc Calculation to make + * @param string|integer|Zend_Date $value Datevalue to calculate with, if null the actual value is taken + * @param string|Zend_Locale $locale Locale for parsing input + * @return integer|Zend_Date new date + * @throws Zend_Date_Exception + */ + private function _calcvalue($calc, $value, $type, $parameter, $locale) + { + if ($value === null) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("parameter $type must be set, null is not allowed"); + } + + if ($locale === null) { + $locale = $this->getLocale(); + } + + if ($value instanceof Zend_Date) { + // extract value from object + $value = $value->toString($parameter, 'iso', $locale); + } else if (!is_array($value) && !is_numeric($value) && ($type != 'iso') && ($type != 'arpa')) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid $type ($value) operand", 0, null, $value); + } + + $return = $this->_calcdetail($calc, $value, $parameter, $locale); + if ($calc != 'cmp') { + return $this; + } + return $return; + } + + + /** + * Returns only the year from the date object as new object. + * For example: 10.May.2000 10:30:00 -> 01.Jan.2000 00:00:00 + * + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date + */ + public function getYear($locale = null) + { + if (self::$_options['format_type'] == 'php') { + $format = 'Y'; + } else { + $format = self::YEAR; + } + + return $this->copyPart($format, $locale); + } + + + /** + * Sets a new year + * If the year is between 0 and 69, 2000 will be set (2000-2069) + * If the year if between 70 and 99, 1999 will be set (1970-1999) + * 3 or 4 digit years are set as expected. If you need to set year 0-99 + * use set() instead. + * Returned is the new date object + * + * @param string|integer|array|Zend_Date $date Year to set + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function setYear($year, $locale = null) + { + return $this->_calcvalue('set', $year, 'year', self::YEAR, $locale); + } + + + /** + * Adds the year to the existing date object + * If the year is between 0 and 69, 2000 will be added (2000-2069) + * If the year if between 70 and 99, 1999 will be added (1970-1999) + * 3 or 4 digit years are added as expected. If you need to add years from 0-99 + * use add() instead. + * Returned is the new date object + * + * @param string|integer|array|Zend_Date $date Year to add + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function addYear($year, $locale = null) + { + return $this->_calcvalue('add', $year, 'year', self::YEAR, $locale); + } + + + /** + * Subs the year from the existing date object + * If the year is between 0 and 69, 2000 will be subtracted (2000-2069) + * If the year if between 70 and 99, 1999 will be subtracted (1970-1999) + * 3 or 4 digit years are subtracted as expected. If you need to subtract years from 0-99 + * use sub() instead. + * Returned is the new date object + * + * @param string|integer|array|Zend_Date $date Year to sub + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function subYear($year, $locale = null) + { + return $this->_calcvalue('sub', $year, 'year', self::YEAR, $locale); + } + + + /** + * Compares the year with the existing date object, ignoring other date parts. + * For example: 10.03.2000 -> 15.02.2000 -> true + * Returns if equal, earlier or later + * + * @param string|integer|array|Zend_Date $year Year to compare + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return integer 0 = equal, 1 = later, -1 = earlier + * @throws Zend_Date_Exception + */ + public function compareYear($year, $locale = null) + { + return $this->_calcvalue('cmp', $year, 'year', self::YEAR, $locale); + } + + + /** + * Returns only the month from the date object as new object. + * For example: 10.May.2000 10:30:00 -> 01.May.1970 00:00:00 + * + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date + */ + public function getMonth($locale = null) + { + if (self::$_options['format_type'] == 'php') { + $format = 'm'; + } else { + $format = self::MONTH; + } + + return $this->copyPart($format, $locale); + } + + + /** + * Returns the calculated month + * + * @param string $calc Calculation to make + * @param string|integer|array|Zend_Date $month Month to calculate with, if null the actual month is taken + * @param string|Zend_Locale $locale Locale for parsing input + * @return integer|Zend_Date new time + * @throws Zend_Date_Exception + */ + private function _month($calc, $month, $locale) + { + if ($month === null) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception('parameter $month must be set, null is not allowed'); + } + + if ($locale === null) { + $locale = $this->getLocale(); + } + + if ($month instanceof Zend_Date) { + // extract month from object + $found = $month->toString(self::MONTH_SHORT, 'iso', $locale); + } else { + if (is_numeric($month)) { + $found = $month; + } else if (is_array($month)) { + if (isset($month['month']) === true) { + $month = $month['month']; + } else { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("no month given in array"); + } + } else { + $monthlist = Zend_Locale_Data::getList($locale, 'month'); + $monthlist2 = Zend_Locale_Data::getList($locale, 'month', array('gregorian', 'format', 'abbreviated')); + + $monthlist = array_merge($monthlist, $monthlist2); + $found = 0; + $cnt = 0; + foreach ($monthlist as $key => $value) { + if (strtoupper($value) == strtoupper($month)) { + $found = ($key % 12) + 1; + break; + } + ++$cnt; + } + if ($found == 0) { + foreach ($monthlist2 as $key => $value) { + if (strtoupper(iconv_substr($value, 0, 1, 'UTF-8')) == strtoupper($month)) { + $found = $key + 1; + break; + } + ++$cnt; + } + } + if ($found == 0) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("unknown month name ($month)", 0, null, $month); + } + } + } + $return = $this->_calcdetail($calc, $found, self::MONTH_SHORT, $locale); + if ($calc != 'cmp') { + return $this; + } + return $return; + } + + + /** + * Sets a new month + * The month can be a number or a string. Setting months lower then 0 and greater then 12 + * will result in adding or subtracting the relevant year. (12 months equal one year) + * If a localized monthname is given it will be parsed with the default locale or the optional + * set locale. + * Returned is the new date object + * + * @param string|integer|array|Zend_Date $month Month to set + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function setMonth($month, $locale = null) + { + return $this->_month('set', $month, $locale); + } + + + /** + * Adds months to the existing date object. + * The month can be a number or a string. Adding months lower then 0 and greater then 12 + * will result in adding or subtracting the relevant year. (12 months equal one year) + * If a localized monthname is given it will be parsed with the default locale or the optional + * set locale. + * Returned is the new date object + * + * @param string|integer|array|Zend_Date $month Month to add + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function addMonth($month, $locale = null) + { + return $this->_month('add', $month, $locale); + } + + + /** + * Subtracts months from the existing date object. + * The month can be a number or a string. Subtracting months lower then 0 and greater then 12 + * will result in adding or subtracting the relevant year. (12 months equal one year) + * If a localized monthname is given it will be parsed with the default locale or the optional + * set locale. + * Returned is the new date object + * + * @param string|integer|array|Zend_Date $month Month to sub + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function subMonth($month, $locale = null) + { + return $this->_month('sub', $month, $locale); + } + + + /** + * Compares the month with the existing date object, ignoring other date parts. + * For example: 10.03.2000 -> 15.03.1950 -> true + * Returns if equal, earlier or later + * + * @param string|integer|array|Zend_Date $month Month to compare + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return integer 0 = equal, 1 = later, -1 = earlier + * @throws Zend_Date_Exception + */ + public function compareMonth($month, $locale = null) + { + return $this->_month('cmp', $month, $locale); + } + + + /** + * Returns the day as new date object + * Example: 20.May.1986 -> 20.Jan.1970 00:00:00 + * + * @param $locale string|Zend_Locale OPTIONAL Locale for parsing input + * @return Zend_Date + */ + public function getDay($locale = null) + { + return $this->copyPart(self::DAY_SHORT, $locale); + } + + + /** + * Returns the calculated day + * + * @param $calc string Type of calculation to make + * @param $day string|integer|Zend_Date Day to calculate, when null the actual day is calculated + * @param $locale string|Zend_Locale Locale for parsing input + * @return Zend_Date|integer + */ + private function _day($calc, $day, $locale) + { + if ($day === null) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception('parameter $day must be set, null is not allowed'); + } + + if ($locale === null) { + $locale = $this->getLocale(); + } + + if ($day instanceof Zend_Date) { + $day = $day->toString(self::DAY_SHORT, 'iso', $locale); + } + + if (is_numeric($day)) { + $type = self::DAY_SHORT; + } else if (is_array($day)) { + if (isset($day['day']) === true) { + $day = $day['day']; + $type = self::WEEKDAY; + } else { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("no day given in array"); + } + } else { + switch (iconv_strlen($day, 'UTF-8')) { + case 1 : + $type = self::WEEKDAY_NARROW; + break; + case 2: + $type = self::WEEKDAY_NAME; + break; + case 3: + $type = self::WEEKDAY_SHORT; + break; + default: + $type = self::WEEKDAY; + break; + } + } + $return = $this->_calcdetail($calc, $day, $type, $locale); + if ($calc != 'cmp') { + return $this; + } + return $return; + } + + + /** + * Sets a new day + * The day can be a number or a string. Setting days lower then 0 or greater than the number of this months days + * will result in adding or subtracting the relevant month. + * If a localized dayname is given it will be parsed with the default locale or the optional + * set locale. + * Returned is the new date object + * Example: setDay('Montag', 'de_AT'); will set the monday of this week as day. + * + * @param string|integer|array|Zend_Date $month Day to set + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function setDay($day, $locale = null) + { + return $this->_day('set', $day, $locale); + } + + + /** + * Adds days to the existing date object. + * The day can be a number or a string. Adding days lower then 0 or greater than the number of this months days + * will result in adding or subtracting the relevant month. + * If a localized dayname is given it will be parsed with the default locale or the optional + * set locale. + * + * @param string|integer|array|Zend_Date $month Day to add + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function addDay($day, $locale = null) + { + return $this->_day('add', $day, $locale); + } + + + /** + * Subtracts days from the existing date object. + * The day can be a number or a string. Subtracting days lower then 0 or greater than the number of this months days + * will result in adding or subtracting the relevant month. + * If a localized dayname is given it will be parsed with the default locale or the optional + * set locale. + * + * @param string|integer|array|Zend_Date $month Day to sub + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function subDay($day, $locale = null) + { + return $this->_day('sub', $day, $locale); + } + + + /** + * Compares the day with the existing date object, ignoring other date parts. + * For example: 'Monday', 'en' -> 08.Jan.2007 -> 0 + * Returns if equal, earlier or later + * + * @param string|integer|array|Zend_Date $day Day to compare + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return integer 0 = equal, 1 = later, -1 = earlier + * @throws Zend_Date_Exception + */ + public function compareDay($day, $locale = null) + { + return $this->_day('cmp', $day, $locale); + } + + + /** + * Returns the weekday as new date object + * Weekday is always from 1-7 + * Example: 09-Jan-2007 -> 2 = Tuesday -> 02-Jan-1970 (when 02.01.1970 is also Tuesday) + * + * @param $locale string|Zend_Locale OPTIONAL Locale for parsing input + * @return Zend_Date + */ + public function getWeekday($locale = null) + { + if (self::$_options['format_type'] == 'php') { + $format = 'l'; + } else { + $format = self::WEEKDAY; + } + + return $this->copyPart($format, $locale); + } + + + /** + * Returns the calculated weekday + * + * @param $calc string Type of calculation to make + * @param $weekday string|integer|array|Zend_Date Weekday to calculate, when null the actual weekday is calculated + * @param $locale string|Zend_Locale Locale for parsing input + * @return Zend_Date|integer + * @throws Zend_Date_Exception + */ + private function _weekday($calc, $weekday, $locale) + { + if ($weekday === null) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception('parameter $weekday must be set, null is not allowed'); + } + + if ($locale === null) { + $locale = $this->getLocale(); + } + + if ($weekday instanceof Zend_Date) { + $weekday = $weekday->toString(self::WEEKDAY_8601, 'iso', $locale); + } + + if (is_numeric($weekday)) { + $type = self::WEEKDAY_8601; + } else if (is_array($weekday)) { + if (isset($weekday['weekday']) === true) { + $weekday = $weekday['weekday']; + $type = self::WEEKDAY; + } else { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("no weekday given in array"); + } + } else { + switch(iconv_strlen($weekday, 'UTF-8')) { + case 1: + $type = self::WEEKDAY_NARROW; + break; + case 2: + $type = self::WEEKDAY_NAME; + break; + case 3: + $type = self::WEEKDAY_SHORT; + break; + default: + $type = self::WEEKDAY; + break; + } + } + $return = $this->_calcdetail($calc, $weekday, $type, $locale); + if ($calc != 'cmp') { + return $this; + } + return $return; + } + + + /** + * Sets a new weekday + * The weekday can be a number or a string. If a localized weekday name is given, + * then it will be parsed as a date in $locale (defaults to the same locale as $this). + * Returned is the new date object. + * Example: setWeekday(3); will set the wednesday of this week as day. + * + * @param string|integer|array|Zend_Date $month Weekday to set + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function setWeekday($weekday, $locale = null) + { + return $this->_weekday('set', $weekday, $locale); + } + + + /** + * Adds weekdays to the existing date object. + * The weekday can be a number or a string. + * If a localized dayname is given it will be parsed with the default locale or the optional + * set locale. + * Returned is the new date object + * Example: addWeekday(3); will add the difference of days from the begining of the month until + * wednesday. + * + * @param string|integer|array|Zend_Date $month Weekday to add + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function addWeekday($weekday, $locale = null) + { + return $this->_weekday('add', $weekday, $locale); + } + + + /** + * Subtracts weekdays from the existing date object. + * The weekday can be a number or a string. + * If a localized dayname is given it will be parsed with the default locale or the optional + * set locale. + * Returned is the new date object + * Example: subWeekday(3); will subtract the difference of days from the begining of the month until + * wednesday. + * + * @param string|integer|array|Zend_Date $month Weekday to sub + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function subWeekday($weekday, $locale = null) + { + return $this->_weekday('sub', $weekday, $locale); + } + + + /** + * Compares the weekday with the existing date object, ignoring other date parts. + * For example: 'Monday', 'en' -> 08.Jan.2007 -> 0 + * Returns if equal, earlier or later + * + * @param string|integer|array|Zend_Date $weekday Weekday to compare + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return integer 0 = equal, 1 = later, -1 = earlier + * @throws Zend_Date_Exception + */ + public function compareWeekday($weekday, $locale = null) + { + return $this->_weekday('cmp', $weekday, $locale); + } + + + /** + * Returns the day of year as new date object + * Example: 02.Feb.1986 10:00:00 -> 02.Feb.1970 00:00:00 + * + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date + */ + public function getDayOfYear($locale = null) + { + if (self::$_options['format_type'] == 'php') { + $format = 'D'; + } else { + $format = self::DAY_OF_YEAR; + } + + return $this->copyPart($format, $locale); + } + + + /** + * Sets a new day of year + * The day of year is always a number. + * Returned is the new date object + * Example: 04.May.2004 -> setDayOfYear(10) -> 10.Jan.2004 + * + * @param string|integer|array|Zend_Date $day Day of Year to set + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function setDayOfYear($day, $locale = null) + { + return $this->_calcvalue('set', $day, 'day of year', self::DAY_OF_YEAR, $locale); + } + + + /** + * Adds a day of year to the existing date object. + * The day of year is always a number. + * Returned is the new date object + * Example: addDayOfYear(10); will add 10 days to the existing date object. + * + * @param string|integer|array|Zend_Date $day Day of Year to add + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function addDayOfYear($day, $locale = null) + { + return $this->_calcvalue('add', $day, 'day of year', self::DAY_OF_YEAR, $locale); + } + + + /** + * Subtracts a day of year from the existing date object. + * The day of year is always a number. + * Returned is the new date object + * Example: subDayOfYear(10); will subtract 10 days from the existing date object. + * + * @param string|integer|array|Zend_Date $day Day of Year to sub + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function subDayOfYear($day, $locale = null) + { + return $this->_calcvalue('sub', $day, 'day of year', self::DAY_OF_YEAR, $locale); + } + + + /** + * Compares the day of year with the existing date object. + * For example: compareDayOfYear(33) -> 02.Feb.2007 -> 0 + * Returns if equal, earlier or later + * + * @param string|integer|array|Zend_Date $day Day of Year to compare + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return integer 0 = equal, 1 = later, -1 = earlier + * @throws Zend_Date_Exception + */ + public function compareDayOfYear($day, $locale = null) + { + return $this->_calcvalue('cmp', $day, 'day of year', self::DAY_OF_YEAR, $locale); + } + + + /** + * Returns the hour as new date object + * Example: 02.Feb.1986 10:30:25 -> 01.Jan.1970 10:00:00 + * + * @param $locale string|Zend_Locale OPTIONAL Locale for parsing input + * @return Zend_Date + */ + public function getHour($locale = null) + { + return $this->copyPart(self::HOUR, $locale); + } + + + /** + * Sets a new hour + * The hour is always a number. + * Returned is the new date object + * Example: 04.May.1993 13:07:25 -> setHour(7); -> 04.May.1993 07:07:25 + * + * @param string|integer|array|Zend_Date $hour Hour to set + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function setHour($hour, $locale = null) + { + return $this->_calcvalue('set', $hour, 'hour', self::HOUR_SHORT, $locale); + } + + + /** + * Adds hours to the existing date object. + * The hour is always a number. + * Returned is the new date object + * Example: 04.May.1993 13:07:25 -> addHour(12); -> 05.May.1993 01:07:25 + * + * @param string|integer|array|Zend_Date $hour Hour to add + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function addHour($hour, $locale = null) + { + return $this->_calcvalue('add', $hour, 'hour', self::HOUR_SHORT, $locale); + } + + + /** + * Subtracts hours from the existing date object. + * The hour is always a number. + * Returned is the new date object + * Example: 04.May.1993 13:07:25 -> subHour(6); -> 05.May.1993 07:07:25 + * + * @param string|integer|array|Zend_Date $hour Hour to sub + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function subHour($hour, $locale = null) + { + return $this->_calcvalue('sub', $hour, 'hour', self::HOUR_SHORT, $locale); + } + + + /** + * Compares the hour with the existing date object. + * For example: 10:30:25 -> compareHour(10) -> 0 + * Returns if equal, earlier or later + * + * @param string|integer|array|Zend_Date $hour Hour to compare + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return integer 0 = equal, 1 = later, -1 = earlier + * @throws Zend_Date_Exception + */ + public function compareHour($hour, $locale = null) + { + return $this->_calcvalue('cmp', $hour, 'hour', self::HOUR_SHORT, $locale); + } + + + /** + * Returns the minute as new date object + * Example: 02.Feb.1986 10:30:25 -> 01.Jan.1970 00:30:00 + * + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date + */ + public function getMinute($locale = null) + { + if (self::$_options['format_type'] == 'php') { + $format = 'i'; + } else { + $format = self::MINUTE; + } + + return $this->copyPart($format, $locale); + } + + + /** + * Sets a new minute + * The minute is always a number. + * Returned is the new date object + * Example: 04.May.1993 13:07:25 -> setMinute(29); -> 04.May.1993 13:29:25 + * + * @param string|integer|array|Zend_Date $minute Minute to set + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function setMinute($minute, $locale = null) + { + return $this->_calcvalue('set', $minute, 'minute', self::MINUTE_SHORT, $locale); + } + + + /** + * Adds minutes to the existing date object. + * The minute is always a number. + * Returned is the new date object + * Example: 04.May.1993 13:07:25 -> addMinute(65); -> 04.May.1993 13:12:25 + * + * @param string|integer|array|Zend_Date $minute Minute to add + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function addMinute($minute, $locale = null) + { + return $this->_calcvalue('add', $minute, 'minute', self::MINUTE_SHORT, $locale); + } + + + /** + * Subtracts minutes from the existing date object. + * The minute is always a number. + * Returned is the new date object + * Example: 04.May.1993 13:07:25 -> subMinute(9); -> 04.May.1993 12:58:25 + * + * @param string|integer|array|Zend_Date $minute Minute to sub + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function subMinute($minute, $locale = null) + { + return $this->_calcvalue('sub', $minute, 'minute', self::MINUTE_SHORT, $locale); + } + + + /** + * Compares the minute with the existing date object. + * For example: 10:30:25 -> compareMinute(30) -> 0 + * Returns if equal, earlier or later + * + * @param string|integer|array|Zend_Date $minute Hour to compare + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return integer 0 = equal, 1 = later, -1 = earlier + * @throws Zend_Date_Exception + */ + public function compareMinute($minute, $locale = null) + { + return $this->_calcvalue('cmp', $minute, 'minute', self::MINUTE_SHORT, $locale); + } + + + /** + * Returns the second as new date object + * Example: 02.Feb.1986 10:30:25 -> 01.Jan.1970 00:00:25 + * + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date + */ + public function getSecond($locale = null) + { + if (self::$_options['format_type'] == 'php') { + $format = 's'; + } else { + $format = self::SECOND; + } + + return $this->copyPart($format, $locale); + } + + + /** + * Sets new seconds to the existing date object. + * The second is always a number. + * Returned is the new date object + * Example: 04.May.1993 13:07:25 -> setSecond(100); -> 04.May.1993 13:08:40 + * + * @param string|integer|array|Zend_Date $second Second to set + * @param string|Zend_Locale $locale (Optional) Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function setSecond($second, $locale = null) + { + return $this->_calcvalue('set', $second, 'second', self::SECOND_SHORT, $locale); + } + + + /** + * Adds seconds to the existing date object. + * The second is always a number. + * Returned is the new date object + * Example: 04.May.1993 13:07:25 -> addSecond(65); -> 04.May.1993 13:08:30 + * + * @param string|integer|array|Zend_Date $second Second to add + * @param string|Zend_Locale $locale (Optional) Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function addSecond($second, $locale = null) + { + return $this->_calcvalue('add', $second, 'second', self::SECOND_SHORT, $locale); + } + + + /** + * Subtracts seconds from the existing date object. + * The second is always a number. + * Returned is the new date object + * Example: 04.May.1993 13:07:25 -> subSecond(10); -> 04.May.1993 13:07:15 + * + * @param string|integer|array|Zend_Date $second Second to sub + * @param string|Zend_Locale $locale (Optional) Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function subSecond($second, $locale = null) + { + return $this->_calcvalue('sub', $second, 'second', self::SECOND_SHORT, $locale); + } + + + /** + * Compares the second with the existing date object. + * For example: 10:30:25 -> compareSecond(25) -> 0 + * Returns if equal, earlier or later + * + * @param string|integer|array|Zend_Date $second Second to compare + * @param string|Zend_Locale $locale (Optional) Locale for parsing input + * @return integer 0 = equal, 1 = later, -1 = earlier + * @throws Zend_Date_Exception + */ + public function compareSecond($second, $locale = null) + { + return $this->_calcvalue('cmp', $second, 'second', self::SECOND_SHORT, $locale); + } + + + /** + * Returns the precision for fractional seconds + * + * @return integer + */ + public function getFractionalPrecision() + { + return $this->_precision; + } + + + /** + * Sets a new precision for fractional seconds + * + * @param integer $precision Precision for the fractional datepart 3 = milliseconds + * @throws Zend_Date_Exception + * @return Zend_Date Provides fluid interface + */ + public function setFractionalPrecision($precision) + { + if (!intval($precision) or ($precision < 0) or ($precision > 9)) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("precision ($precision) must be a positive integer less than 10", 0, null, $precision); + } + + $this->_precision = (int) $precision; + if ($this->_precision < strlen($this->_fractional)) { + $this->_fractional = substr($this->_fractional, 0, $this->_precision); + } else { + $this->_fractional = str_pad($this->_fractional, $this->_precision, '0', STR_PAD_RIGHT); + } + + return $this; + } + + + /** + * Returns the milliseconds of the date object + * + * @return string + */ + public function getMilliSecond() + { + return $this->_fractional; + } + + + /** + * Sets new milliseconds for the date object + * Example: setMilliSecond(550, 2) -> equals +5 Sec +50 MilliSec + * + * @param integer|Zend_Date $milli (Optional) Millisecond to set, when null the actual millisecond is set + * @param integer $precision (Optional) Fraction precision of the given milliseconds + * @return Zend_Date Provides fluid interface + */ + public function setMilliSecond($milli = null, $precision = null) + { + if ($milli === null) { + list($milli, $time) = explode(" ", microtime()); + $milli = intval($milli); + $precision = 6; + } else if (!is_numeric($milli)) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid milli second ($milli) operand", 0, null, $milli); + } + + if ($precision === null) { + $precision = $this->_precision; + } + + if (!is_int($precision) || $precision < 1 || $precision > 9) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("precision ($precision) must be a positive integer less than 10", 0, null, $precision); + } + + $this->_fractional = 0; + $this->addMilliSecond($milli, $precision); + return $this; + } + + + /** + * Adds milliseconds to the date object + * + * @param integer|Zend_Date $milli (Optional) Millisecond to add, when null the actual millisecond is added + * @param integer $precision (Optional) Fractional precision for the given milliseconds + * @return Zend_Date Provides fluid interface + */ + public function addMilliSecond($milli = null, $precision = null) + { + if ($milli === null) { + list($milli, $time) = explode(" ", microtime()); + $milli = intval($milli); + } else if (!is_numeric($milli)) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid milli second ($milli) operand", 0, null, $milli); + } + + if ($precision === null) { + $precision = strlen($milli); + if ($milli < 0) { + --$precision; + } + } + + if (!is_int($precision) || $precision < 1 || $precision > 9) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("precision ($precision) must be a positive integer less than 10", 0, null, $precision); + } + + $this->_fractional += $milli; + + // Add/sub milliseconds + add/sub seconds + $max = pow(10, $this->_precision); + // Milli includes seconds + if ($this->_fractional >= $max) { + while ($this->_fractional >= $max) { + $this->addSecond(1); + $this->_fractional -= $max; + } + } + + if ($this->_fractional < 0) { + while ($this->_fractional < 0) { + $this->subSecond(1); + $this->_fractional += $max; + } + } + + if ($this->_precision > strlen($this->_fractional)) { + $this->_fractional = str_pad($this->_fractional, $this->_precision, '0', STR_PAD_LEFT); + } + + return $this; + } + + + /** + * Subtracts a millisecond + * + * @param integer|Zend_Date $milli (Optional) Millisecond to sub, when null the actual millisecond is subtracted + * @param integer $precision (Optional) Fractional precision for the given milliseconds + * @return Zend_Date Provides fluid interface + */ + public function subMilliSecond($milli = null, $precision = null) + { + $this->addMilliSecond(0 - $milli, $precision); + return $this; + } + + /** + * Compares only the millisecond part, returning the difference + * + * @param integer|Zend_Date $milli OPTIONAL Millisecond to compare, when null the actual millisecond is compared + * @param integer $precision OPTIONAL Fractional precision for the given milliseconds + * @throws Zend_Date_Exception On invalid input + * @return integer 0 = equal, 1 = later, -1 = earlier + */ + public function compareMilliSecond($milli = null, $precision = null) + { + if ($milli === null) { + list($milli, $time) = explode(" ", microtime()); + $milli = intval($milli); + } else if (is_numeric($milli) === false) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("invalid milli second ($milli) operand", 0, null, $milli); + } + + if ($precision === null) { + $precision = strlen($milli); + } else if (!is_int($precision) || $precision < 1 || $precision > 9) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception("precision ($precision) must be a positive integer less than 10", 0, null, $precision); + } + + if ($precision === 0) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception('precision is 0'); + } + + if ($precision != $this->_precision) { + if ($precision > $this->_precision) { + $diff = $precision - $this->_precision; + $milli = (int) ($milli / (10 * $diff)); + } else { + $diff = $this->_precision - $precision; + $milli = (int) ($milli * (10 * $diff)); + } + } + + $comp = $this->_fractional - $milli; + if ($comp < 0) { + return -1; + } else if ($comp > 0) { + return 1; + } + return 0; + } + + /** + * Returns the week as new date object using monday as begining of the week + * Example: 12.Jan.2007 -> 08.Jan.1970 00:00:00 + * + * @param $locale string|Zend_Locale OPTIONAL Locale for parsing input + * @return Zend_Date + */ + public function getWeek($locale = null) + { + if (self::$_options['format_type'] == 'php') { + $format = 'W'; + } else { + $format = self::WEEK; + } + + return $this->copyPart($format, $locale); + } + + /** + * Sets a new week. The week is always a number. The day of week is not changed. + * Returned is the new date object + * Example: 09.Jan.2007 13:07:25 -> setWeek(1); -> 02.Jan.2007 13:07:25 + * + * @param string|integer|array|Zend_Date $week Week to set + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function setWeek($week, $locale = null) + { + return $this->_calcvalue('set', $week, 'week', self::WEEK, $locale); + } + + /** + * Adds a week. The week is always a number. The day of week is not changed. + * Returned is the new date object + * Example: 09.Jan.2007 13:07:25 -> addWeek(1); -> 16.Jan.2007 13:07:25 + * + * @param string|integer|array|Zend_Date $week Week to add + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function addWeek($week, $locale = null) + { + return $this->_calcvalue('add', $week, 'week', self::WEEK, $locale); + } + + /** + * Subtracts a week. The week is always a number. The day of week is not changed. + * Returned is the new date object + * Example: 09.Jan.2007 13:07:25 -> subWeek(1); -> 02.Jan.2007 13:07:25 + * + * @param string|integer|array|Zend_Date $week Week to sub + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return Zend_Date Provides fluid interface + * @throws Zend_Date_Exception + */ + public function subWeek($week, $locale = null) + { + return $this->_calcvalue('sub', $week, 'week', self::WEEK, $locale); + } + + /** + * Compares only the week part, returning the difference + * Returned is the new date object + * Returns if equal, earlier or later + * Example: 09.Jan.2007 13:07:25 -> compareWeek(2); -> 0 + * + * @param string|integer|array|Zend_Date $week Week to compare + * @param string|Zend_Locale $locale OPTIONAL Locale for parsing input + * @return integer 0 = equal, 1 = later, -1 = earlier + */ + public function compareWeek($week, $locale = null) + { + return $this->_calcvalue('cmp', $week, 'week', self::WEEK, $locale); + } + + /** + * Sets a new standard locale for the date object. + * This locale will be used for all functions + * Returned is the really set locale. + * Example: 'de_XX' will be set to 'de' because 'de_XX' does not exist + * 'xx_YY' will be set to 'root' because 'xx' does not exist + * + * @param string|Zend_Locale $locale (Optional) Locale for parsing input + * @throws Zend_Date_Exception When the given locale does not exist + * @return Zend_Date Provides fluent interface + */ + public function setLocale($locale = null) + { + try { + $this->_locale = Zend_Locale::findLocale($locale); + } catch (Zend_Locale_Exception $e) { + #require_once 'Zend/Date/Exception.php'; + throw new Zend_Date_Exception($e->getMessage(), 0, $e); + } + + return $this; + } + + /** + * Returns the actual set locale + * + * @return string + */ + public function getLocale() + { + return $this->_locale; + } + + /** + * Checks if the given date is a real date or datepart. + * Returns false if a expected datepart is missing or a datepart exceeds its possible border. + * But the check will only be done for the expected dateparts which are given by format. + * If no format is given the standard dateformat for the actual locale is used. + * f.e. 30.February.2007 will return false if format is 'dd.MMMM.YYYY' + * + * @param string|array|Zend_Date $date Date to parse for correctness + * @param string $format (Optional) Format for parsing the date string + * @param string|Zend_Locale $locale (Optional) Locale for parsing date parts + * @return boolean True when all date parts are correct + */ + public static function isDate($date, $format = null, $locale = null) + { + if (!is_string($date) && !is_numeric($date) && !($date instanceof Zend_Date) && + !is_array($date)) { + return false; + } + + if (($format !== null) && ($format != 'ee') && ($format != 'ss') && ($format != 'GG') && ($format != 'MM') && ($format != 'EE') && ($format != 'TT') + && (Zend_Locale::isLocale($format, null, false))) { + $locale = $format; + $format = null; + } + + $locale = Zend_Locale::findLocale($locale); + + if ($format === null) { + $format = Zend_Locale_Format::getDateFormat($locale); + } else if ((self::$_options['format_type'] == 'php') && !defined($format)) { + $format = Zend_Locale_Format::convertPhpToIsoFormat($format); + } + + $format = self::_getLocalizedToken($format, $locale); + if (!is_array($date)) { + try { + $parsed = Zend_Locale_Format::getDate($date, array('locale' => $locale, + 'date_format' => $format, 'format_type' => 'iso', + 'fix_date' => false)); + } catch (Zend_Locale_Exception $e) { + // Date can not be parsed + return false; + } + } else { + $parsed = $date; + } + + if (((strpos($format, 'Y') !== false) or (strpos($format, 'y') !== false)) and + (!isset($parsed['year']))) { + // Year expected but not found + return false; + } + + if ((strpos($format, 'M') !== false) and (!isset($parsed['month']))) { + // Month expected but not found + return false; + } + + if ((strpos($format, 'd') !== false) and (!isset($parsed['day']))) { + // Day expected but not found + return false; + } + + if (((strpos($format, 'H') !== false) or (strpos($format, 'h') !== false)) and + (!isset($parsed['hour']))) { + // Hour expected but not found + return false; + } + + if ((strpos($format, 'm') !== false) and (!isset($parsed['minute']))) { + // Minute expected but not found + return false; + } + + if ((strpos($format, 's') !== false) and (!isset($parsed['second']))) { + // Second expected but not found + return false; + } + + // Set not given dateparts + if (isset($parsed['hour']) === false) { + $parsed['hour'] = 12; + } + + if (isset($parsed['minute']) === false) { + $parsed['minute'] = 0; + } + + if (isset($parsed['second']) === false) { + $parsed['second'] = 0; + } + + if (isset($parsed['month']) === false) { + $parsed['month'] = 1; + } + + if (isset($parsed['day']) === false) { + $parsed['day'] = 1; + } + + if (isset($parsed['year']) === false) { + $parsed['year'] = 1970; + } + + if (self::isYearLeapYear($parsed['year'])) { + $parsed['year'] = 1972; + } else { + $parsed['year'] = 1971; + } + + $date = new self($parsed, null, $locale); + $timestamp = $date->mktime($parsed['hour'], $parsed['minute'], $parsed['second'], + $parsed['month'], $parsed['day'], $parsed['year']); + + if ($parsed['year'] != $date->date('Y', $timestamp)) { + // Given year differs from parsed year + return false; + } + + if ($parsed['month'] != $date->date('n', $timestamp)) { + // Given month differs from parsed month + return false; + } + + if ($parsed['day'] != $date->date('j', $timestamp)) { + // Given day differs from parsed day + return false; + } + + if ($parsed['hour'] != $date->date('G', $timestamp)) { + // Given hour differs from parsed hour + return false; + } + + if ($parsed['minute'] != $date->date('i', $timestamp)) { + // Given minute differs from parsed minute + return false; + } + + if ($parsed['second'] != $date->date('s', $timestamp)) { + // Given second differs from parsed second + return false; + } + + return true; + } + + /** + * Returns the ISO Token for all localized constants + * + * @param string $token Token to normalize + * @param string $locale Locale to search + * @return string + */ + protected static function _getLocalizedToken($token, $locale) + { + switch($token) { + case self::ISO_8601 : + return "yyyy-MM-ddThh:mm:ss"; + break; + case self::RFC_2822 : + return "EEE, dd MMM yyyy HH:mm:ss"; + break; + case self::DATES : + return Zend_Locale_Data::getContent($locale, 'date'); + break; + case self::DATE_FULL : + return Zend_Locale_Data::getContent($locale, 'date', array('gregorian', 'full')); + break; + case self::DATE_LONG : + return Zend_Locale_Data::getContent($locale, 'date', array('gregorian', 'long')); + break; + case self::DATE_MEDIUM : + return Zend_Locale_Data::getContent($locale, 'date', array('gregorian', 'medium')); + break; + case self::DATE_SHORT : + return Zend_Locale_Data::getContent($locale, 'date', array('gregorian', 'short')); + break; + case self::TIMES : + return Zend_Locale_Data::getContent($locale, 'time'); + break; + case self::TIME_FULL : + return Zend_Locale_Data::getContent($locale, 'time', array('gregorian', 'full')); + break; + case self::TIME_LONG : + return Zend_Locale_Data::getContent($locale, 'time', array('gregorian', 'long')); + break; + case self::TIME_MEDIUM : + return Zend_Locale_Data::getContent($locale, 'time', array('gregorian', 'medium')); + break; + case self::TIME_SHORT : + return Zend_Locale_Data::getContent($locale, 'time', array('gregorian', 'short')); + break; + case self::DATETIME : + return Zend_Locale_Data::getContent($locale, 'datetime'); + break; + case self::DATETIME_FULL : + return Zend_Locale_Data::getContent($locale, 'datetime', array('gregorian', 'full')); + break; + case self::DATETIME_LONG : + return Zend_Locale_Data::getContent($locale, 'datetime', array('gregorian', 'long')); + break; + case self::DATETIME_MEDIUM : + return Zend_Locale_Data::getContent($locale, 'datetime', array('gregorian', 'medium')); + break; + case self::DATETIME_SHORT : + return Zend_Locale_Data::getContent($locale, 'datetime', array('gregorian', 'short')); + break; + case self::ATOM : + case self::RFC_3339 : + case self::W3C : + return "yyyy-MM-DD HH:mm:ss"; + break; + case self::COOKIE : + case self::RFC_850 : + return "EEEE, dd-MM-yyyy HH:mm:ss"; + break; + case self::RFC_822 : + case self::RFC_1036 : + case self::RFC_1123 : + case self::RSS : + return "EEE, dd MM yyyy HH:mm:ss"; + break; + } + + return $token; + } + + /** + * Get unix timestamp. + * Added limitation: $year value must be between -10 000 and 10 000 + * Parent method implementation causes 504 error if it gets too big(small) year value + * + * @see Zend_Date_DateObject::mktime + * @throws Zend_Date_Exception + * @param $hour + * @param $minute + * @param $second + * @param $month + * @param $day + * @param $year + * @param bool $gmt + * @return float|int + */ + protected function mktime($hour, $minute, $second, $month, $day, $year, $gmt = false) + { + $day = intval($day); + $month = intval($month); + $year = intval($year); + + // correct months > 12 and months < 1 + if ($month > 12) { + $overlap = floor($month / 12); + $year += $overlap; + $month -= $overlap * 12; + } else { + $overlap = ceil((1 - $month) / 12); + $year -= $overlap; + $month += $overlap * 12; + } + + if ($year > self::YEAR_MAX_VALUE || $year < self::YEAR_MIN_VALUE) { + throw new Zend_Date_Exception('Invalid year, it must be between ' . self::YEAR_MIN_VALUE . ' and ' + . self::YEAR_MAX_VALUE); + } + + return parent::mktime($hour, $minute, $second, $month, $day, $year, $gmt); + } +} diff --git a/app/design/adminhtml/default/default/layout/captcha.xml b/app/design/adminhtml/default/default/layout/captcha.xml new file mode 100755 index 0000000000..0615f967d7 --- /dev/null +++ b/app/design/adminhtml/default/default/layout/captcha.xml @@ -0,0 +1,48 @@ + + + + + + + backend_login + 226 + 50 + + + + + + + backend_forgotpassword + 226 + 50 + + + + diff --git a/app/design/adminhtml/default/default/layout/customer.xml b/app/design/adminhtml/default/default/layout/customer.xml index b5b7415149..5b439d8c93 100644 --- a/app/design/adminhtml/default/default/layout/customer.xml +++ b/app/design/adminhtml/default/default/layout/customer.xml @@ -57,4 +57,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/design/adminhtml/default/default/layout/main.xml b/app/design/adminhtml/default/default/layout/main.xml index 77af339d41..eb72265b83 100644 --- a/app/design/adminhtml/default/default/layout/main.xml +++ b/app/design/adminhtml/default/default/layout/main.xml @@ -56,6 +56,7 @@ Default layout, loads most of the pages Magento Admin + jsextjs/fix-defer-before.jscan_load_ext_js @@ -247,10 +248,42 @@ Base preview layout + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - diff --git a/app/design/adminhtml/default/default/layout/promo.xml b/app/design/adminhtml/default/default/layout/promo.xml index 642380065e..b76eed4c57 100644 --- a/app/design/adminhtml/default/default/layout/promo.xml +++ b/app/design/adminhtml/default/default/layout/promo.xml @@ -73,11 +73,17 @@ + + + + main_sectionpromo_quote_edit_tab_main conditions_sectionpromo_quote_edit_tab_conditions actions_sectionpromo_quote_edit_tab_actions labels_sectionpromo_quote_edit_tab_labels + coupons_sectionpromo_quote_edit_tab_coupons + @@ -87,4 +93,9 @@ + + + + + diff --git a/app/design/adminhtml/default/default/layout/report.xml b/app/design/adminhtml/default/default/layout/report.xml index c042d44672..c76ca36561 100644 --- a/app/design/adminhtml/default/default/layout/report.xml +++ b/app/design/adminhtml/default/default/layout/report.xml @@ -32,4 +32,24 @@ + + + + This report depends on timezone configuration. Once timezone is changed, the lifetime statistics need to be refreshed. + + + + + + store_ids + + + + report_type + 0 + + + + + diff --git a/app/design/adminhtml/default/default/layout/sales.xml b/app/design/adminhtml/default/default/layout/sales.xml index 3ab7b25798..c7ac871b38 100644 --- a/app/design/adminhtml/default/default/layout/sales.xml +++ b/app/design/adminhtml/default/default/layout/sales.xml @@ -924,7 +924,7 @@ store_ids - + created_at_order Order Created Date diff --git a/app/design/adminhtml/default/default/layout/tag.xml b/app/design/adminhtml/default/default/layout/tag.xml index b47e61f329..1af8f3d49b 100644 --- a/app/design/adminhtml/default/default/layout/tag.xml +++ b/app/design/adminhtml/default/default/layout/tag.xml @@ -49,4 +49,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/design/adminhtml/default/default/template/backup/dialogs.phtml b/app/design/adminhtml/default/default/template/backup/dialogs.phtml new file mode 100644 index 0000000000..e19bbc9c94 --- /dev/null +++ b/app/design/adminhtml/default/default/template/backup/dialogs.phtml @@ -0,0 +1,125 @@ + + + + +
' . $this->helper('catalog/output') ->productAttribute($product, $product->getShortDescription(), 'short_description') . '

'; - if ($wishlistProduct->getAllowedPriceInRss()) { - $description .= $this->getPriceHtml($wishlistProduct,true); + if ($product->getAllowedPriceInRss()) { + $description .= $this->getPriceHtml($product,true); } $description .= '

'; if ($this->hasDescription($product)) { diff --git a/app/code/core/Mage/Rss/etc/config.xml b/app/code/core/Mage/Rss/etc/config.xml index 7f7b4f7719..7b798df1e9 100644 --- a/app/code/core/Mage/Rss/etc/config.xml +++ b/app/code/core/Mage/Rss/etc/config.xml @@ -49,7 +49,7 @@ - + @@ -71,22 +71,22 @@ - + rss/observer salesOrderItemSaveAfterNotifyStock - - + + rss/observer salesOrderItemSaveAfterOrderNew - + diff --git a/app/code/core/Mage/Rule/Block/Editable.php b/app/code/core/Mage/Rule/Block/Editable.php index 4a33bad404..951cd22c0c 100644 --- a/app/code/core/Mage/Rule/Block/Editable.php +++ b/app/code/core/Mage/Rule/Block/Editable.php @@ -28,35 +28,47 @@ class Mage_Rule_Block_Editable extends Mage_Core_Block_Abstract implements Varien_Data_Form_Element_Renderer_Interface { + /** + * Render element + * + * @param Varien_Data_Form_Element_Abstract $element + * @see Varien_Data_Form_Element_Renderer_Interface::render() + * @return string + */ public function render(Varien_Data_Form_Element_Abstract $element) { $element->addClass('element-value-changer'); $valueName = $element->getValueName(); - if ($valueName==='') { + if ($valueName === '') { $valueName = '...'; } - if ($element->getShowAsText()) { - $html = ' '; - $html.= htmlspecialchars($valueName).' '; + if ($element->getShowAsText()) { + $html = ' ' + . htmlspecialchars($valueName) . ' '; } else { - $html = ' getParamId() ? ' id="' . $element->getParamId() . '"' : '') . '>'; - - $html.= ''; + $html = ' getParamId() ? ' id="' . $element->getParamId() . '"' : '') . '>' + . ''; - $html.= htmlspecialchars(Mage::helper('core/string')->truncate($valueName, 33, '...')); + $translate = Mage::getSingleton('core/translate_inline'); - $html.= ' '; + $html .= $translate->isAllowed() ? Mage::helper('core')->escapeHtml($valueName) : + Mage::helper('core')->escapeHtml(Mage::helper('core/string')->truncate($valueName, 33, '...')); - $html.= $element->getElementHtml(); + $html .= ' ' . $element->getElementHtml(); if ($element->getExplicitApply()) { - $html.= ' '.$this->__('Apply').' '; + $html .= ' '
+                    . $this->__('Apply') . ' '; } - $html.= ' '; - } + $html .= ' '; + } + return $html; } } diff --git a/app/code/core/Mage/Rule/Model/Condition/Combine.php b/app/code/core/Mage/Rule/Model/Condition/Combine.php index ffab00c731..70e2a33327 100644 --- a/app/code/core/Mage/Rule/Model/Condition/Combine.php +++ b/app/code/core/Mage/Rule/Model/Condition/Combine.php @@ -27,6 +27,47 @@ class Mage_Rule_Model_Condition_Combine extends Mage_Rule_Model_Condition_Abstract { + /** + * Store all used condition models + * + * @var array + */ + static protected $_conditionModels = array(); + + + + + + /** + * Retrieve new object for each requested model. + * If model is requested first time, store it at static array. + * + * It's made by performance reasons to avoid initialization of same models each time when rules are being processed. + * + * @param string $modelClass + * @return Mage_Rule_Model_Condition_Abstract|bool + */ + protected function _getNewConditionModelInstance($modelClass) + { + if (empty($modelClass)) { + return false; + } + + if (!array_key_exists($modelClass, self::$_conditionModels)) { + $model = Mage::getModel($modelClass); + self::$_conditionModels[$modelClass] = $model; + } else { + $model = self::$_conditionModels[$modelClass]; + } + + if (!$model) { + return false; + } + + $newModel = clone $model; + return $newModel; + } + public function __construct() { parent::__construct(); @@ -165,8 +206,8 @@ public function loadArray($arr, $key='conditions') if (!empty($arr[$key]) && is_array($arr[$key])) { foreach ($arr[$key] as $condArr) { try { - $cond = @Mage::getModel($condArr['type']); - if (!empty($cond)) { + $cond = $this->_getNewConditionModelInstance($condArr['type']); + if ($cond) { $this->addCondition($cond); $cond->loadArray($condArr, $key); } @@ -194,11 +235,8 @@ public function loadXml($xml) public function asHtml() { $html = $this->getTypeElement()->getHtml(). - Mage::helper('rule')->__("If %s of these conditions are %s:", - $this->getAggregatorElement()->getHtml(), - $this->getValueElement()->getHtml() - ); - if ($this->getId()!='1') { + Mage::helper('rule')->__('If %s of these conditions are %s:', $this->getAggregatorElement()->getHtml(), $this->getValueElement()->getHtml()); + if ($this->getId() != '1') { $html.= $this->getRemoveLinkHtml(); } return $html; diff --git a/app/code/core/Mage/Sales/Block/Adminhtml/Report/Filter/Form/Coupon.php b/app/code/core/Mage/Sales/Block/Adminhtml/Report/Filter/Form/Coupon.php new file mode 100644 index 0000000000..bb35aa56b2 --- /dev/null +++ b/app/code/core/Mage/Sales/Block/Adminhtml/Report/Filter/Form/Coupon.php @@ -0,0 +1,89 @@ + + */ +class Mage_Sales_Block_Adminhtml_Report_Filter_Form_Coupon extends Mage_Sales_Block_Adminhtml_Report_Filter_Form +{ + /** + * Prepare form + * + * @return Mage_Sales_Block_Adminhtml_Report_Filter_Form_Coupon + */ + protected function _prepareForm() + { + parent::_prepareForm(); + + $form = $this->getForm(); + $htmlIdPrefix = $form->getHtmlIdPrefix(); + + /** @var Varien_Data_Form_Element_Fieldset $fieldset */ + $fieldset = $this->getForm()->getElement('base_fieldset'); + + if (is_object($fieldset) && $fieldset instanceof Varien_Data_Form_Element_Fieldset) { + + $fieldset->addField('price_rule_type', 'select', array( + 'name' => 'price_rule_type', + 'options' => array( + Mage::helper('reports')->__('Any'), + Mage::helper('reports')->__('Specified') + ), + 'label' => Mage::helper('reports')->__('Shopping Cart Price Rule'), + )); + + $rulesList = Mage::getResourceModel('salesrule/report_rule')->getUniqRulesNamesList(); + + $rulesListOptions = array(); + + foreach ($rulesList as $key => $ruleName) { + $rulesListOptions[] = array( + 'label' => $ruleName, + 'value' => $key, + 'title' => $ruleName + ); + } + + $fieldset->addField('rules_list', 'multiselect', array( + 'name' => 'rules_list', + 'values' => $rulesListOptions, + 'display' => 'none' + ), 'price_rule_type'); + + $this->setChild('form_after', $this->getLayout()->createBlock('adminhtml/widget_form_element_dependence') + ->addFieldMap($htmlIdPrefix . 'price_rule_type', 'price_rule_type') + ->addFieldMap($htmlIdPrefix . 'rules_list', 'rules_list') + ->addFieldDependence('rules_list', 'price_rule_type', '1') + ); + } + + return $this; + } +} diff --git a/app/code/core/Mage/Sales/Model/Entity/Quote/Address/Attribute/Frontend/Discount.php b/app/code/core/Mage/Sales/Model/Entity/Quote/Address/Attribute/Frontend/Discount.php index 6b237bcc9a..eee7f5eb8b 100644 --- a/app/code/core/Mage/Sales/Model/Entity/Quote/Address/Attribute/Frontend/Discount.php +++ b/app/code/core/Mage/Sales/Model/Entity/Quote/Address/Attribute/Frontend/Discount.php @@ -33,8 +33,9 @@ public function fetchTotals(Mage_Sales_Model_Quote_Address $address) $amount = $address->getDiscountAmount(); if ($amount!=0) { $title = Mage::helper('sales')->__('Discount'); - if ($address->getQuote()->getCouponCode()) { - $title .= ' ('.$address->getQuote()->getCouponCode().')'; + $couponCode = $address->getQuote()->getCouponCode(); + if (strlen($couponCode)) { + $title .= ' ('. $couponCode .')'; } $address->addTotal(array( 'code'=>'discount', diff --git a/app/code/core/Mage/Sales/Model/Mysql4/Abstract.php b/app/code/core/Mage/Sales/Model/Mysql4/Abstract.php index 3b01bf77fe..e15afa35b3 100644 --- a/app/code/core/Mage/Sales/Model/Mysql4/Abstract.php +++ b/app/code/core/Mage/Sales/Model/Mysql4/Abstract.php @@ -32,6 +32,6 @@ * @package Mage_Sales * @author Magento Core Team */ -class Mage_Sales_Model_Mysql4_Abstract extends Mage_Sales_Model_Resource_Abstract +abstract class Mage_Sales_Model_Mysql4_Abstract extends Mage_Sales_Model_Resource_Abstract { } diff --git a/app/code/core/Mage/Sales/Model/Mysql4/Report/Abstract.php b/app/code/core/Mage/Sales/Model/Mysql4/Report/Abstract.php index 34858bb550..9292c9db2f 100644 --- a/app/code/core/Mage/Sales/Model/Mysql4/Report/Abstract.php +++ b/app/code/core/Mage/Sales/Model/Mysql4/Report/Abstract.php @@ -32,6 +32,6 @@ * @package Mage_Sales * @author Magento Core Team */ -class Mage_Sales_Model_Mysql4_Report_Abstract extends Mage_Sales_Model_Resource_Report_Abstract +abstract class Mage_Sales_Model_Mysql4_Report_Abstract extends Mage_Sales_Model_Resource_Report_Abstract { } diff --git a/app/code/core/Mage/Sales/Model/Observer.php b/app/code/core/Mage/Sales/Model/Observer.php index 46981f51e5..4700dd31bc 100644 --- a/app/code/core/Mage/Sales/Model/Observer.php +++ b/app/code/core/Mage/Sales/Model/Observer.php @@ -334,5 +334,113 @@ public function setQuoteCanApplyMsrp(Varien_Event_Observer $observer) $quote->setCanApplyMsrp($canApplyMsrp); } -} + /** + * Handle customer VAT number if needed + * + * @param Varien_Event_Observer $observer + */ + public function handleCustomerVatNumber(Varien_Event_Observer $observer) + { + /** @var $addressHelper Mage_Customer_Helper_Address */ + $addressHelper = Mage::helper('customer/address'); + + /** @var $customerInstance Mage_Customer_Model_Customer */ + $customerInstance = $observer->getQuote()->getCustomer(); + + if (!$addressHelper->isVatValidationEnabled($customerInstance->getStore())) { + return; + } + + /** @var $quoteInstance Mage_Sales_Model_Quote */ + $quoteInstance = $observer->getQuote(); + $quoteBillingAddress = $quoteInstance->getBillingAddress(); + + /** @var $customerHelper Mage_Customer_Helper_Data */ + $customerHelper = Mage::helper('customer'); + + $customerAddressId = $quoteBillingAddress->getCustomerAddressId(); + $customerDefaultBillingAddressId = $customerInstance->getDefaultBilling(); + + $customerCountryCode = $quoteBillingAddress->getCountryId(); + $customerVatNumber = $quoteBillingAddress->getVatId(); + + if (empty($customerVatNumber) || !Mage::helper('core')->isCountryInEU($customerCountryCode)) { + $groupId = $customerHelper->getDefaultCustomerGroupId($customerInstance->getStore()); + + if ($groupId && $customerInstance->getGroupId() != $groupId) { + $quoteInstance->setCustomer($customerInstance->setGroupId($groupId)); + $quoteInstance->setCustomerGroupId($groupId); + } + + return; + } + + $mustValidateVat = $addressHelper->getValidateOnEachTransaction($customerInstance->getStore()) + || $customerCountryCode != $quoteBillingAddress->getValidatedCountryCode() + || $customerVatNumber != $quoteBillingAddress->getValidatedVatNumber(); + + if (!$mustValidateVat) { + return; + } + + /** @var $coreHelper Mage_Core_Helper_Data */ + $coreHelper = Mage::helper('core'); + $merchantCountryCode = $coreHelper->getMerchantCountryCode(); + $merchantVatNumber = $coreHelper->getMerchantVatNumber(); + + $gatewayResponse = $customerHelper->checkVatNumber( + $customerCountryCode, + $customerVatNumber, + ($merchantVatNumber !== '') ? $merchantCountryCode : '', + $merchantVatNumber + ); + + // Store validation results in quote billing address + $quoteBillingAddress->setVatIsValid((int) $gatewayResponse->getIsValid()) + ->setVatRequestId($gatewayResponse->getRequestIdentifier()) + ->setVatRequestDate($gatewayResponse->getRequestDate()) + ->setVatRequestSuccess($gatewayResponse->getRequestSuccess()) + ->setValidatedVatNumber($customerVatNumber) + ->setValidatedCountryCode($customerCountryCode) + ->save(); + + if ($customerAddressId != $customerDefaultBillingAddressId) { + $groupId = $customerHelper->getCustomerGroupIdBasedOnVatNumber( + $customerCountryCode, $gatewayResponse, $customerInstance->getStore()); + + if ($groupId) { + $quoteInstance->setCustomer($customerInstance->setGroupId($groupId)); + $quoteInstance->setCustomerGroupId($groupId); + } + } + } + + /** + * Add VAT validation request date and identifier to order comments + * + * @param Varien_Event_Observer $observer + * @return null + */ + public function addVatRequestParamsOrderComment(Varien_Event_Observer $observer) + { + /** @var $orderInstance Mage_Sales_Model_Order */ + $orderInstance = $observer->getOrder(); + /** @var $billingAddress Mage_Sales_Model_Order_Address */ + $billingAddress = $orderInstance->getBillingAddress(); + if ($billingAddress instanceof Mage_Sales_Model_Order_Address) { + $vatRequestId = $billingAddress->getVatRequestId(); + $vatRequestDate = $billingAddress->getVatRequestDate(); + if (is_string($vatRequestId) + && !empty($vatRequestId) + && is_string($vatRequestDate) + && !empty($vatRequestDate) + ) { + $orderHistoryComment = Mage::helper('customer')->__('VAT Request Identifier') + . ': ' . $vatRequestId . '
' . Mage::helper('customer')->__('VAT Request Date') + . ': ' . $vatRequestDate; + $orderInstance->addStatusHistoryComment($orderHistoryComment, false); + } + } + } +} diff --git a/app/code/core/Mage/Sales/Model/Order/Config.php b/app/code/core/Mage/Sales/Model/Order/Config.php index a7be7851e5..6a330dcb98 100644 --- a/app/code/core/Mage/Sales/Model/Order/Config.php +++ b/app/code/core/Mage/Sales/Model/Order/Config.php @@ -142,7 +142,11 @@ public function getStates() */ public function getStateStatuses($state, $addLabels = true) { - $key = $state . $addLabels; + if (is_array($state)) { + $key = implode("|", $state) . $addLabels; + } else { + $key = $state . $addLabels; + } if (isset($this->_stateStatuses[$key])) { return $this->_stateStatuses[$key]; } diff --git a/app/code/core/Mage/Sales/Model/Order/Creditmemo.php b/app/code/core/Mage/Sales/Model/Order/Creditmemo.php index b5fd93a6c0..f7ae46a412 100644 --- a/app/code/core/Mage/Sales/Model/Order/Creditmemo.php +++ b/app/code/core/Mage/Sales/Model/Order/Creditmemo.php @@ -160,6 +160,13 @@ class Mage_Sales_Model_Order_Creditmemo extends Mage_Sales_Model_Abstract protected $_order; protected $_comments; + /** + * Calculator instances for delta rounding of prices + * + * @var array + */ + protected $_calculators = array(); + protected $_eventPrefix = 'sales_order_creditmemo'; protected $_eventObject = 'creditmemo'; @@ -285,6 +292,22 @@ public function getItemById($itemId) return false; } + /** + * Returns credit memo item by its order id + * + * @param $orderId + * @return Mage_Sales_Model_Order_Creditmemo_Item|bool + */ + public function getItemByOrderId($orderId) + { + foreach ($this->getItemsCollection() as $item) { + if ($item->getOrderItemId() == $orderId) { + return $item; + } + } + return false; + } + public function addItem(Mage_Sales_Model_Order_Creditmemo_Item $item) { $item->setCreditmemo($this) @@ -309,6 +332,25 @@ public function collectTotals() return $this; } + /** + * Round price considering delta + * + * @param float $price + * @param string $type + * @param bool $negative Indicates if we perform addition (true) or subtraction (false) of rounded value + * @return float + */ + public function roundPrice($price, $type = 'regular', $negative = false) + { + if ($price) { + if (!isset($this->_calculators[$type])) { + $this->_calculators[$type] = Mage::getModel('core/calculator', $this->getStore()); + } + $price = $this->_calculators[$type]->deltaRound($price, $negative); + } + return $price; + } + public function canRefund() { if ($this->getState() != self::STATE_CANCELED @@ -374,9 +416,7 @@ public function refund() $baseAvailableRefund = $this->getOrder()->getBaseTotalPaid()- $this->getOrder()->getBaseTotalRefunded(); Mage::throwException( - Mage::helper('sales')->__('Maximum amount available to refund is %s', - $this->getOrder()->formatBasePrice($baseAvailableRefund) - ) + Mage::helper('sales')->__('Maximum amount available to refund is %s', $this->getOrder()->formatBasePrice($baseAvailableRefund)) ); } $order = $this->getOrder(); diff --git a/app/code/core/Mage/Sales/Model/Order/Creditmemo/Item.php b/app/code/core/Mage/Sales/Model/Order/Creditmemo/Item.php index 3d2397912f..6539a7c837 100644 --- a/app/code/core/Mage/Sales/Model/Order/Creditmemo/Item.php +++ b/app/code/core/Mage/Sales/Model/Order/Creditmemo/Item.php @@ -214,17 +214,18 @@ public function setQty($qty) */ public function register() { - $this->getOrderItem()->setQtyRefunded( - $this->getOrderItem()->getQtyRefunded() + $this->getQty() - ); - $this->getOrderItem()->setTaxRefunded( - $this->getOrderItem()->getTaxRefunded() - + $this->getOrderItem()->getBaseTaxAmount() * $this->getQty() / $this->getOrderItem()->getQtyOrdered() - ); - $this->getOrderItem()->setHiddenTaxRefunded( - $this->getOrderItem()->getHiddenTaxRefunded() - + $this->getOrderItem()->getHiddenTaxAmount() * $this->getQty() / $this->getOrderItem()->getQtyOrdered() - ); + $orderItem = $this->getOrderItem(); + + $orderItem->setQtyRefunded($orderItem->getQtyRefunded() + $this->getQty()); + $orderItem->setTaxRefunded($orderItem->getTaxRefunded() + $this->getTaxAmount()); + $orderItem->setBaseTaxRefunded($orderItem->getBaseTaxRefunded() + $this->getBaseTaxAmount()); + $orderItem->setHiddenTaxRefunded($orderItem->getHiddenTaxRefunded() + $this->getHiddenTaxAmount()); + $orderItem->setBaseHiddenTaxRefunded($orderItem->getBaseHiddenTaxRefunded() + $this->getBaseHiddenTaxAmount()); + $orderItem->setAmountRefunded($orderItem->getAmountRefunded() + $this->getRowTotal()); + $orderItem->setBaseAmountRefunded($orderItem->getBaseAmountRefunded() + $this->getBaseRowTotal()); + $orderItem->setDiscountRefunded($orderItem->getDiscountRefunded() + $this->getDiscountAmount()); + $orderItem->setBaseDiscountRefunded($orderItem->getBaseDiscountRefunded() + $this->getBaseDiscountAmount()); + return $this; } @@ -251,24 +252,31 @@ public function cancel() */ public function calcRowTotal() { - $store = $this->getCreditmemo()->getStore(); - $orderItem = $this->getOrderItem(); - $orderItemQty = $orderItem->getQtyOrdered(); - - $rowTotal = $orderItem->getRowTotal(); - $baseRowTotal = $orderItem->getBaseRowTotal(); - $rowTotalInclTax = $orderItem->getRowTotalInclTax(); - $baseRowTotalInclTax= $orderItem->getBaseRowTotalInclTax(); + $creditmemo = $this->getCreditmemo(); + $orderItem = $this->getOrderItem(); + $orderItemQtyInvoiced = $orderItem->getQtyInvoiced(); - $rowTotal = $rowTotal/$orderItemQty*$this->getQty(); - $baseRowTotal = $baseRowTotal/$orderItemQty*$this->getQty(); + $rowTotal = $orderItem->getRowInvoiced() - $orderItem->getAmountRefunded(); + $baseRowTotal = $orderItem->getBaseRowInvoiced() - $orderItem->getBaseAmountRefunded(); + $rowTotalInclTax = $orderItem->getRowTotalInclTax(); + $baseRowTotalInclTax = $orderItem->getBaseRowTotalInclTax(); - $this->setRowTotal($store->roundPrice($rowTotal)); - $this->setBaseRowTotal($store->roundPrice($baseRowTotal)); + if (!$this->isLast()) { + $availableQty = $orderItemQtyInvoiced - $orderItem->getQtyRefunded(); + $rowTotal = $creditmemo->roundPrice($rowTotal / $availableQty * $this->getQty()); + $baseRowTotal = $creditmemo->roundPrice($baseRowTotal / $availableQty * $this->getQty(), 'base'); + } + $this->setRowTotal($rowTotal); + $this->setBaseRowTotal($baseRowTotal); if ($rowTotalInclTax && $baseRowTotalInclTax) { - $this->setRowTotalInclTax($store->roundPrice($rowTotalInclTax/$orderItemQty*$this->getQty())); - $this->setBaseRowTotalInclTax($store->roundPrice($baseRowTotalInclTax/$orderItemQty*$this->getQty())); + $orderItemQty = $orderItem->getQtyOrdered(); + $this->setRowTotalInclTax( + $creditmemo->roundPrice($rowTotalInclTax / $orderItemQty * $this->getQty(), 'including') + ); + $this->setBaseRowTotalInclTax( + $creditmemo->roundPrice($baseRowTotalInclTax / $orderItemQty * $this->getQty(), 'including_base') + ); } return $this; } diff --git a/app/code/core/Mage/Sales/Model/Order/Creditmemo/Total/Discount.php b/app/code/core/Mage/Sales/Model/Order/Creditmemo/Total/Discount.php index ea2209cf81..bcc37f97b1 100644 --- a/app/code/core/Mage/Sales/Model/Order/Creditmemo/Total/Discount.php +++ b/app/code/core/Mage/Sales/Model/Order/Creditmemo/Total/Discount.php @@ -49,20 +49,30 @@ public function collect(Mage_Sales_Model_Order_Creditmemo $creditmemo) $baseTotalDiscountAmount = $baseTotalDiscountAmount + $baseShippingDiscount; } + /** @var $item Mage_Sales_Model_Order_Invoice_Item */ foreach ($creditmemo->getAllItems() as $item) { - if ($item->getOrderItem()->isDummy()) { + $orderItem = $item->getOrderItem(); + + if ($orderItem->isDummy()) { continue; } - $orderItemDiscount = (float) $item->getOrderItem()->getDiscountAmount(); - $baseOrderItemDiscount = (float) $item->getOrderItem()->getBaseDiscountAmount(); - $orderItemQty = $item->getOrderItem()->getQtyOrdered(); - if ($orderItemDiscount && $orderItemQty) { - $discount = $orderItemDiscount*$item->getQty()/$orderItemQty; - $baseDiscount = $baseOrderItemDiscount*$item->getQty()/$orderItemQty; + $orderItemDiscount = (float) $orderItem->getDiscountInvoiced(); + $baseOrderItemDiscount = (float) $orderItem->getBaseDiscountInvoiced(); + $orderItemQty = $orderItem->getQtyInvoiced(); - $discount = $creditmemo->getStore()->roundPrice($discount); - $baseDiscount = $creditmemo->getStore()->roundPrice($baseDiscount); + if ($orderItemDiscount && $orderItemQty) { + $discount = $orderItemDiscount - $orderItem->getDiscountRefunded(); + $baseDiscount = $baseOrderItemDiscount - $orderItem->getBaseDiscountRefunded(); + if (!$item->isLast()) { + $availableQty = $orderItemQty - $orderItem->getQtyRefunded(); + $discount = $creditmemo->roundPrice( + $discount / $availableQty * $item->getQty(), 'regular', true + ); + $baseDiscount = $creditmemo->roundPrice( + $baseDiscount / $availableQty * $item->getQty(), 'base', true + ); + } $item->setDiscountAmount($discount); $item->setBaseDiscountAmount($baseDiscount); diff --git a/app/code/core/Mage/Sales/Model/Order/Creditmemo/Total/Subtotal.php b/app/code/core/Mage/Sales/Model/Order/Creditmemo/Total/Subtotal.php index d1c80ca878..509a6722df 100644 --- a/app/code/core/Mage/Sales/Model/Order/Creditmemo/Total/Subtotal.php +++ b/app/code/core/Mage/Sales/Model/Order/Creditmemo/Total/Subtotal.php @@ -41,12 +41,12 @@ public function collect(Mage_Sales_Model_Order_Creditmemo $creditmemo) $baseSubtotalInclTax = 0; foreach ($creditmemo->getAllItems() as $item) { - $item->calcRowTotal(); - if ($item->getOrderItem()->isDummy()) { continue; } + $item->calcRowTotal(); + $subtotal += $item->getRowTotal(); $baseSubtotal += $item->getBaseRowTotal(); $subtotalInclTax+= $item->getRowTotalInclTax(); diff --git a/app/code/core/Mage/Sales/Model/Order/Creditmemo/Total/Tax.php b/app/code/core/Mage/Sales/Model/Order/Creditmemo/Total/Tax.php index 7d3432e917..b6fba96335 100644 --- a/app/code/core/Mage/Sales/Model/Order/Creditmemo/Total/Tax.php +++ b/app/code/core/Mage/Sales/Model/Order/Creditmemo/Total/Tax.php @@ -38,49 +38,38 @@ public function collect(Mage_Sales_Model_Order_Creditmemo $creditmemo) $order = $creditmemo->getOrder(); + /** @var $item Mage_Sales_Model_Order_Creditmemo_Item */ foreach ($creditmemo->getAllItems() as $item) { - if ($item->getOrderItem()->isDummy()) { + $orderItem = $item->getOrderItem(); + if ($orderItem->isDummy()) { continue; } - $orderItem = $item->getOrderItem(); - $orderItemTax = $item->getOrderItem()->getTaxAmount(); - $baseOrderItemTax = $item->getOrderItem()->getBaseTaxAmount(); - $orderItemQty = $item->getOrderItem()->getQtyOrdered(); + $orderItemTax = $orderItem->getTaxInvoiced(); + $baseOrderItemTax = $orderItem->getBaseTaxInvoiced(); + $orderItemQty = $orderItem->getQtyInvoiced(); if ($orderItemTax && $orderItemQty) { /** * Check item tax amount */ - - if ($item->isLast()) { - $tax = $orderItemTax - $item->getOrderItem()->getTaxRefunded() - - $item->getOrderItem()->getTaxCanceled(); - $baseTax = $baseOrderItemTax - $item->getOrderItem()->getTaxRefunded() - - $item->getOrderItem()->getTaxCanceled(); - $hiddenTax = $orderItem->getHiddenTaxAmount() - $orderItem->getHiddenTaxRefunded() - - $item->getOrderItem()->getHiddenTaxCanceled(); - $baseHiddenTax = $orderItem->getBaseHiddenTaxAmount() - $orderItem->getBaseHiddenTaxRefunded() - - $item->getOrderItem()->getHiddenTaxCanceled(); - - } - else { - $tax = $orderItemTax*$item->getQty()/$orderItemQty; - $baseTax = $baseOrderItemTax*$item->getQty()/$orderItemQty; - $hiddenTax = $orderItem->getHiddenTaxAmount()*$item->getQty()/$orderItemQty; - $baseHiddenTax = $orderItem->getBaseHiddenTaxAmount()*$item->getQty()/$orderItemQty; - - $tax = $creditmemo->getStore()->roundPrice($tax); - $baseTax = $creditmemo->getStore()->roundPrice($baseTax); - $hiddenTax = $creditmemo->getStore()->roundPrice($hiddenTax); - $baseHiddenTax = $creditmemo->getStore()->roundPrice($baseHiddenTax); + $tax = $orderItemTax - $orderItem->getTaxRefunded(); + $baseTax = $baseOrderItemTax - $orderItem->getTaxRefunded(); + $hiddenTax = $orderItem->getHiddenTaxAmount() - $orderItem->getHiddenTaxRefunded(); + $baseHiddenTax = $orderItem->getBaseHiddenTaxAmount() - $orderItem->getBaseHiddenTaxRefunded(); + if (!$item->isLast()) { + $availableQty = $orderItemQty - $orderItem->getQtyRefunded(); + $tax = $creditmemo->roundPrice($tax / $availableQty * $item->getQty()); + $baseTax = $creditmemo->roundPrice($baseTax / $availableQty * $item->getQty(), 'base'); + $hiddenTax = $creditmemo->roundPrice($hiddenTax / $availableQty * $item->getQty()); + $baseHiddenTax = $creditmemo->roundPrice($baseHiddenTax / $availableQty * $item->getQty(), 'base'); } + $item->setTaxAmount($tax); $item->setBaseTaxAmount($baseTax); $item->setHiddenTaxAmount($hiddenTax); $item->setBaseHiddenTaxAmount($baseHiddenTax); - $totalTax += $tax; $baseTotalTax += $baseTax; $totalHiddenTax += $hiddenTax; @@ -98,12 +87,12 @@ public function collect(Mage_Sales_Model_Order_Creditmemo $creditmemo) $baseTotalHiddenTax += $invoice->getBaseShippingHiddenTaxAmount()*$taxFactor; $shippingHiddenTaxAmount = $invoice->getShippingHiddenTaxAmount()*$taxFactor; $baseShippingHiddenTaxAmount = $invoice->getBaseShippingHiddenTaxAmount()*$taxFactor; - $shippingTaxAmount = $creditmemo->getStore()->roundPrice($shippingTaxAmount); - $baseShippingTaxAmount = $creditmemo->getStore()->roundPrice($baseShippingTaxAmount); - $totalHiddenTax = $creditmemo->getStore()->roundPrice($totalHiddenTax); - $baseTotalHiddenTax = $creditmemo->getStore()->roundPrice($baseTotalHiddenTax); - $shippingHiddenTaxAmount = $creditmemo->getStore()->roundPrice($shippingHiddenTaxAmount); - $baseShippingHiddenTaxAmount = $creditmemo->getStore()->roundPrice($baseShippingHiddenTaxAmount); + $shippingTaxAmount = $creditmemo->roundPrice($shippingTaxAmount); + $baseShippingTaxAmount = $creditmemo->roundPrice($baseShippingTaxAmount, 'base'); + $totalHiddenTax = $creditmemo->roundPrice($totalHiddenTax); + $baseTotalHiddenTax = $creditmemo->roundPrice($baseTotalHiddenTax, 'base'); + $shippingHiddenTaxAmount = $creditmemo->roundPrice($shippingHiddenTaxAmount); + $baseShippingHiddenTaxAmount = $creditmemo->roundPrice($baseShippingHiddenTaxAmount, 'base'); $totalTax += $shippingTaxAmount; $baseTotalTax += $baseShippingTaxAmount; } @@ -130,10 +119,10 @@ public function collect(Mage_Sales_Model_Order_Creditmemo $creditmemo) $baseShippingTaxAmount = $order->getBaseShippingTaxAmount()*$basePart; $shippingHiddenTaxAmount = $order->getShippingHiddenTaxAmount()*$part; $baseShippingHiddenTaxAmount= $order->getBaseShippingHiddenTaxAmount()*$basePart; - $shippingTaxAmount = $creditmemo->getStore()->roundPrice($shippingTaxAmount); - $baseShippingTaxAmount = $creditmemo->getStore()->roundPrice($baseShippingTaxAmount); - $shippingHiddenTaxAmount = $creditmemo->getStore()->roundPrice($shippingHiddenTaxAmount); - $baseShippingHiddenTaxAmount= $creditmemo->getStore()->roundPrice($baseShippingHiddenTaxAmount); + $shippingTaxAmount = $creditmemo->roundPrice($shippingTaxAmount); + $baseShippingTaxAmount = $creditmemo->roundPrice($baseShippingTaxAmount, 'base'); + $shippingHiddenTaxAmount = $creditmemo->roundPrice($shippingHiddenTaxAmount); + $baseShippingHiddenTaxAmount= $creditmemo->roundPrice($baseShippingHiddenTaxAmount, 'base'); } elseif ($shippingDelta == $creditmemo->getBaseShippingAmount()) { $shippingTaxAmount = $order->getShippingTaxAmount() - $order->getShippingTaxRefunded(); $baseShippingTaxAmount = $order->getBaseShippingTaxAmount() - $order->getBaseShippingTaxRefunded(); diff --git a/app/code/core/Mage/Sales/Model/Order/Invoice.php b/app/code/core/Mage/Sales/Model/Order/Invoice.php index a031c95fc8..7e3f9a49b1 100644 --- a/app/code/core/Mage/Sales/Model/Order/Invoice.php +++ b/app/code/core/Mage/Sales/Model/Order/Invoice.php @@ -160,6 +160,13 @@ class Mage_Sales_Model_Order_Invoice extends Mage_Sales_Model_Abstract protected $_comments; protected $_order; + /** + * Calculator instances for delta rounding of prices + * + * @var array + */ + protected $_rounders = array(); + protected $_saveBeforeDestruct = false; protected $_eventPrefix = 'sales_order_invoice'; @@ -495,6 +502,25 @@ public function collectTotals() return $this; } + /** + * Round price considering delta + * + * @param float $price + * @param string $type + * @param bool $negative Indicates if we perform addition (true) or subtraction (false) of rounded value + * @return float + */ + public function roundPrice($price, $type = 'regular', $negative = false) + { + if ($price) { + if (!isset($this->_rounders[$type])) { + $this->_rounders[$type] = Mage::getModel('core/calculator', $this->getStore()); + } + $price = $this->_rounders[$type]->deltaRound($price, $negative); + } + return $price; + } + /** * Get invoice items collection * diff --git a/app/code/core/Mage/Sales/Model/Order/Invoice/Item.php b/app/code/core/Mage/Sales/Model/Order/Invoice/Item.php index aa23d32c67..6e27f7e044 100644 --- a/app/code/core/Mage/Sales/Model/Order/Invoice/Item.php +++ b/app/code/core/Mage/Sales/Model/Order/Invoice/Item.php @@ -264,24 +264,27 @@ public function cancel() */ public function calcRowTotal() { - $store = $this->getInvoice()->getStore(); + $invoice = $this->getInvoice(); $orderItem = $this->getOrderItem(); $orderItemQty = $orderItem->getQtyOrdered(); - $rowTotal = $orderItem->getRowTotal(); - $baseRowTotal = $orderItem->getBaseRowTotal(); - $rowTotalInclTax = $orderItem->getRowTotalInclTax(); - $baseRowTotalInclTax= $orderItem->getBaseRowTotalInclTax(); + $rowTotal = $orderItem->getRowTotal() - $orderItem->getRowInvoiced(); + $baseRowTotal = $orderItem->getBaseRowTotal() - $orderItem->getBaseRowInvoiced(); + $rowTotalInclTax = $orderItem->getRowTotalInclTax(); + $baseRowTotalInclTax = $orderItem->getBaseRowTotalInclTax(); - $rowTotal = $rowTotal/$orderItemQty*$this->getQty(); - $baseRowTotal = $baseRowTotal/$orderItemQty*$this->getQty(); + if (!$this->isLast()) { + $availableQty = $orderItemQty - $orderItem->getQtyInvoiced(); + $rowTotal = $invoice->roundPrice($rowTotal / $availableQty * $this->getQty()); + $baseRowTotal = $invoice->roundPrice($baseRowTotal / $availableQty * $this->getQty(), 'base'); + } - $this->setRowTotal($store->roundPrice($rowTotal)); - $this->setBaseRowTotal($store->roundPrice($baseRowTotal)); + $this->setRowTotal($rowTotal); + $this->setBaseRowTotal($baseRowTotal); if ($rowTotalInclTax && $baseRowTotalInclTax) { - $this->setRowTotalInclTax($store->roundPrice($rowTotalInclTax/$orderItemQty*$this->getQty())); - $this->setBaseRowTotalInclTax($store->roundPrice($baseRowTotalInclTax/$orderItemQty*$this->getQty())); + $this->setRowTotalInclTax($invoice->roundPrice($rowTotalInclTax / $orderItemQty * $this->getQty(), 'including')); + $this->setBaseRowTotalInclTax($invoice->roundPrice($baseRowTotalInclTax / $orderItemQty * $this->getQty(), 'including_base')); } return $this; } diff --git a/app/code/core/Mage/Sales/Model/Order/Invoice/Total/Discount.php b/app/code/core/Mage/Sales/Model/Order/Invoice/Total/Discount.php index 4d6f531b3c..7d39444a28 100644 --- a/app/code/core/Mage/Sales/Model/Order/Invoice/Total/Discount.php +++ b/app/code/core/Mage/Sales/Model/Order/Invoice/Total/Discount.php @@ -52,6 +52,7 @@ public function collect(Mage_Sales_Model_Order_Invoice $invoice) $baseTotalDiscountAmount = $baseTotalDiscountAmount + $invoice->getOrder()->getBaseShippingDiscountAmount(); } + /** @var $item Mage_Sales_Model_Order_Invoice_Item */ foreach ($invoice->getAllItems() as $item) { if ($item->getOrderItem()->isDummy()) { continue; @@ -65,16 +66,12 @@ public function collect(Mage_Sales_Model_Order_Invoice $invoice) /** * Resolve rounding problems */ - if ($item->isLast()) { - $discount = $orderItemDiscount - $orderItem->getDiscountInvoiced(); - $baseDiscount = $baseOrderItemDiscount - $orderItem->getBaseDiscountInvoiced(); - } - else { - $discount = $orderItemDiscount*$item->getQty()/$orderItemQty; - $baseDiscount = $baseOrderItemDiscount*$item->getQty()/$orderItemQty; - - $discount = $invoice->getStore()->roundPrice($discount); - $baseDiscount = $invoice->getStore()->roundPrice($baseDiscount); + $discount = $orderItemDiscount - $orderItem->getDiscountInvoiced(); + $baseDiscount = $baseOrderItemDiscount - $orderItem->getBaseDiscountInvoiced(); + if (!$item->isLast()) { + $activeQty = $orderItemQty - $orderItem->getQtyInvoiced(); + $discount = $invoice->roundPrice($discount / $activeQty * $item->getQty(), 'regular', true); + $baseDiscount = $invoice->roundPrice($baseDiscount / $activeQty * $item->getQty(), 'base', true); } $item->setDiscountAmount($discount); diff --git a/app/code/core/Mage/Sales/Model/Order/Invoice/Total/Subtotal.php b/app/code/core/Mage/Sales/Model/Order/Invoice/Total/Subtotal.php index 0ed5d0affa..e61d1c393f 100644 --- a/app/code/core/Mage/Sales/Model/Order/Invoice/Total/Subtotal.php +++ b/app/code/core/Mage/Sales/Model/Order/Invoice/Total/Subtotal.php @@ -43,12 +43,12 @@ public function collect(Mage_Sales_Model_Order_Invoice $invoice) $order = $invoice->getOrder(); foreach ($invoice->getAllItems() as $item) { - $item->calcRowTotal(); - if ($item->getOrderItem()->isDummy()) { continue; } + $item->calcRowTotal(); + $subtotal += $item->getRowTotal(); $baseSubtotal += $item->getBaseRowTotal(); $subtotalInclTax+= $item->getRowTotalInclTax(); @@ -56,11 +56,30 @@ public function collect(Mage_Sales_Model_Order_Invoice $invoice) } $allowedSubtotal = $order->getSubtotal() - $order->getSubtotalInvoiced(); - $baseAllowedSubtotal = $order->getBaseSubtotal() -$order->getBaseSubtotalInvoiced(); + $baseAllowedSubtotal = $order->getBaseSubtotal() - $order->getBaseSubtotalInvoiced(); $allowedSubtotalInclTax = $allowedSubtotal + $order->getHiddenTaxAmount() - + $order->getTaxAmount() - $order->getTaxInvoiced() - $order->getShippingTaxAmount(); + + $order->getTaxAmount() - $order->getTaxInvoiced() - $order->getHiddenTaxInvoiced(); $baseAllowedSubtotalInclTax = $baseAllowedSubtotal + $order->getBaseHiddenTaxAmount() - + $order->getBaseTaxAmount() - $order->getBaseTaxInvoiced() - $order->getBaseShippingTaxAmount(); + + $order->getBaseTaxAmount() - $order->getBaseTaxInvoiced() - $order->getBaseHiddenTaxInvoiced(); + + /** + * Check if shipping tax calculation is included to current invoice. + */ + $includeShippingTax = true; + foreach ($invoice->getOrder()->getInvoiceCollection() as $previousInvoice) { + if ($previousInvoice->getShippingAmount() && !$previousInvoice->isCanceled()) { + $includeShippingTax = false; + break; + } + } + + if ($includeShippingTax) { + $allowedSubtotalInclTax -= $order->getShippingTaxAmount(); + $baseAllowedSubtotalInclTax -= $order->getBaseShippingTaxAmount(); + } else { + $allowedSubtotalInclTax += $order->getShippingHiddenTaxAmount(); + $baseAllowedSubtotalInclTax += $order->getBaseShippingHiddenTaxAmount(); + } if ($invoice->isLast()) { $subtotal = $allowedSubtotal; diff --git a/app/code/core/Mage/Sales/Model/Order/Invoice/Total/Tax.php b/app/code/core/Mage/Sales/Model/Order/Invoice/Total/Tax.php index 03e1c975b6..8d5574f508 100644 --- a/app/code/core/Mage/Sales/Model/Order/Invoice/Total/Tax.php +++ b/app/code/core/Mage/Sales/Model/Order/Invoice/Total/Tax.php @@ -31,6 +31,7 @@ class Mage_Sales_Model_Order_Invoice_Total_Tax extends Mage_Sales_Model_Order_In * Collect invoice tax amount * * @param Mage_Sales_Model_Order_Invoice $invoice + * @return Mage_Sales_Model_Order_Invoice_Total_Tax */ public function collect(Mage_Sales_Model_Order_Invoice $invoice) { @@ -40,6 +41,8 @@ public function collect(Mage_Sales_Model_Order_Invoice $invoice) $baseTotalHiddenTax = 0; $order = $invoice->getOrder(); + + /** @var $item Mage_Sales_Model_Order_Invoice_Item */ foreach ($invoice->getAllItems() as $item) { $orderItem = $item->getOrderItem(); $orderItemQty = $orderItem->getQtyOrdered(); @@ -52,21 +55,16 @@ public function collect(Mage_Sales_Model_Order_Invoice $invoice) /** * Resolve rounding problems */ - if ($item->isLast()) { - $tax = $orderItem->getTaxAmount() - $orderItem->getTaxInvoiced(); - $baseTax = $orderItem->getBaseTaxAmount() - $orderItem->getBaseTaxInvoiced(); - $hiddenTax = $orderItem->getHiddenTaxAmount() - $orderItem->getHiddenTaxInvoiced(); - $baseHiddenTax = $orderItem->getBaseHiddenTaxAmount() - $orderItem->getBaseHiddenTaxInvoiced(); - } else { - $tax = $orderItem->getTaxAmount()*$item->getQty()/$orderItemQty; - $baseTax = $orderItem->getBaseTaxAmount()*$item->getQty()/$orderItemQty; - $hiddenTax = $orderItem->getHiddenTaxAmount()*$item->getQty()/$orderItemQty; - $baseHiddenTax = $orderItem->getBaseHiddenTaxAmount()*$item->getQty()/$orderItemQty; - - $tax = $invoice->getStore()->roundPrice($tax); - $baseTax = $invoice->getStore()->roundPrice($baseTax); - $hiddenTax = $invoice->getStore()->roundPrice($hiddenTax); - $baseHiddenTax = $invoice->getStore()->roundPrice($baseHiddenTax); + $tax = $orderItem->getTaxAmount() - $orderItem->getTaxInvoiced(); + $baseTax = $orderItem->getBaseTaxAmount() - $orderItem->getBaseTaxInvoiced(); + $hiddenTax = $orderItem->getHiddenTaxAmount() - $orderItem->getHiddenTaxInvoiced(); + $baseHiddenTax = $orderItem->getBaseHiddenTaxAmount() - $orderItem->getBaseHiddenTaxInvoiced(); + if (!$item->isLast()) { + $availableQty = $orderItemQty - $orderItem->getQtyInvoiced(); + $tax = $invoice->roundPrice($tax / $availableQty * $item->getQty()); + $baseTax = $invoice->roundPrice($baseTax / $availableQty * $item->getQty(), 'base'); + $hiddenTax = $invoice->roundPrice($hiddenTax / $availableQty * $item->getQty()); + $baseHiddenTax = $invoice->roundPrice($baseHiddenTax / $availableQty * $item->getQty(), 'base'); } $item->setTaxAmount($tax); @@ -124,12 +122,13 @@ public function collect(Mage_Sales_Model_Order_Invoice $invoice) /** * Check if shipping tax calculation can be included to current invoice * @param Mage_Sales_Model_Order_Invoice $invoice + * @return boolean */ protected function _canIncludeShipping($invoice) { $includeShippingTax = true; /** - * Check shipping amount in previus invoices + * Check shipping amount in previous invoices */ foreach ($invoice->getOrder()->getInvoiceCollection() as $previusInvoice) { if ($previusInvoice->getShippingAmount() && !$previusInvoice->isCanceled()) { diff --git a/app/code/core/Mage/Sales/Model/Order/Item.php b/app/code/core/Mage/Sales/Model/Order/Item.php index 85f28ff6b6..7e3692ef26 100644 --- a/app/code/core/Mage/Sales/Model/Order/Item.php +++ b/app/code/core/Mage/Sales/Model/Order/Item.php @@ -180,6 +180,12 @@ * @method Mage_Sales_Model_Order_Item setHiddenTaxCanceled(float $value) * @method float getTaxRefunded() * @method Mage_Sales_Model_Order_Item setTaxRefunded(float $value) + * @method float getBaseTaxRefunded() + * @method Mage_Sales_Model_Order_Item setBaseTaxRefunded(float $value) + * @method float getDiscountRefunded() + * @method Mage_Sales_Model_Order_Item setDiscountRefunded(float $value) + * @method float getBaseDiscountRefunded() + * @method Mage_Sales_Model_Order_Item setBaseDiscountRefunded(float $value) * * @category Mage * @package Mage_Sales @@ -427,7 +433,7 @@ public function getStatusId() if (!$invoiced && !$shipped && !$refunded && !$canceled && !$backordered) { return self::STATUS_PENDING; } - if ($shipped && !$invoiced && ($actuallyOrdered == $shipped)) { + if ($shipped && $invoiced && ($actuallyOrdered == $shipped)) { return self::STATUS_SHIPPED; } diff --git a/app/code/core/Mage/Sales/Model/Order/Payment.php b/app/code/core/Mage/Sales/Model/Order/Payment.php index a3530f8a54..6a96243724 100644 --- a/app/code/core/Mage/Sales/Model/Order/Payment.php +++ b/app/code/core/Mage/Sales/Model/Order/Payment.php @@ -331,7 +331,7 @@ public function place() $orderIsNotified = $stateObject->getIsNotified(); } else { $orderStatus = $methodInstance->getConfigData('order_status'); - if (!$orderStatus || $order->getIsVirtual()) { + if (!$orderStatus) { $orderStatus = $order->getConfig()->getStateDefaultStatus($orderState); } } @@ -1024,11 +1024,6 @@ protected function _authorize($isOnline, $amount) if ($isOnline) { // invoke authorization on gateway $this->getMethodInstance()->setStore($order->getStoreId())->authorize($this, $amount); - } else { - $message = Mage::helper('sales')->__( - 'Registered notification about authorized amount of %s.', - $this->_formatPrice($amount) - ); } // similar logic of "payment review" order as in capturing diff --git a/app/code/core/Mage/Sales/Model/Order/Pdf/Abstract.php b/app/code/core/Mage/Sales/Model/Order/Pdf/Abstract.php index 04f85c9a16..43a134ef15 100644 --- a/app/code/core/Mage/Sales/Model/Order/Pdf/Abstract.php +++ b/app/code/core/Mage/Sales/Model/Order/Pdf/Abstract.php @@ -577,7 +577,7 @@ protected function _drawItem(Varien_Object $item, Zend_Pdf_Page $page, Mage_Sale protected function _setFontRegular($object, $size = 7) { - $font = Zend_Pdf_Font::fontWithPath(Mage::getBaseDir() . '/lib/LinLibertineFont/LinLibertineC_Re-2.8.0.ttf'); + $font = Zend_Pdf_Font::fontWithPath(Mage::getBaseDir() . '/lib/LinLibertineFont/LinLibertine_Re-4.4.1.ttf'); $object->setFont($font, $size); return $font; } @@ -728,6 +728,10 @@ public function drawLineBlocks(Zend_Pdf_Page $page, array $draw, array $pageSett $lineSpacing = !empty($column['height']) ? $column['height'] : $height; $top = 0; foreach ($column['text'] as $part) { + if ($this->y - $lineSpacing < 15) { + $page = $this->newPage($pageSettings); + } + $feed = $column['feed']; $textAlign = empty($column['align']) ? 'left' : $column['align']; $width = empty($column['width']) ? 0 : $column['width']; diff --git a/app/code/core/Mage/Sales/Model/Order/Pdf/Items/Abstract.php b/app/code/core/Mage/Sales/Model/Order/Pdf/Items/Abstract.php index f6b0aa972c..c6fbc952a1 100644 --- a/app/code/core/Mage/Sales/Model/Order/Pdf/Items/Abstract.php +++ b/app/code/core/Mage/Sales/Model/Order/Pdf/Items/Abstract.php @@ -259,7 +259,7 @@ public function getItemOptions() { protected function _setFontRegular($size = 7) { - $font = Zend_Pdf_Font::fontWithPath(Mage::getBaseDir() . '/lib/LinLibertineFont/LinLibertineC_Re-2.8.0.ttf'); + $font = Zend_Pdf_Font::fontWithPath(Mage::getBaseDir() . '/lib/LinLibertineFont/LinLibertine_Re-4.4.1.ttf'); $this->getPage()->setFont($font, $size); return $font; } diff --git a/app/code/core/Mage/Sales/Model/Quote.php b/app/code/core/Mage/Sales/Model/Quote.php index 11461cc4c9..0ff6ee2c30 100644 --- a/app/code/core/Mage/Sales/Model/Quote.php +++ b/app/code/core/Mage/Sales/Model/Quote.php @@ -466,9 +466,10 @@ public function getCustomer() */ public function getCustomerGroupId() { - if ($this->getCustomerId()) { - return ($this->getData('customer_group_id')) ? $this->getData('customer_group_id') - : $this->getCustomer()->getGroupId(); + if ($this->hasData('customer_group_id')) { + return $this->getData('customer_group_id'); + } else if ($this->getCustomerId()) { + return $this->getCustomer()->getGroupId(); } else { return Mage_Customer_Model_Group::NOT_LOGGED_IN_ID; } @@ -816,6 +817,19 @@ public function removeItem($itemId) return $this; } + /** + * Mark all quote items as deleted (empty quote) + * + * @return Mage_Sales_Model_Quote + */ + public function removeAllItems() + { + foreach ($this->getItemsCollection() as $item) { + $item->isDeleted(true); + } + return $this; + } + /** * Adding new item to quote * @@ -1209,12 +1223,7 @@ public function collectTotals() if ($this->getTotalsCollectedFlag()) { return $this; } - Mage::dispatchEvent( - $this->_eventPrefix . '_collect_totals_before', - array( - $this->_eventObject=>$this - ) - ); + Mage::dispatchEvent($this->_eventPrefix . '_collect_totals_before', array($this->_eventObject => $this)); $this->setSubtotal(0); $this->setBaseSubtotal(0); @@ -1279,10 +1288,7 @@ public function collectTotals() $this->setData('trigger_recollect', 0); $this->_validateCouponCode(); - Mage::dispatchEvent( - $this->_eventPrefix . '_collect_totals_after', - array($this->_eventObject => $this) - ); + Mage::dispatchEvent($this->_eventPrefix . '_collect_totals_after', array($this->_eventObject => $this)); $this->setTotalsCollectedFlag(true); return $this; @@ -1350,6 +1356,11 @@ public function addMessage($message, $index = 'error') return $this; } + /** + * Retrieve current quote messages + * + * @return array + */ public function getMessages() { $messages = $this->getData('messages'); @@ -1360,6 +1371,23 @@ public function getMessages() return $messages; } + /** + * Retrieve current quote errors + * + * @return array + */ + public function getErrors() + { + $errors = array(); + foreach ($this->getMessages() as $message) { + /* @var $error Mage_Core_Model_Message_Abstract */ + if ($message->getType() == Mage_Core_Model_Message::ERROR) { + array_push($errors, $message); + } + } + return $errors; + } + /** * Sets flag, whether this quote has some error associated with it. * @@ -1763,7 +1791,7 @@ public function prepareRecurringPaymentProfiles() protected function _validateCouponCode() { $code = $this->_getData('coupon_code'); - if ($code) { + if (strlen($code)) { $addressHasCoupon = false; $addresses = $this->getAllAddresses(); if (count($addresses)>0) { diff --git a/app/code/core/Mage/Sales/Model/Quote/Address.php b/app/code/core/Mage/Sales/Model/Quote/Address.php index d1099ac7fc..2764d48de8 100644 --- a/app/code/core/Mage/Sales/Model/Quote/Address.php +++ b/app/code/core/Mage/Sales/Model/Quote/Address.php @@ -874,6 +874,8 @@ public function requestShippingRates(Mage_Sales_Model_Quote_Item_Abstract $item $request->setPackageCurrency($this->getQuote()->getStore()->getCurrentCurrency()); $request->setLimitCarrier($this->getLimitCarrier()); + $request->setBaseSubtotalInclTax($this->getBaseSubtotalInclTax()); + $result = Mage::getModel('shipping/shipping')->collectRates($request)->getResult(); $found = false; diff --git a/app/code/core/Mage/Sales/Model/Quote/Address/Total/Discount.php b/app/code/core/Mage/Sales/Model/Quote/Address/Total/Discount.php index 3a2e77b197..5ba7816a67 100644 --- a/app/code/core/Mage/Sales/Model/Quote/Address/Total/Discount.php +++ b/app/code/core/Mage/Sales/Model/Quote/Address/Total/Discount.php @@ -139,7 +139,8 @@ public function fetch(Mage_Sales_Model_Quote_Address $address) $amount = $address->getDiscountAmount(); if ($amount!=0) { $title = Mage::helper('sales')->__('Discount'); - if ($code = $address->getCouponCode()) { + $code = $address->getCouponCode(); + if (strlen($code)) { $title = Mage::helper('sales')->__('Discount (%s)', $code); } $address->addTotal(array( diff --git a/app/code/core/Mage/Sales/Model/Quote/Item/Abstract.php b/app/code/core/Mage/Sales/Model/Quote/Item/Abstract.php index 202970f56b..38695cfa4c 100644 --- a/app/code/core/Mage/Sales/Model/Quote/Item/Abstract.php +++ b/app/code/core/Mage/Sales/Model/Quote/Item/Abstract.php @@ -308,8 +308,9 @@ public function getTotalQty() public function calcRowTotal() { $qty = $this->getTotalQty(); - $total = $this->getCalculationPriceOriginal()*$qty; - $baseTotal = $this->getBaseCalculationPriceOriginal()*$qty; + // Round unit price before multiplying to prevent losing 1 cent on subtotal + $total = $this->getStore()->roundPrice($this->getCalculationPriceOriginal()) * $qty; + $baseTotal = $this->getBaseCalculationPriceOriginal() * $qty; $this->setRowTotal($this->getStore()->roundPrice($total)); $this->setBaseRowTotal($this->getStore()->roundPrice($baseTotal)); diff --git a/app/code/core/Mage/Sales/Model/Resource/Helper/Interface.php b/app/code/core/Mage/Sales/Model/Resource/Helper/Interface.php new file mode 100644 index 0000000000..1f116f2d76 --- /dev/null +++ b/app/code/core/Mage/Sales/Model/Resource/Helper/Interface.php @@ -0,0 +1,49 @@ + + */ +interface Mage_Sales_Model_Resource_Helper_Interface +{ + /** + * Update rating position + * + * @param string $aggregation One of Mage_Sales_Model_Resource_Report_Bestsellers::AGGREGATION_XXX constants + * @param array $aggregationAliases + * @param string $mainTable + * @param string $aggregationTable + * @return Mage_Sales_Model_Resource_Helper_Abstract + */ + public function getBestsellersReportUpdateRatingPos($aggregation, $aggregationAliases, + $mainTable, $aggregationTable + ); +} diff --git a/app/code/core/Mage/Sales/Model/Resource/Helper/Mysql4.php b/app/code/core/Mage/Sales/Model/Resource/Helper/Mysql4.php index e52f9b3af3..ed27a4ba7f 100644 --- a/app/code/core/Mage/Sales/Model/Resource/Helper/Mysql4.php +++ b/app/code/core/Mage/Sales/Model/Resource/Helper/Mysql4.php @@ -33,6 +33,7 @@ * @author Magento Core Team */ class Mage_Sales_Model_Resource_Helper_Mysql4 extends Mage_Core_Model_Resource_Helper_Mysql4 + implements Mage_Sales_Model_Resource_Helper_Interface { /** * Update rating position @@ -46,55 +47,20 @@ class Mage_Sales_Model_Resource_Helper_Mysql4 extends Mage_Core_Model_Resource_H public function getBestsellersReportUpdateRatingPos($aggregation, $aggregationAliases, $mainTable, $aggregationTable ) { - $adapter = $this->_getWriteAdapter(); - $periodSubSelect = $adapter->select(); - $ratingSubSelect = $adapter->select(); - $ratingSelect = $adapter->select(); + /** @var $reportsResourceHelper Mage_Reports_Model_Resource_Helper_Interface */ + $reportsResourceHelper = Mage::getResourceHelper('reports'); - $periodCol = 't.period'; if ($aggregation == $aggregationAliases['monthly']) { - $periodCol = $adapter->getDateFormatSql('t.period', '%Y-%m-01'); + $reportsResourceHelper + ->updateReportRatingPos('month', 'qty_ordered', $mainTable, $aggregationTable); } elseif ($aggregation == $aggregationAliases['yearly']) { - $periodCol = $adapter->getDateFormatSql('t.period', '%Y-01-01'); + $reportsResourceHelper + ->updateReportRatingPos('year', 'qty_ordered', $mainTable, $aggregationTable); + } else { + $reportsResourceHelper + ->updateReportRatingPos('day', 'qty_ordered', $mainTable, $aggregationTable); } - $columns = array( - 'period' => 't.period', - 'store_id' => 't.store_id', - 'product_id' => 't.product_id', - 'product_name' => 't.product_name', - 'product_price' => 't.product_price', - ); - - if ($aggregation == $aggregationAliases['daily']) { - $columns['id'] = 't.id'; // to speed-up insert on duplicate key update - } - - $cols = array_keys($columns); - $cols['total_qty_ordered'] = new Zend_Db_Expr('SUM(t.qty_ordered)'); - $periodSubSelect->from(array('t' => $mainTable), $cols) - ->group(array('t.store_id', $periodCol, 't.product_id')) - ->order(array('t.store_id', $periodCol, 'total_qty_ordered DESC')); - - $cols = $columns; - $cols['qty_ordered'] = 't.total_qty_ordered'; - $cols['rating_pos'] = new Zend_Db_Expr( - "(@pos := IF(t.`store_id` <> @prevStoreId OR {$periodCol} <> @prevPeriod, 1, @pos+1))"); - $cols['prevStoreId'] = new Zend_Db_Expr('(@prevStoreId := t.`store_id`)'); - $cols['prevPeriod'] = new Zend_Db_Expr("(@prevPeriod := {$periodCol})"); - $ratingSubSelect->from($periodSubSelect, $cols); - - $cols = $columns; - $cols['period'] = $periodCol; // important! - $cols['qty_ordered'] = 't.qty_ordered'; - $cols['rating_pos'] = 't.rating_pos'; - $ratingSelect->from($ratingSubSelect, $cols); - - $sql = $ratingSelect->insertFromSelect($aggregationTable, array_keys($cols)); - $adapter->query("SET @pos = 0, @prevStoreId = -1, @prevPeriod = '0000-00-00'"); - - $adapter->query($sql); - return $this; } } diff --git a/app/code/core/Mage/Sales/Model/Resource/Quote/Address/Attribute/Frontend/Discount.php b/app/code/core/Mage/Sales/Model/Resource/Quote/Address/Attribute/Frontend/Discount.php index c2363b0697..577ca0bc1b 100755 --- a/app/code/core/Mage/Sales/Model/Resource/Quote/Address/Attribute/Frontend/Discount.php +++ b/app/code/core/Mage/Sales/Model/Resource/Quote/Address/Attribute/Frontend/Discount.php @@ -46,8 +46,9 @@ public function fetchTotals(Mage_Sales_Model_Quote_Address $address) $amount = $address->getDiscountAmount(); if ($amount != 0) { $title = Mage::helper('sales')->__('Discount'); - if ($address->getQuote()->getCouponCode()) { - $title .= sprintf(' (%s)', $address->getQuote()->getCouponCode()); + $couponCode = $address->getQuote()->getCouponCode(); + if (strlen($couponCode)) { + $title .= sprintf(' (%s)', $couponCode); } $address->addTotal(array( 'code' => 'discount', diff --git a/app/code/core/Mage/Sales/Model/Resource/Report/Bestsellers/Collection.php b/app/code/core/Mage/Sales/Model/Resource/Report/Bestsellers/Collection.php index c6b8578320..d5b445480b 100755 --- a/app/code/core/Mage/Sales/Model/Resource/Report/Bestsellers/Collection.php +++ b/app/code/core/Mage/Sales/Model/Resource/Report/Bestsellers/Collection.php @@ -110,7 +110,7 @@ protected function _makeBoundarySelect($from, $to) ->where('period >= ?', $from) ->where('period <= ?', $to) ->group('product_id') - ->order('qty_ordered') + ->order('qty_ordered DESC') ->limit($this->_ratingLimit); $this->_applyStoresFilterToSelect($sel); @@ -218,7 +218,6 @@ protected function _beforeLoad() parent::_beforeLoad(); $this->_applyStoresFilter(); - $this->_applyDateRangeFilter(); if ($this->_period) { // @@ -342,6 +341,8 @@ protected function _beforeLoad() } + $this->_applyDateRangeFilter(); + // add unions to select if ($selectUnions) { $unionParts = array(); diff --git a/app/code/core/Mage/Sales/Model/Resource/Report/Collection/Abstract.php b/app/code/core/Mage/Sales/Model/Resource/Report/Collection/Abstract.php index 8c19fe7d68..3b73b63db1 100755 --- a/app/code/core/Mage/Sales/Model/Resource/Report/Collection/Abstract.php +++ b/app/code/core/Mage/Sales/Model/Resource/Report/Collection/Abstract.php @@ -32,22 +32,9 @@ * @package Mage_Sales * @author Magento Core Team */ -class Mage_Sales_Model_Resource_Report_Collection_Abstract extends Mage_Core_Model_Resource_Db_Collection_Abstract +class Mage_Sales_Model_Resource_Report_Collection_Abstract + extends Mage_Reports_Model_Resource_Report_Collection_Abstract { - /** - * From date - * - * @var string - */ - protected $_from = null; - - /** - * To date - * - * @var string - */ - protected $_to = null; - /** * Order status * @@ -55,169 +42,6 @@ class Mage_Sales_Model_Resource_Report_Collection_Abstract extends Mage_Core_Mod */ protected $_orderStatus = null; - /** - * Period - * - * @var string - */ - protected $_period = null; - - /** - * Store ids - * - * @var int|array - */ - protected $_storesIds = 0; - - /** - * Does filters should be applied - * - * @var bool - */ - protected $_applyFilters = true; - - /** - * Is totals - * - * @var bool - */ - protected $_isTotals = false; - - /** - * Is subtotals - * - * @var bool - */ - protected $_isSubTotals = false; - - /** - * Aggregated columns - * - * @var array - */ - protected $_aggregatedColumns = array(); - - /** - * Set array of columns that should be aggregated - * - * @param array $columns - * @return Mage_Sales_Model_Resource_Report_Collection_Abstract - */ - public function setAggregatedColumns(array $columns) - { - $this->_aggregatedColumns = $columns; - return $this; - } - - /** - * Retrieve array of columns that should be aggregated - * - * @return array - */ - public function getAggregatedColumns() - { - return $this->_aggregatedColumns; - } - - /** - * Set date range - * - * @param mixed $from - * @param mixed $to - * @return Mage_Sales_Model_Resource_Report_Collection_Abstract - */ - public function setDateRange($from = null, $to = null) - { - $this->_from = $from; - $this->_to = $to; - return $this; - } - - /** - * Set period - * - * @param string $period - * @return Mage_Sales_Model_Resource_Report_Collection_Abstract - */ - public function setPeriod($period) - { - $this->_period = $period; - return $this; - } - - /** - * Apply date range filter - * - * @return Mage_Sales_Model_Resource_Report_Collection_Abstract - */ - protected function _applyDateRangeFilter() - { - // Remember that field PERIOD is a DATE(YYYY-MM-DD) in all databases including Oracle - if ($this->_from !== null) { - $this->getSelect()->where('period >= ?', $this->_from); - } - if ($this->_to !== null) { - $this->getSelect()->where('period <= ?', $this->_to); - } - - return $this; - } - - /** - * Set store ids - * - * @param mixed $storeIds (null, int|string, array, array may contain null) - * @return Mage_Sales_Model_Resource_Report_Collection_Abstract - */ - public function addStoreFilter($storeIds) - { - $this->_storesIds = $storeIds; - return $this; - } - - /** - * Apply stores filter to select object - * - * @param Zend_Db_Select $select - * @return Mage_Sales_Model_Resource_Report_Collection_Abstract - */ - protected function _applyStoresFilterToSelect(Zend_Db_Select $select) - { - $nullCheck = false; - $storeIds = $this->_storesIds; - - if (!is_array($storeIds)) { - $storeIds = array($storeIds); - } - - $storeIds = array_unique($storeIds); - - if ($index = array_search(null, $storeIds)) { - unset($storeIds[$index]); - $nullCheck = true; - } - - $storeIds[0] = ($storeIds[0] == '') ? 0 : $storeIds[0]; - - if ($nullCheck) { - $select->where('store_id IN(?) OR store_id IS NULL', $storeIds); - } else { - $select->where('store_id IN(?)', $storeIds); - } - - return $this; - } - - /** - * Apply stores filter - * - * @return Mage_Sales_Model_Resource_Report_Collection_Abstract - */ - protected function _applyStoresFilter() - { - return $this->_applyStoresFilterToSelect($this->getSelect()); - } - /** * Set status filter * @@ -249,66 +73,12 @@ protected function _applyOrderStatusFilter() } /** - * Set apply filters flag - * - * @param boolean $flag - * @return Mage_Sales_Model_Resource_Report_Collection_Abstract - */ - public function setApplyFilters($flag) - { - $this->_applyFilters = $flag; - return $this; - } - - /** - * Getter/Setter for isTotals - * - * @param null|boolean $flag - * @return Mage_Sales_Model_Resource_Report_Collection_Abstract - */ - public function isTotals($flag = null) - { - if (is_null($flag)) { - return $this->_isTotals; - } - $this->_isTotals = $flag; - return $this; - } - - /** - * Getter/Setter for isSubTotals + * Order status filter is custom for this collection * - * @param null|boolean $flag * @return Mage_Sales_Model_Resource_Report_Collection_Abstract */ - public function isSubTotals($flag = null) + protected function _applyCustomFilter() { - if (is_null($flag)) { - return $this->_isSubTotals; - } - $this->_isSubTotals = $flag; - return $this; - } - - /** - * Load data - * Redeclare parent load method just for adding method _beforeLoad - * - * @param bool $printQuery - * @param bool $logQuery - * @return Mage_Sales_Model_Resource_Report_Collection_Abstract - */ - public function load($printQuery = false, $logQuery = false) - { - if ($this->isLoaded()) { - return $this; - } - $this->_initSelect(); - if ($this->_applyFilters) { - $this->_applyDateRangeFilter(); - $this->_applyStoresFilter(); - $this->_applyOrderStatusFilter(); - } - return parent::load($printQuery, $logQuery); + return $this->_applyOrderStatusFilter(); } } diff --git a/app/code/core/Mage/Sales/Model/Service/Order.php b/app/code/core/Mage/Sales/Model/Service/Order.php index 88a5935661..458c3e879f 100644 --- a/app/code/core/Mage/Sales/Model/Service/Order.php +++ b/app/code/core/Mage/Sales/Model/Service/Order.php @@ -77,9 +77,10 @@ public function getOrder() } /** - * Prepare order invoice based on order data and requested items qtys + * Prepare order invoice based on order data and requested items qtys. If $qtys is not empty - the function will + * prepare only specified items, otherwise all containing in the order. * - * @param array $data + * @param array $qtys * @return Mage_Sales_Model_Order_Invoice */ public function prepareInvoice($qtys = array()) @@ -93,14 +94,12 @@ public function prepareInvoice($qtys = array()) $item = $this->_convertor->itemToInvoiceItem($orderItem); if ($orderItem->isDummy()) { $qty = $orderItem->getQtyOrdered() ? $orderItem->getQtyOrdered() : 1; - } else { + } else if (!empty($qtys)) { if (isset($qtys[$orderItem->getId()])) { $qty = (float) $qtys[$orderItem->getId()]; - } elseif (!count($qtys)) { - $qty = $orderItem->getQtyToInvoice(); - } else { - continue; } + } else { + $qty = $orderItem->getQtyToInvoice(); } $totalQty += $qty; $item->setQty($qty); diff --git a/app/code/core/Mage/Sales/Model/Service/Quote.php b/app/code/core/Mage/Sales/Model/Service/Quote.php index bf49660f59..18b37e3655 100644 --- a/app/code/core/Mage/Sales/Model/Service/Quote.php +++ b/app/code/core/Mage/Sales/Model/Service/Quote.php @@ -289,31 +289,30 @@ protected function _inactivateQuote() */ protected function _validate() { - $helper = Mage::helper('sales'); if (!$this->getQuote()->isVirtual()) { $address = $this->getQuote()->getShippingAddress(); $addressValidation = $address->validate(); if ($addressValidation !== true) { Mage::throwException( - $helper->__('Please check shipping address information. %s', implode(' ', $addressValidation)) + Mage::helper('sales')->__('Please check shipping address information. %s', implode(' ', $addressValidation)) ); } $method= $address->getShippingMethod(); $rate = $address->getShippingRateByCode($method); if (!$this->getQuote()->isVirtual() && (!$method || !$rate)) { - Mage::throwException($helper->__('Please specify a shipping method.')); + Mage::throwException(Mage::helper('sales')->__('Please specify a shipping method.')); } } $addressValidation = $this->getQuote()->getBillingAddress()->validate(); if ($addressValidation !== true) { Mage::throwException( - $helper->__('Please check billing address information. %s', implode(' ', $addressValidation)) + Mage::helper('sales')->__('Please check billing address information. %s', implode(' ', $addressValidation)) ); } if (!($this->getQuote()->getPayment()->getMethod())) { - Mage::throwException($helper->__('Please select a valid payment method.')); + Mage::throwException(Mage::helper('sales')->__('Please select a valid payment method.')); } return $this; diff --git a/app/code/core/Mage/Sales/data/sales_setup/data-upgrade-1.6.0.4-1.6.0.5.php b/app/code/core/Mage/Sales/data/sales_setup/data-upgrade-1.6.0.4-1.6.0.5.php new file mode 100644 index 0000000000..608d430bac --- /dev/null +++ b/app/code/core/Mage/Sales/data/sales_setup/data-upgrade-1.6.0.4-1.6.0.5.php @@ -0,0 +1,76 @@ +getConnection()->select() + ->from( + array('citem' => $installer->getTable('sales/creditmemo_item')), + array( + 'amount_refunded' => 'SUM(citem.row_total)', + 'base_amount_refunded' => 'SUM(citem.base_row_total)', + 'base_tax_refunded' => 'SUM(citem.base_tax_amount)', + 'discount_refunded' => 'SUM(citem.discount_amount)', + 'base_discount_refunded' => 'SUM(citem.base_discount_amount)', + ) + ) + ->joinLeft( + array('c' => $installer->getTable('sales/creditmemo')), + 'c.entity_id = citem.parent_id', + array() + ) + ->joinLeft( + array('o' => $installer->getTable('sales/order')), + 'o.entity_id = c.order_id', + array() + ) + ->joinLeft( + array('oitem' => $installer->getTable('sales/order_item')), + 'oitem.order_id = o.entity_id AND oitem.product_id=citem.product_id', + array('item_id') + ) + ->group('oitem.item_id'); + +$select = $installer->getConnection()->select() + ->from( + array('selected' => $subSelect), + array( + 'amount_refunded' => 'amount_refunded', + 'base_amount_refunded' => 'base_amount_refunded', + 'base_tax_refunded' => 'base_tax_refunded', + 'discount_refunded' => 'discount_refunded', + 'base_discount_refunded' => 'base_discount_refunded', + ) + ) + ->where('main.item_id = selected.item_id'); + +$updateQuery = $installer->getConnection()->updateFromSelect( + $select, + array('main' => $installer->getTable('sales/order_item')) +); + +$installer->getConnection()->query($updateQuery); diff --git a/app/code/core/Mage/Sales/etc/config.xml b/app/code/core/Mage/Sales/etc/config.xml index cfb1f8d5cc..46db3b3716 100644 --- a/app/code/core/Mage/Sales/etc/config.xml +++ b/app/code/core/Mage/Sales/etc/config.xml @@ -28,7 +28,7 @@ - 1.6.0.4 + 1.6.0.7 @@ -1464,6 +1464,24 @@ + + + + + sales/observer + handleCustomerVatNumber + + + + + + + sales/observer + addVatRequestParamsOrderComment + + + + diff --git a/app/code/core/Mage/Sales/sql/sales_setup/upgrade-1.6.0.4-1.6.0.5.php b/app/code/core/Mage/Sales/sql/sales_setup/upgrade-1.6.0.4-1.6.0.5.php new file mode 100644 index 0000000000..ed4255763c --- /dev/null +++ b/app/code/core/Mage/Sales/sql/sales_setup/upgrade-1.6.0.4-1.6.0.5.php @@ -0,0 +1,49 @@ +getConnection() + ->addColumn($installer->getTable('sales/order_item'), 'base_tax_refunded', array( + 'type' => Varien_Db_Ddl_Table::TYPE_DECIMAL, + 'comment' => 'Base Tax Refunded', + 'scale' => 4, + 'precision' => 12, + )); +$installer->getConnection() + ->addColumn($installer->getTable('sales/order_item'), 'discount_refunded', array( + 'type' => Varien_Db_Ddl_Table::TYPE_DECIMAL, + 'comment' => 'Discount Refunded', + 'scale' => 4, + 'precision' => 12, + )); +$installer->getConnection() + ->addColumn($installer->getTable('sales/order_item'), 'base_discount_refunded', array( + 'type' => Varien_Db_Ddl_Table::TYPE_DECIMAL, + 'comment' => 'Base Discount Refunded', + 'scale' => 4, + 'precision' => 12, + )); diff --git a/app/code/core/Mage/Sales/sql/sales_setup/upgrade-1.6.0.5-1.6.0.6.php b/app/code/core/Mage/Sales/sql/sales_setup/upgrade-1.6.0.5-1.6.0.6.php new file mode 100644 index 0000000000..a7d3f63621 --- /dev/null +++ b/app/code/core/Mage/Sales/sql/sales_setup/upgrade-1.6.0.5-1.6.0.6.php @@ -0,0 +1,47 @@ + array('type' => Varien_Db_Ddl_Table::TYPE_TEXT), + 'vat_is_valid' => array('type' => Varien_Db_Ddl_Table::TYPE_SMALLINT), + 'vat_request_id' => array('type' => Varien_Db_Ddl_Table::TYPE_TEXT), + 'vat_request_date' => array('type' => Varien_Db_Ddl_Table::TYPE_TEXT), + 'vat_request_success' => array('type' => Varien_Db_Ddl_Table::TYPE_SMALLINT) +); + +foreach ($entitiesToAlter as $entityName) { + foreach ($attributes as $attributeCode => $attributeParams) { + $installer->addAttribute($entityName, $attributeCode, $attributeParams); + } +} diff --git a/app/code/core/Mage/Sales/sql/sales_setup/upgrade-1.6.0.6-1.6.0.7.php b/app/code/core/Mage/Sales/sql/sales_setup/upgrade-1.6.0.6-1.6.0.7.php new file mode 100644 index 0000000000..7fb9f66f47 --- /dev/null +++ b/app/code/core/Mage/Sales/sql/sales_setup/upgrade-1.6.0.6-1.6.0.7.php @@ -0,0 +1,36 @@ +getConnection() + ->addColumn($installer->getTable('sales/order'), 'coupon_rule_name', array( + 'TYPE' => Varien_Db_Ddl_Table::TYPE_TEXT, + 'LENGTH' => 255, + 'NULLABLE' => true, + 'COMMENT' => 'Coupon Sales Rule Name' + )); diff --git a/app/code/core/Mage/SalesRule/Helper/Coupon.php b/app/code/core/Mage/SalesRule/Helper/Coupon.php new file mode 100644 index 0000000000..240967ec2a --- /dev/null +++ b/app/code/core/Mage/SalesRule/Helper/Coupon.php @@ -0,0 +1,147 @@ + + */ +class Mage_SalesRule_Helper_Coupon extends Mage_Core_Helper_Abstract +{ + /** + * Constants which defines all possible coupon codes formats + */ + const COUPON_FORMAT_ALPHANUMERIC = 'alphanum'; + const COUPON_FORMAT_ALPHABETICAL = 'alpha'; + const COUPON_FORMAT_NUMERIC = 'num'; + + /** + * Defines type of Coupon + */ + const COUPON_TYPE_SPECIFIC_AUTOGENERATED = 1; + + /** + * XML paths to coupon codes generation options + */ + const XML_PATH_SALES_RULE_COUPON_LENGTH = 'promo/auto_generated_coupon_codes/length'; + const XML_PATH_SALES_RULE_COUPON_FORMAT = 'promo/auto_generated_coupon_codes/format'; + const XML_PATH_SALES_RULE_COUPON_PREFIX = 'promo/auto_generated_coupon_codes/prefix'; + const XML_PATH_SALES_RULE_COUPON_SUFFIX = 'promo/auto_generated_coupon_codes/suffix'; + const XML_PATH_SALES_RULE_COUPON_DASH_INTERVAL = 'promo/auto_generated_coupon_codes/dash'; + + /** + * Config path for character set and separator + */ + const XML_CHARSET_NODE = 'global/salesrule/coupon/charset/%s'; + const XML_CHARSET_SEPARATOR = 'global/salesrule/coupon/separator'; + + /** + * Get all possible coupon codes formats + * + * @return array + */ + public function getFormatsList() + { + return array( + self::COUPON_FORMAT_ALPHANUMERIC => $this->__('Alphanumeric'), + self::COUPON_FORMAT_ALPHABETICAL => $this->__('Alphabetical'), + self::COUPON_FORMAT_NUMERIC => $this->__('Numeric'), + ); + } + + /** + * Get default coupon code length + * + * @return int + */ + public function getDefaultLength() + { + return (int)Mage::getStoreConfig(self::XML_PATH_SALES_RULE_COUPON_LENGTH); + } + + /** + * Get default coupon code format + * + * @return int + */ + public function getDefaultFormat() + { + return (int)Mage::getStoreConfig(self::XML_PATH_SALES_RULE_COUPON_FORMAT); + } + + /** + * Get default coupon code prefix + * + * @return string + */ + public function getDefaultPrefix() + { + return Mage::getStoreConfig(self::XML_PATH_SALES_RULE_COUPON_PREFIX); + } + + /** + * Get default coupon code suffix + * + * @return string + */ + public function getDefaultSuffix() + { + return Mage::getStoreConfig(self::XML_PATH_SALES_RULE_COUPON_SUFFIX); + } + + /** + * Get dashes occurrences frequency in coupon code + * + * @return int + */ + public function getDefaultDashInterval() + { + return (int)Mage::getStoreConfig(self::XML_PATH_SALES_RULE_COUPON_DASH_INTERVAL); + } + + /** + * Get Coupon's alphabet as array of chars + * + * @param string $format + * @return array|bool + */ + public function getCharset($format) + { + return str_split((string) Mage::app()->getConfig()->getNode(sprintf(self::XML_CHARSET_NODE, $format))); + } + + /** + * Retrieve Separator from config + * + * @return string + */ + public function getCodeSeparator() + { + return (string) Mage::app()->getConfig()->getNode(Mage_SalesRule_Helper_Coupon::XML_CHARSET_SEPARATOR); + } +} diff --git a/app/code/core/Mage/SalesRule/Model/Coupon.php b/app/code/core/Mage/SalesRule/Model/Coupon.php index cbd0c40411..0a1f10412c 100644 --- a/app/code/core/Mage/SalesRule/Model/Coupon.php +++ b/app/code/core/Mage/SalesRule/Model/Coupon.php @@ -44,6 +44,8 @@ * @method Mage_SalesRule_Model_Coupon setExpirationDate(string $value) * @method int getIsPrimary() * @method Mage_SalesRule_Model_Coupon setIsPrimary(int $value) + * @method int getType() + * @method Mage_SalesRule_Model_Coupon setType(int $value) * * @category Mage * @package Mage_SalesRule @@ -92,11 +94,24 @@ public function setRule(Mage_SalesRule_Model_Rule $rule) /** * Load primary coupon for specified rule * - * @param Mage_SalesRule_Model_Rule|int Rule + * @param Mage_SalesRule_Model_Rule|int $rule + * @return Mage_SalesRule_Model_Coupon */ public function loadPrimaryByRule($rule) { $this->getResource()->loadPrimaryByRule($this, $rule); return $this; } + + /** + * Load Shopping Cart Price Rule by coupon code + * + * @param string $couponCode + * @return Mage_SalesRule_Model_Coupon + */ + public function loadByCode($couponCode) + { + $this->load($couponCode, 'code'); + return $this; + } } diff --git a/app/code/core/Mage/SalesRule/Model/Coupon/Massgenerator.php b/app/code/core/Mage/SalesRule/Model/Coupon/Massgenerator.php new file mode 100644 index 0000000000..0107f6f26a --- /dev/null +++ b/app/code/core/Mage/SalesRule/Model/Coupon/Massgenerator.php @@ -0,0 +1,188 @@ + + */ +class Mage_SalesRule_Model_Coupon_Massgenerator extends Mage_Core_Model_Abstract + implements Mage_SalesRule_Model_Coupon_CodegeneratorInterface +{ + /** + * Maximum probability of guessing the coupon on the first attempt + */ + const MAX_PROBABILITY_OF_GUESSING = 0.25; + const MAX_GENERATE_ATTEMPTS = 10; + + /** + * Count of generated Coupons + * @var int + */ + protected $_generatedCount = 0; + + /** + * Initialize resource + */ + protected function _construct() + { + $this->_init('salesrule/coupon'); + } + + /** + * Generate coupon code + * + * @return string + */ + public function generateCode() + { + $format = $this->getFormat(); + if (!$format) { + $format = Mage_SalesRule_Helper_Coupon::COUPON_FORMAT_ALPHANUMERIC; + } + $length = max(1, (int) $this->getLength()); + $split = max(0, (int) $this->getDash()); + $suffix = $this->getSuffix(); + $prefix = $this->getPrefix(); + + $splitChar = $this->getDelimiter(); + $charset = Mage::helper('salesrule/coupon')->getCharset($format); + + $code = ''; + for ($i=0; $i<$length; $i++) { + $char = $charset[array_rand($charset)]; + if ($split > 0 && ($i%$split) == 0 && $i != 0) { + $char = $splitChar . $char; + } + $code .= $char; + } + + $code = $prefix . $code . $suffix; + return $code; + } + + /** + * Retrieve delimiter + * + * @return string + */ + public function getDelimiter() + { + if ($this->getData('delimiter')) { + return $this->getData('delimiter'); + } else { + return Mage::helper('salesrule/coupon')->getCodeSeparator(); + } + } + + /** + * Generate Coupons Pool + * + * @return Mage_SalesRule_Model_Coupon_Massgenerator + */ + public function generatePool() + { + $this->_generatedCount = 0; + $size = $this->getQty(); + + $maxProbability = $this->getMaxProbability() ? $this->getMaxProbability() : self::MAX_PROBABILITY_OF_GUESSING; + $maxAttempts = $this->getMaxAttempts() ? $this->getMaxAttempts() : self::MAX_GENERATE_ATTEMPTS; + + /** @var $coupon Mage_SalesRule_Model_Coupon */ + $coupon = Mage::getModel('salesrule/coupon'); + + $chars = count(Mage::helper('salesrule/coupon')->getCharset($this->getFormat())); + $length = (int) $this->getLength(); + $maxCodes = pow($chars, $length); + $probability = $size / $maxCodes; + //increase the length of Code if probability is low + if ($probability > $maxProbability) { + do { + $length++; + $maxCodes = pow($chars, $length); + $probability = $size / $maxCodes; + } while ($probability > $maxProbability); + $this->setLength($length); + } + + for ($i = 0; $i < $size; $i++) { + $attempt = 0; + do { + if ($attempt >= $maxAttempts) { + Mage::throwException(Mage::helper('salesrule')->__('Unable to create requested Coupon Qty. Please check settings and try again.')); + } + $code = $this->generateCode(); + $attempt++; + } while ($this->getResource()->exists($code)); + + $expirationDate = $this->getToDate(); + if ($expirationDate instanceof Zend_Date) { + $expirationDate = $expirationDate->toString(Varien_Date::DATETIME_INTERNAL_FORMAT); + } + + $coupon->setId(null) + ->setRuleId($this->getRuleId()) + ->setUsageLimit($this->getUsesPerCoupon()) + ->setUsagePerCustomer($this->getUsesPerCustomer()) + ->setExpirationDate($expirationDate) + ->setType(Mage_SalesRule_Helper_Coupon::COUPON_TYPE_SPECIFIC_AUTOGENERATED) + ->setCode($code) + ->save(); + + $this->_generatedCount++; + } + return $this; + } + + /** + * Validate input + * + * @param array $data + * @return bool + */ + public function validateData($data) + { + return !empty($data) && !empty($data['qty']) && !empty($data['rule_id']) + && !empty($data['length']) && !empty($data['format']) + && (int)$data['qty'] > 0 && (int) $data['rule_id'] > 0 + && (int) $data['length'] > 0; + } + + /** + * Retrieve count of generated Coupons + * + * @return int + */ + public function getGeneratedCount() + { + return $this->_generatedCount; + } +} diff --git a/app/code/core/Mage/SalesRule/Model/Observer.php b/app/code/core/Mage/SalesRule/Model/Observer.php index 9235701076..14745af65b 100644 --- a/app/code/core/Mage/SalesRule/Model/Observer.php +++ b/app/code/core/Mage/SalesRule/Model/Observer.php @@ -241,5 +241,43 @@ public function addProductAttributes(Varien_Event_Observer $observer) $attributesTransfer->addData($result); return $this; } + + /** + * Add coupon's rule name to order data + * + * @param Varien_Event_Observer $observer + * @return Mage_SalesRule_Model_Observer + */ + public function addSalesRuleNameToOrder($observer) + { + $order = $observer->getOrder(); + $couponCode = $order->getCouponCode(); + + if (empty($couponCode)) { + return $this; + } + + /** + * @var Mage_SalesRule_Model_Coupon $couponModel + */ + $couponModel = Mage::getModel('salesrule/coupon'); + $couponModel->loadByCode($couponCode); + + $ruleId = $couponModel->getRuleId(); + + if (empty($ruleId)) { + return $this; + } + + /** + * @var Mage_SalesRule_Model_Rule $ruleModel + */ + $ruleModel = Mage::getModel('salesrule/rule'); + $ruleModel->load($ruleId); + + $order->setCouponRuleName($ruleModel->getName()); + + return $this; + } } diff --git a/app/code/core/Mage/SalesRule/Model/Quote/Discount.php b/app/code/core/Mage/SalesRule/Model/Quote/Discount.php index 01639d91bc..5f884b899c 100644 --- a/app/code/core/Mage/SalesRule/Model/Quote/Discount.php +++ b/app/code/core/Mage/SalesRule/Model/Quote/Discount.php @@ -139,7 +139,7 @@ public function fetch(Mage_Sales_Model_Quote_Address $address) if ($amount!=0) { $description = $address->getDiscountDescription(); - if ($description) { + if (strlen($description)) { $title = Mage::helper('sales')->__('Discount (%s)', $description); } else { $title = Mage::helper('sales')->__('Discount'); diff --git a/app/code/core/Mage/SalesRule/Model/Resource/Coupon.php b/app/code/core/Mage/SalesRule/Model/Resource/Coupon.php index 96045a2102..3fa2d14c3c 100755 --- a/app/code/core/Mage/SalesRule/Model/Resource/Coupon.php +++ b/app/code/core/Mage/SalesRule/Model/Resource/Coupon.php @@ -36,22 +36,21 @@ class Mage_SalesRule_Model_Resource_Coupon extends Mage_Core_Model_Resource_Db_A { /** * Constructor adds unique fields - * */ protected function _construct() { $this->_init('salesrule/coupon', 'coupon_id'); $this->addUniqueField(array( 'field' => 'code', - 'title' => Mage::helper('salesRule')->__('Coupon with the same code') + 'title' => Mage::helper('salesrule')->__('Coupon with the same code') )); } /** * Perform actions before object save * - * @param Varien_Object $object - * @return unknown + * @param Mage_Core_Model_Abstract $object + * @return Mage_Core_Model_Resource_Db_Abstract */ public function _beforeSave(Mage_Core_Model_Abstract $object) { @@ -72,7 +71,7 @@ public function _beforeSave(Mage_Core_Model_Abstract $object) * * * @param Mage_SalesRule_Model_Coupon $object - * @param unknown_type $rule + * @param Mage_SalesRule_Model_Rule|int $rule * @return unknown */ public function loadPrimaryByRule(Mage_SalesRule_Model_Coupon $object, $rule) @@ -100,4 +99,62 @@ public function loadPrimaryByRule(Mage_SalesRule_Model_Coupon $object, $rule) $this->_afterLoad($object); return true; } + + /** + * Check if code exists + * + * @param string $code + * @return bool + */ + public function exists($code) + { + $read = $this->_getReadAdapter(); + $select = $read->select(); + $select->from($this->getMainTable(), 'code'); + $select->where('code = :code'); + + if ($read->fetchOne($select, array('code' => $code)) === false) { + return false; + } + return true; + } + + /** + * Update auto generated Specific Coupon if it's rule changed + * + * @param Mage_SalesRule_Model_Rule $rule + * @return Mage_SalesRule_Model_Resource_Coupon + */ + public function updateSpecificCoupons(Mage_SalesRule_Model_Rule $rule) + { + if (!$rule || !$rule->getId() || !$rule->hasDataChanges()) { + return $this; + } + + $updateArray = array(); + if ($rule->dataHasChangedFor('uses_per_coupon')) { + $updateArray['usage_limit'] = $rule->getUsesPerCoupon(); + } + + if ($rule->dataHasChangedFor('uses_per_customer')) { + $updateArray['usage_per_customer'] = $rule->getUsesPerCustomer(); + } + + $ruleNewDate = new Zend_Date($rule->getToDate()); + $ruleOldDate = new Zend_Date($rule->getOrigData('to_date')); + + if ($ruleNewDate->compare($ruleOldDate)) { + $updateArray['expiration_date'] = $rule->getToDate(); + } + + if (!empty($updateArray)) { + $this->_getWriteAdapter()->update( + $this->getTable('salesrule/coupon'), + $updateArray, + array('rule_id = ?' => $rule->getId()) + ); + } + + return $this; + } } diff --git a/app/code/core/Mage/SalesRule/Model/Resource/Coupon/Collection.php b/app/code/core/Mage/SalesRule/Model/Resource/Coupon/Collection.php index f0f0af9a5b..77115ff227 100755 --- a/app/code/core/Mage/SalesRule/Model/Resource/Coupon/Collection.php +++ b/app/code/core/Mage/SalesRule/Model/Resource/Coupon/Collection.php @@ -47,8 +47,9 @@ protected function _construct() /** * Add rule to filter * + * @param Mage_SalesRule_Model_Rule|int $rule * - * @param unknown_type $rule + * @return Mage_SalesRule_Model_Resource_Coupon_Collection */ public function addRuleToFilter($rule) { @@ -57,17 +58,48 @@ public function addRuleToFilter($rule) } else { $ruleId = (int)$rule; } + $this->addFieldToFilter('rule_id', $ruleId); + + return $this; } /** * Add rule IDs to filter * - * * @param array $ruleIds + * + * @return Mage_SalesRule_Model_Resource_Coupon_Collection */ public function addRuleIdsToFilter(array $ruleIds) { $this->addFieldToFilter('rule_id', array('in' => $ruleIds)); + return $this; + } + + /** + * Filter collection to be filled with auto-generated coupons only + * + * @return Mage_SalesRule_Model_Resource_Coupon_Collection + */ + public function addGeneratedCouponsFilter() + { + $this->addFieldToFilter('is_primary', array('null' => 1)); + return $this; + } + + /** + * Callback function that filters collection by field "Used" from grid + * + * @param Mage_Core_Model_Resource_Db_Collection_Abstract $collection + * @param Mage_Adminhtml_Block_Widget_Grid_Column $column + */ + public function addIsUsedFilterCallback($collection, $column) + { + $filterValue = $column->getFilter()->getCondition(); + $collection->addFieldToFilter( + $this->getConnection()->getCheckSql('main_table.times_used > 0', 1, 0), + array('eq' => $filterValue) + ); } } diff --git a/app/code/core/Mage/SalesRule/Model/Resource/Report/Collection.php b/app/code/core/Mage/SalesRule/Model/Resource/Report/Collection.php index 911c6a871a..bc7d552f9d 100755 --- a/app/code/core/Mage/SalesRule/Model/Resource/Report/Collection.php +++ b/app/code/core/Mage/SalesRule/Model/Resource/Report/Collection.php @@ -55,6 +55,13 @@ class Mage_SalesRule_Model_Resource_Report_Collection extends Mage_Sales_Model_R */ protected $_selectedColumns = array(); + /** + * array where rules ids stored + * + * @var array + */ + protected $_rulesIdsFilter; + /** * Initialize custom resource model * @@ -88,6 +95,7 @@ protected function _getSelectedColumns() $this->_selectedColumns = array( 'period' => $this->_periodFormat, 'coupon_code', + 'rule_name', 'coupon_uses' => 'SUM(coupon_uses)', 'subtotal_amount' => 'SUM(subtotal_amount)', 'discount_amount' => 'SUM(discount_amount)', @@ -131,5 +139,54 @@ protected function _initSelect() return $this; } + /** + * Add filtering by rules ids + * + * @param array $rulesList + * @return Mage_SalesRule_Model_Resource_Report_Collection + */ + public function addRuleFilter($rulesList) + { + $this->_rulesIdsFilter = $rulesList; + return $this; + } + /** + * Apply filtering by rules ids + * + * @return Mage_SalesRule_Model_Resource_Report_Collection + */ + protected function _applyRulesFilter() + { + if (empty($this->_rulesIdsFilter) || !is_array($this->_rulesIdsFilter)) { + return $this; + } + + $rulesList = Mage::getResourceModel('salesrule/report_rule')->getUniqRulesNamesList(); + + $rulesFilterSqlParts = array(); + + foreach ($this->_rulesIdsFilter as $ruleId) { + if (!isset($rulesList[$ruleId])) { + continue; + } + $ruleName = $rulesList[$ruleId]; + $rulesFilterSqlParts[] = $this->getConnection()->quoteInto('rule_name = ?', $ruleName); + } + + if (!empty($rulesFilterSqlParts)) { + $this->getSelect()->where(implode($rulesFilterSqlParts, ' OR ')); + } + } + + /** + * Apply collection custom filter + * + * @return Mage_Sales_Model_Resource_Report_Collection_Abstract + */ + protected function _applyCustomFilter() + { + $this->_applyRulesFilter(); + return parent::_applyCustomFilter(); + } } diff --git a/app/code/core/Mage/SalesRule/Model/Resource/Report/Rule.php b/app/code/core/Mage/SalesRule/Model/Resource/Report/Rule.php index f769a42d09..927a342d67 100755 --- a/app/code/core/Mage/SalesRule/Model/Resource/Report/Rule.php +++ b/app/code/core/Mage/SalesRule/Model/Resource/Report/Rule.php @@ -59,6 +59,35 @@ public function aggregate($from = null, $to = null) return $this; } + /** + * Get all unique Rule Names from aggregated coupons usage data + * + * @return array + */ + public function getUniqRulesNamesList() + { + $adapter = $this->_getReadAdapter(); + $tableName = $this->getTable('salesrule/coupon_aggregated'); + $select = $adapter->select() + ->from( + $tableName, + new Zend_Db_Expr('DISTINCT rule_name') + ) + ->where('rule_name IS NOT NULL') + ->where('rule_name <> ""') + ->order('rule_name ASC'); + + $rulesNames = $adapter->fetchAll($select); + + $result = array(); + + foreach ($rulesNames as $row) { + $result[] = $row['rule_name']; + } + + return $result; + } + /** * Aggregate coupons reports by order created at as range * diff --git a/app/code/core/Mage/SalesRule/Model/Resource/Report/Rule/Createdat.php b/app/code/core/Mage/SalesRule/Model/Resource/Report/Rule/Createdat.php index 0a7b620426..f1aa2ec8a4 100644 --- a/app/code/core/Mage/SalesRule/Model/Resource/Report/Rule/Createdat.php +++ b/app/code/core/Mage/SalesRule/Model/Resource/Report/Rule/Createdat.php @@ -66,6 +66,11 @@ public function aggregate($from = null, $to = null) */ protected function _aggregateByOrder($aggregationField, $from, $to) { + $from = $this->_dateToUtc($from); + $to = $this->_dateToUtc($to); + + $this->_checkDates($from, $to); + $table = $this->getMainTable(); $sourceTable = $this->getTable('sales/order'); $adapter = $this->_getWriteAdapter(); @@ -90,6 +95,7 @@ protected function _aggregateByOrder($aggregationField, $from, $to) 'store_id' => 'store_id', 'order_status' => 'status', 'coupon_code' => 'coupon_code', + 'rule_name' => 'coupon_rule_name', 'coupon_uses' => 'COUNT(entity_id)', 'subtotal_amount' => @@ -151,6 +157,7 @@ protected function _aggregateByOrder($aggregationField, $from, $to) 'store_id' => new Zend_Db_Expr('0'), 'order_status' => 'order_status', 'coupon_code' => 'coupon_code', + 'rule_name' => 'rule_name', 'coupon_uses' => 'SUM(coupon_uses)', 'subtotal_amount' => 'SUM(subtotal_amount)', 'discount_amount' => 'SUM(discount_amount)', diff --git a/app/code/core/Mage/SalesRule/Model/Resource/Rule.php b/app/code/core/Mage/SalesRule/Model/Resource/Rule.php index 250b9f2acf..a35bc90f18 100755 --- a/app/code/core/Mage/SalesRule/Model/Resource/Rule.php +++ b/app/code/core/Mage/SalesRule/Model/Resource/Rule.php @@ -58,7 +58,7 @@ public function _beforeSave(Mage_Core_Model_Abstract $object) } if (!$object->getToDate()) { - $object->setToDate(new Zend_Db_Expr('NULL')); + $object->setToDate(null); } else { if ($object->getToDate() instanceof Zend_Date) { $object->setToDate($object->getToDate()->toString(Varien_Date::DATETIME_INTERNAL_FORMAT)); @@ -66,7 +66,7 @@ public function _beforeSave(Mage_Core_Model_Abstract $object) } if (!$object->getDiscountQty()) { - $object->setDiscountQty(new Zend_Db_Expr('NULL')); + $object->setDiscountQty(null); } parent::_beforeSave($object); diff --git a/app/code/core/Mage/SalesRule/Model/Resource/Rule/Collection.php b/app/code/core/Mage/SalesRule/Model/Resource/Rule/Collection.php index 9260151da8..e7443c5a74 100755 --- a/app/code/core/Mage/SalesRule/Model/Resource/Rule/Collection.php +++ b/app/code/core/Mage/SalesRule/Model/Resource/Rule/Collection.php @@ -47,10 +47,10 @@ protected function _construct() /** * Set filter to select rules that matches current criteria * - * @param unknown_type $websiteId - * @param unknown_type $customerGroupId - * @param unknown_type $couponCode - * @param unknown_type $now + * @param int $websiteId + * @param int $customerGroupId + * @param string $couponCode + * @param string $now * @return Mage_SalesRule_Model_Resource_Rule_Collection */ public function setValidationFilter($websiteId, $customerGroupId, $couponCode = '', $now = null) @@ -66,7 +66,7 @@ public function setValidationFilter($websiteId, $customerGroupId, $couponCode = ->addFieldToFilter('customer_group_ids', array('finset' => (int)$customerGroupId)) ->addFieldToFilter('is_active', 1); - if ($couponCode) { + if (strlen($couponCode)) { $this->getSelect() ->joinLeft( array('rule_coupons' => $this->getTable('salesrule/coupon')), @@ -144,4 +144,19 @@ public function addAttributeInConditionFilter($attributeCode) return $this; } + + /** + * Excludes price rules with generated specific coupon codes from collection + * + * @return Mage_SalesRule_Model_Resource_Rule_Collection + */ + public function addAllowedSalesRulesFilter() + { + $this->addFieldToFilter( + 'main_table.use_auto_generation', + array('neq' => 1) + ); + + return $this; + } } diff --git a/app/code/core/Mage/SalesRule/Model/Rule.php b/app/code/core/Mage/SalesRule/Model/Rule.php index a25044b7d4..372bf41aed 100644 --- a/app/code/core/Mage/SalesRule/Model/Rule.php +++ b/app/code/core/Mage/SalesRule/Model/Rule.php @@ -40,6 +40,8 @@ * @method Mage_SalesRule_Model_Rule setToDate(string $value) * @method int getUsesPerCustomer() * @method Mage_SalesRule_Model_Rule setUsesPerCustomer(int $value) + * @method int getUsesPerCoupon() + * @method Mage_SalesRule_Model_Rule setUsesPerCoupon(int $value) * @method string getCustomerGroupIds() * @method Mage_SalesRule_Model_Rule setCustomerGroupIds(string $value) * @method int getIsActive() @@ -76,6 +78,10 @@ * @method Mage_SalesRule_Model_Rule setWebsiteIds(string $value) * @method int getCouponType() * @method Mage_SalesRule_Model_Rule setCouponType(int $value) + * @method int getUseAutoGeneration() + * @method Mage_SalesRule_Model_Rule setUseAutoGeneration(int $value) + * @method string getCouponCode() + * @method Mage_SalesRule_Model_Rule setCouponCode(string $value) * * @category Mage * @package Mage_SalesRule @@ -83,9 +89,15 @@ */ class Mage_SalesRule_Model_Rule extends Mage_Rule_Model_Rule { + /** + * Free shipping for Item and Address constants + */ const FREE_SHIPPING_ITEM = 1; const FREE_SHIPPING_ADDRESS = 2; + /** + * Coupon types + */ const COUPON_TYPE_NO_COUPON = 1; const COUPON_TYPE_SPECIFIC = 2; const COUPON_TYPE_AUTO = 3; @@ -100,7 +112,6 @@ class Mage_SalesRule_Model_Rule extends Mage_Rule_Model_Rule const CART_FIXED_ACTION = 'cart_fixed'; const BUY_X_GET_Y_ACTION = 'buy_x_get_y'; - /** * @var Mage_SalesRule_Model_Coupon_CodegeneratorInterface */ @@ -122,6 +133,11 @@ class Mage_SalesRule_Model_Rule extends Mage_Rule_Model_Rule */ protected $_eventObject = 'rule'; + /** + * Rule labels for stores + * + * @var array + */ protected $_labels = array(); /** @@ -152,6 +168,9 @@ class Mage_SalesRule_Model_Rule extends Mage_Rule_Model_Rule */ protected $_validatedAddresses = array(); + /** + * Initializes resource and sets ID field name + */ protected function _construct() { parent::_construct(); @@ -160,7 +179,17 @@ protected function _construct() } /** - * Set code generator instance for auto generated coupons + * Returns code mass generator instance for auto generated specific coupons + * + * @return Mage_SalesRule_Model_Coupon_MassgneratorInterface + */ + public static function getCouponMassGenerator() + { + return Mage::getSingleton('salesrule/coupon_massgenerator'); + } + + /** + * Returns code generator instance for auto generated coupons * * @return Mage_SalesRule_Model_Coupon_CodegeneratorInterface */ @@ -205,36 +234,36 @@ public function getPrimaryCoupon() protected function _afterLoad() { $this->setCouponCode($this->getPrimaryCoupon()->getCode()); - $this->setUsesPerCoupon($this->getPrimaryCoupon()->getUsageLimit()); + if ($this->getUsesPerCoupon() !== null && !$this->getUseAutoGeneration()) { + $this->setUsesPerCoupon($this->getPrimaryCoupon()->getUsageLimit()); + } return parent::_afterLoad(); } + /** + * Get rule condition combine model instance + * + * @return Mage_SalesRule_Model_Rule_Condition_Combine + */ public function getConditionsInstance() { return Mage::getModel('salesrule/rule_condition_combine'); } + /** + * Get rule condition product combine model instance + * + * @return Mage_SalesRule_Model_Rule_Condition_Product_Combine + */ public function getActionsInstance() { return Mage::getModel('salesrule/rule_condition_product_combine'); } - public function toString($format='') - { - $str = Mage::helper('salesrule')->__("Name: %s", $this->getName()) ."\n" - . Mage::helper('salesrule')->__("Start at: %s", $this->getStartAt()) ."\n" - . Mage::helper('salesrule')->__("Expire at: %s", $this->getExpireAt()) ."\n" - . Mage::helper('salesrule')->__("Customer registered: %s", $this->getCustomerRegistered()) ."\n" - . Mage::helper('salesrule')->__("Customer is new buyer: %s", $this->getCustomerNewBuyer()) ."\n" - . Mage::helper('salesrule')->__("Description: %s", $this->getDescription()) ."\n\n" - . $this->getConditions()->toStringRecursive() ."\n\n" - . $this->getActions()->toStringRecursive() ."\n\n"; - return $str; - } - /** * Initialize rule model data from array * + * * @param array $rule * @return Mage_SalesRule_Model_Rule */ @@ -254,26 +283,10 @@ public function loadPost(array $rule) } /** - * Returns rule as an array for admin interface - * - * Output example: - * array( - * 'name'=>'Example rule', - * 'conditions'=>{condition_combine::toArray} - * 'actions'=>{action_collection::toArray} - * ) + * Get resource collection * - * @return array + * @return Mage_SalesRule_Model_Resource_Rule_Collection */ - public function toArray(array $arrAttributes = array()) - { - $out = parent::toArray($arrAttributes); - $out['customer_registered'] = $this->getCustomerRegistered(); - $out['customer_new_buyer'] = $this->getCustomerNewBuyer(); - - return $out; - } - public function getResourceCollection() { return Mage::getResourceModel('salesrule/rule_collection'); @@ -290,7 +303,10 @@ protected function _afterSave() $this->_getResource()->saveStoreLabels($this->getId(), $this->getStoreLabels()); } $couponCode = trim($this->getCouponCode()); - if ($couponCode && $this->getCouponType() == self::COUPON_TYPE_SPECIFIC) { + if (strlen($couponCode) + && $this->getCouponType() == self::COUPON_TYPE_SPECIFIC + && !$this->getUseAutoGeneration() + ) { $this->getPrimaryCoupon() ->setCode($couponCode) ->setUsageLimit($this->getUsesPerCoupon() ? $this->getUsesPerCoupon() : null) @@ -309,6 +325,11 @@ protected function _afterSave() if (count($ruleProductAttributes)) { $this->getResource()->setActualProductAttributes($this, $ruleProductAttributes); } + + //Update auto geterated specific coupons if exists + if ($this->getUseAutoGeneration() && $this->hasDataChanges()) { + Mage::getResourceModel('salesrule/coupon')->updateSpecificCoupons($this); + } return parent::_afterSave(); } @@ -454,7 +475,7 @@ protected function _getUsedAttributes($serializedString) $result = array(); if (preg_match_all('~s:32:"salesrule/rule_condition_product";s:9:"attribute";s:\d+:"(.*?)"~s', $serializedString, $matches)){ - foreach ($matches[1] as $offset => $attributeCode) { + foreach ($matches[1] as $attributeCode) { $result[] = $attributeCode; } } diff --git a/app/code/core/Mage/SalesRule/Model/Rule/Condition/Product/Found.php b/app/code/core/Mage/SalesRule/Model/Rule/Condition/Product/Found.php index a0a9ae0388..3aa3e54e81 100644 --- a/app/code/core/Mage/SalesRule/Model/Rule/Condition/Product/Found.php +++ b/app/code/core/Mage/SalesRule/Model/Rule/Condition/Product/Found.php @@ -50,12 +50,10 @@ public function loadValueOptions() public function asHtml() { - $html = $this->getTypeElement()->getHtml(). - Mage::helper('salesrule')->__("If an item is %s in the cart with %s of these conditions true:", - $this->getValueElement()->getHtml(), $this->getAggregatorElement()->getHtml()); - if ($this->getId()!='1') { - $html.= $this->getRemoveLinkHtml(); - } + $html = $this->getTypeElement()->getHtml() . Mage::helper('salesrule')->__("If an item is %s in the cart with %s of these conditions true:", $this->getValueElement()->getHtml(), $this->getAggregatorElement()->getHtml()); + if ($this->getId() != '1') { + $html.= $this->getRemoveLinkHtml(); + } return $html; } diff --git a/app/code/core/Mage/SalesRule/Model/Rule/Condition/Product/Subselect.php b/app/code/core/Mage/SalesRule/Model/Rule/Condition/Product/Subselect.php index 2bdd63178c..04bd530db9 100644 --- a/app/code/core/Mage/SalesRule/Model/Rule/Condition/Product/Subselect.php +++ b/app/code/core/Mage/SalesRule/Model/Rule/Condition/Product/Subselect.php @@ -45,29 +45,17 @@ public function loadArray($arr, $key='conditions') public function asXml($containerKey='conditions', $itemKey='condition') { - $xml .= ''.$this->getAttribute().'' - .''.$this->getOperator().'' - .parent::asXml($containerKey, $itemKey); + $xml = ''.$this->getAttribute().'' + . ''.$this->getOperator().'' + . parent::asXml($containerKey, $itemKey); return $xml; } -// public function loadAggregatorOptions() -// { -// $this->setAggregatorOption(array( -// '1/all' => Mage::helper('rule')->__('MATCHING ALL'), -// '1/any' => Mage::helper('rule')->__('MATCHING ANY'), -// '0/all' => Mage::helper('rule')->__('NOT MATCHING ALL'), -// '0/any' => Mage::helper('rule')->__('NOT MATCHING ANY'), -// )); -// return $this; -// } - public function loadAttributeOptions() { - $hlp = Mage::helper('salesrule'); $this->setAttributeOption(array( - 'qty' => $hlp->__('total quantity'), - 'base_row_total' => $hlp->__('total amount'), + 'qty' => Mage::helper('salesrule')->__('total quantity'), + 'base_row_total' => Mage::helper('salesrule')->__('total amount'), )); return $this; } @@ -100,15 +88,10 @@ public function getValueElementType() public function asHtml() { $html = $this->getTypeElement()->getHtml(). - Mage::helper('salesrule')->__("If %s %s %s for a subselection of items in cart matching %s of these conditions:", - $this->getAttributeElement()->getHtml(), - $this->getOperatorElement()->getHtml(), - $this->getValueElement()->getHtml(), - $this->getAggregatorElement()->getHtml() - ); - if ($this->getId()!='1') { - $html.= $this->getRemoveLinkHtml(); - } + Mage::helper('salesrule')->__("If %s %s %s for a subselection of items in cart matching %s of these conditions:", $this->getAttributeElement()->getHtml(), $this->getOperatorElement()->getHtml(), $this->getValueElement()->getHtml(), $this->getAggregatorElement()->getHtml()); + if ($this->getId() != '1') { + $html .= $this->getRemoveLinkHtml(); + } return $html; } diff --git a/app/code/core/Mage/SalesRule/Model/System/Config/Source/Coupon/Format.php b/app/code/core/Mage/SalesRule/Model/System/Config/Source/Coupon/Format.php new file mode 100644 index 0000000000..8980237bcb --- /dev/null +++ b/app/code/core/Mage/SalesRule/Model/System/Config/Source/Coupon/Format.php @@ -0,0 +1,54 @@ + + */ +class Mage_SalesRule_Model_System_Config_Source_Coupon_Format +{ + /** + * Options getter + * + * @return array + */ + public function toOptionArray() + { + $formatsList = Mage::helper('salesrule/coupon')->getFormatsList(); + $result = array(); + foreach ($formatsList as $formatId => $formatTitle) { + $result[] = array( + 'value' => $formatId, + 'label' => $formatTitle + ); + } + + return $result; + } +} diff --git a/app/code/core/Mage/SalesRule/Model/Validator.php b/app/code/core/Mage/SalesRule/Model/Validator.php index e260a2f784..e426d62d9e 100644 --- a/app/code/core/Mage/SalesRule/Model/Validator.php +++ b/app/code/core/Mage/SalesRule/Model/Validator.php @@ -44,6 +44,7 @@ class Mage_SalesRule_Model_Validator extends Mage_Core_Model_Abstract protected $_rules; protected $_roundingDeltas = array(); + protected $_baseRoundingDeltas = array(); /** @@ -148,7 +149,7 @@ protected function _canProcessRule($rule, $address) */ if ($rule->getCouponType() != Mage_SalesRule_Model_Rule::COUPON_TYPE_NO_COUPON) { $couponCode = $address->getQuote()->getCouponCode(); - if ($couponCode) { + if (strlen($couponCode)) { $coupon = Mage::getModel('salesrule/coupon'); $coupon->load($couponCode, 'code'); if ($coupon->getId()) { @@ -678,18 +679,24 @@ public function initTotals($items, Mage_Sales_Model_Quote_Address $address) } /** - * Retrieve subordinate coupon IDs + * Set coupon code to address if $rule contains validated coupon * - * @return array + * @param Mage_Sales_Model_Quote_Address $address + * @param Mage_SalesRule_Model_Rule $rule + * + * @return Mage_SalesRule_Model_Validator */ protected function _maintainAddressCouponCode($address, $rule) { - foreach ($rule->getCoupons() as $coupon) { - if (strtolower($coupon->getCode()) == strtolower($this->getCouponCode())) { - $address->setCouponCode($this->getCouponCode()); - break; - } + /* + Rule is a part of rules collection, which includes only rules with 'No Coupon' type or with validated coupon. + As a result, if rule uses coupon code(s) ('Specific' or 'Auto' Coupon Type), it always contains validated coupon + */ + if ($rule->getCoponType() != Mage_SalesRule_Model_Rule::COUPON_TYPE_NO_COUPON) { + $address->setCouponCode($this->getCouponCode()); } + + return $this; } /** @@ -706,14 +713,16 @@ protected function _addDiscountDescription($address, $rule) $label = ''; if ($ruleLabel) { $label = $ruleLabel; - } else if ($address->getCouponCode()) { + } else if (strlen($address->getCouponCode())) { $label = $address->getCouponCode(); } - if (!empty($label)) { + if (strlen($label)) { $description[$rule->getId()] = $label; } + $address->setDiscountDescriptionArray($description); + return $this; } diff --git a/app/code/core/Mage/SalesRule/etc/config.xml b/app/code/core/Mage/SalesRule/etc/config.xml index af4213d1d5..23d3d53968 100644 --- a/app/code/core/Mage/SalesRule/etc/config.xml +++ b/app/code/core/Mage/SalesRule/etc/config.xml @@ -28,7 +28,7 @@ - 1.6.0.1 + 1.6.0.2 @@ -114,6 +114,14 @@ + + + + salesrule/observer + addSalesRuleNameToOrder + + + @@ -137,6 +145,16 @@ + + + - + + ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 + ABCDEFGHIJKLMNOPQRSTUVWXYZ + 0123456789 + + + @@ -190,4 +208,12 @@ + + + + 12 + 1 + + + diff --git a/app/code/core/Mage/SalesRule/etc/system.xml b/app/code/core/Mage/SalesRule/etc/system.xml new file mode 100644 index 0000000000..a6f9131240 --- /dev/null +++ b/app/code/core/Mage/SalesRule/etc/system.xml @@ -0,0 +1,95 @@ + + + + + + separator-top + + customer + text + 400 + 1 + 0 + 0 + + + + 1 + 10 + + + + text + 10 + 1 + 0 + 0 + Excluding prefix, suffix and separators. + validate-digits + + + + select + salesrule/system_config_source_coupon_format + 20 + 1 + 0 + 0 + + + + text + 30 + 1 + 0 + 0 + + + + text + 40 + 1 + 0 + 0 + + + + text + 50 + 1 + 0 + 0 + If empty no separation. + validate-digits + + + + + + + diff --git a/app/code/core/Mage/SalesRule/sql/salesrule_setup/upgrade-1.6.0.1-1.6.0.2.php b/app/code/core/Mage/SalesRule/sql/salesrule_setup/upgrade-1.6.0.1-1.6.0.2.php new file mode 100644 index 0000000000..5f4af2ccb9 --- /dev/null +++ b/app/code/core/Mage/SalesRule/sql/salesrule_setup/upgrade-1.6.0.1-1.6.0.2.php @@ -0,0 +1,145 @@ +getConnection() + ->addColumn( + $installer->getTable('salesrule/coupon'), + 'created_at', + array( + 'type' => Varien_Db_Ddl_Table::TYPE_TIMESTAMP, + 'comment' => 'Coupon Code Creation Date', + 'nullable' => false, + 'default' => Varien_Db_Ddl_Table::TIMESTAMP_INIT + ) + ); + +$installer->getConnection()->addColumn( + $installer->getTable('salesrule/coupon'), + 'type', + array( + 'type' => Varien_Db_Ddl_Table::TYPE_SMALLINT, + 'comment' => 'Coupon Code Type', + 'default' => 0 + ) + ); + +$installer->getConnection() + ->addColumn( + $installer->getTable('salesrule/rule'), + 'use_auto_generation', + array( + 'type' => Varien_Db_Ddl_Table::TYPE_SMALLINT, + 'comment' => 'Use Auto Generation', + 'nullable' => false, + 'default' => 0 + ) + ); + +$installer->getConnection() + ->addColumn( + $installer->getTable('salesrule/rule'), + 'uses_per_coupon', + array( + 'type' => Varien_Db_Ddl_Table::TYPE_INTEGER, + 'comment' => 'Uses Per Coupon', + 'nullable' => false, + 'default' => 0 + ) + ); + +$installer->getConnection() + ->addColumn( + $installer->getTable('salesrule/coupon_aggregated'), + 'rule_name', + array( + 'type' => Varien_Db_Ddl_Table::TYPE_TEXT, + 'length' => 255, + 'comment' => 'Rule Name', + ) + ); + +$installer->getConnection() + ->addColumn( + $installer->getTable('salesrule/coupon_aggregated_order'), + 'rule_name', + array( + 'type' => Varien_Db_Ddl_Table::TYPE_TEXT, + 'length' => 255, + 'comment' => 'Rule Name', + ) + ); + +$installer->getConnection() + ->addColumn( + $installer->getTable('salesrule/coupon_aggregated_updated'), + 'rule_name', + array( + 'type' => Varien_Db_Ddl_Table::TYPE_TEXT, + 'length' => 255, + 'comment' => 'Rule Name', + ) + ); + +$installer->getConnection() + ->addIndex( + $installer->getTable('salesrule/coupon_aggregated'), + $installer->getIdxName( + 'salesrule/coupon_aggregated', + array('rule_name'), + Varien_Db_Adapter_Interface::INDEX_TYPE_INDEX + ), + array('rule_name'), + Varien_Db_Adapter_Interface::INDEX_TYPE_INDEX + ); + +$installer->getConnection() + ->addIndex( + $installer->getTable('salesrule/coupon_aggregated_order'), + $installer->getIdxName( + 'salesrule/coupon_aggregated_order', + array('rule_name'), + Varien_Db_Adapter_Interface::INDEX_TYPE_INDEX + ), + array('rule_name'), + Varien_Db_Adapter_Interface::INDEX_TYPE_INDEX + ); + +$installer->getConnection() + ->addIndex( + $installer->getTable('salesrule/coupon_aggregated_updated'), + $installer->getIdxName( + 'salesrule/coupon_aggregated_updated', + array('rule_name'), + Varien_Db_Adapter_Interface::INDEX_TYPE_INDEX + ), + array('rule_name'), + Varien_Db_Adapter_Interface::INDEX_TYPE_INDEX + ); diff --git a/app/code/core/Mage/Sendfriend/etc/system.xml b/app/code/core/Mage/Sendfriend/etc/system.xml index 756af4194d..060fcc001f 100644 --- a/app/code/core/Mage/Sendfriend/etc/system.xml +++ b/app/code/core/Mage/Sendfriend/etc/system.xml @@ -78,6 +78,7 @@ 1 1 1 + validate-digits @@ -86,6 +87,7 @@ 1 1 1 + validate-digits diff --git a/app/code/core/Mage/Shipping/Model/Carrier/Abstract.php b/app/code/core/Mage/Shipping/Model/Carrier/Abstract.php index 91b86492e0..b043a217dc 100644 --- a/app/code/core/Mage/Shipping/Model/Carrier/Abstract.php +++ b/app/code/core/Mage/Shipping/Model/Carrier/Abstract.php @@ -27,10 +27,34 @@ abstract class Mage_Shipping_Model_Carrier_Abstract extends Varien_Object { + /** + * Carrier's code + * + * @var string + */ protected $_code; + + /** + * Rates result + * + * @var array + */ protected $_rates = null; + + /** + * Number of boxes in package + * + * @var int + */ protected $_numBoxes = 1; + /** + * Free Method config path + * + * @var string + */ + protected $_freeMethod = 'free_method'; + /** * Whether this carrier has fixed rates calculation * @@ -316,7 +340,7 @@ protected function _updateFreeMethodQuote($request) return; } - $freeMethod = $this->getConfigData('free_method'); + $freeMethod = $this->getConfigData($this->_freeMethod); if (!$freeMethod) { return; } @@ -378,9 +402,9 @@ protected function _updateFreeMethodQuote($request) */ public function getMethodPrice($cost, $method='') { - if ($method == $this->getConfigData('free_method') && $this->getConfigData('free_shipping_enable') - && $this->getConfigData('free_shipping_subtotal') <= $this->_rawRequest->getValueWithDiscount() - ){ + if ($method == $this->getConfigData($this->_freeMethod) && $this->getConfigData('free_shipping_enable') + && $this->getConfigData('free_shipping_subtotal') <= $this->_rawRequest->getBaseSubtotalInclTax() + ) { $price = '0.00'; } else { $price = $this->getFinalPriceWithHandlingFee($cost); @@ -391,12 +415,12 @@ public function getMethodPrice($cost, $method='') /** * get the handling fee for the shipping + cost * - * @return final price for shipping emthod + * @param float $cost + * @return float final price for shipping method */ public function getFinalPriceWithHandlingFee($cost) { $handlingFee = $this->getConfigData('handling_fee'); - $finalMethodPrice = 0; $handlingType = $this->getConfigData('handling_type'); if (!$handlingType) { $handlingType = self::HANDLING_TYPE_FIXED; @@ -406,22 +430,43 @@ public function getFinalPriceWithHandlingFee($cost) $handlingAction = self::HANDLING_ACTION_PERORDER; } - if($handlingAction == self::HANDLING_ACTION_PERPACKAGE) - { - if ($handlingType == self::HANDLING_TYPE_PERCENT) { - $finalMethodPrice = ($cost + ($cost * $handlingFee/100)) * $this->_numBoxes; - } else { - $finalMethodPrice = ($cost + $handlingFee) * $this->_numBoxes; - } - } else { - if ($handlingType == self::HANDLING_TYPE_PERCENT) { - $finalMethodPrice = ($cost * $this->_numBoxes) + ($cost * $this->_numBoxes * $handlingFee / 100); - } else { - $finalMethodPrice = ($cost * $this->_numBoxes) + $handlingFee; - } + return ($handlingAction == self::HANDLING_ACTION_PERPACKAGE) + ? $this->_getPerpackagePrice($cost, $handlingType, $handlingFee) + : $this->_getPerorderPrice($cost, $handlingType, $handlingFee); + } + + /** + * Get final price for shipping method with handling fee per package + * + * @param float $cost + * @param string $handlingType + * @param float $handlingFee + * @return float + */ + protected function _getPerpackagePrice($cost, $handlingType, $handlingFee) + { + if ($handlingType == self::HANDLING_TYPE_PERCENT) { + return ($cost + ($cost * $handlingFee/100)) * $this->_numBoxes; + } + + return ($cost + $handlingFee) * $this->_numBoxes; + } + /** + * Get final price for shipping method with handling fee per order + * + * @param float $cost + * @param string $handlingType + * @param float $handlingFee + * @return float + */ + protected function _getPerorderPrice($cost, $handlingType, $handlingFee) + { + if ($handlingType == self::HANDLING_TYPE_PERCENT) { + return ($cost * $this->_numBoxes) + ($cost * $this->_numBoxes * $handlingFee / 100); } - return $finalMethodPrice; + + return ($cost * $this->_numBoxes) + $handlingFee; } /** diff --git a/app/code/core/Mage/Shipping/Model/Carrier/Freeshipping.php b/app/code/core/Mage/Shipping/Model/Carrier/Freeshipping.php index d2ef1e3ebe..eec6ddd615 100644 --- a/app/code/core/Mage/Shipping/Model/Carrier/Freeshipping.php +++ b/app/code/core/Mage/Shipping/Model/Carrier/Freeshipping.php @@ -53,14 +53,12 @@ public function collectRates(Mage_Shipping_Model_Rate_Request $request) } $result = Mage::getModel('shipping/rate_result'); - $packageValue = $request->getPackageValue(); $this->_updateFreeMethodQuote($request); - $allow = ($request->getFreeShipping()) - || ($packageValue >= $this->getConfigData('free_shipping_subtotal')); - - if ($allow) { + if (($request->getFreeShipping()) + || ($request->getBaseSubtotalInclTax() >= $this->getConfigData('free_shipping_subtotal')) + ) { $method = Mage::getModel('shipping/rate_result_method'); $method->setCarrier('freeshipping'); diff --git a/app/code/core/Mage/Shipping/Model/Resource/Carrier/Tablerate.php b/app/code/core/Mage/Shipping/Model/Resource/Carrier/Tablerate.php index 60e78c47d7..7568ab16dc 100755 --- a/app/code/core/Mage/Shipping/Model/Resource/Carrier/Tablerate.php +++ b/app/code/core/Mage/Shipping/Model/Resource/Carrier/Tablerate.php @@ -267,8 +267,7 @@ public function uploadAndImport(Varien_Object $object) $adapter->commit(); if ($this->_importErrors) { - $error = Mage::helper('shipping')->__('%1$d records have been imported. See the following list of errors for each record that has not been imported: %2$s', - $this->_importedRows, implode(" \n", $this->_importErrors)); + $error = Mage::helper('shipping')->__('%1$d records have been imported. See the following list of errors for each record that has not been imported: %2$s', $this->_importedRows, implode(" \n", $this->_importErrors)); Mage::throwException($error); } @@ -349,8 +348,7 @@ protected function _getImportRow($row, $rowNumber = 0) { // validate row if (count($row) < 5) { - $this->_importErrors[] = Mage::helper('shipping')->__('Invalid Table Rates format in the Row #%s', - $rowNumber); + $this->_importErrors[] = Mage::helper('shipping')->__('Invalid Table Rates format in the Row #%s', $rowNumber); return false; } @@ -367,8 +365,7 @@ protected function _getImportRow($row, $rowNumber = 0) } elseif ($row[0] == '*' || $row[0] == '') { $countryId = '0'; } else { - $this->_importErrors[] = Mage::helper('shipping')->__('Invalid Country "%s" in the Row #%s.', - $row[0], $rowNumber); + $this->_importErrors[] = Mage::helper('shipping')->__('Invalid Country "%s" in the Row #%s.', $row[0], $rowNumber); return false; } @@ -378,8 +375,7 @@ protected function _getImportRow($row, $rowNumber = 0) } elseif ($row[1] == '*' || $row[1] == '') { $regionId = 0; } else { - $this->_importErrors[] = Mage::helper('shipping')->__('Invalid Region/State "%s" in the Row #%s.', - $row[1], $rowNumber); + $this->_importErrors[] = Mage::helper('shipping')->__('Invalid Region/State "%s" in the Row #%s.', $row[1], $rowNumber); return false; } @@ -393,24 +389,21 @@ protected function _getImportRow($row, $rowNumber = 0) // validate condition value $value = $this->_parseDecimalValue($row[3]); if ($value === false) { - $this->_importErrors[] = Mage::helper('shipping')->__('Invalid %s "%s" in the Row #%s.', - $this->_getConditionFullName($this->_importConditionName), $row[3], $rowNumber); + $this->_importErrors[] = Mage::helper('shipping')->__('Invalid %s "%s" in the Row #%s.', $this->_getConditionFullName($this->_importConditionName), $row[3], $rowNumber); return false; } // validate price $price = $this->_parseDecimalValue($row[4]); if ($price === false) { - $this->_importErrors[] = Mage::helper('shipping')->__('Invalid Shipping Price "%s" in the Row #%s.', - $row[4], $rowNumber); + $this->_importErrors[] = Mage::helper('shipping')->__('Invalid Shipping Price "%s" in the Row #%s.', $row[4], $rowNumber); return false; } // protect from duplicate $hash = sprintf("%s-%d-%s-%F", $countryId, $regionId, $zipCode, $value); if (isset($this->_importUniqueHash[$hash])) { - $this->_importErrors[] = Mage::helper('shipping')->__('Duplicate Row #%s (Country "%s", Region/State "%s", Zip "%s" and Value "%s").', - $rowNumber, $row[0], $row[1], $zipCode, $value); + $this->_importErrors[] = Mage::helper('shipping')->__('Duplicate Row #%s (Country "%s", Region/State "%s", Zip "%s" and Value "%s").', $rowNumber, $row[0], $row[1], $zipCode, $value); return false; } $this->_importUniqueHash[$hash] = true; diff --git a/app/code/core/Mage/Shipping/Model/Shipping.php b/app/code/core/Mage/Shipping/Model/Shipping.php index 06478f08b8..24d3fe787a 100644 --- a/app/code/core/Mage/Shipping/Model/Shipping.php +++ b/app/code/core/Mage/Shipping/Model/Shipping.php @@ -147,12 +147,13 @@ public function collectRates(Mage_Shipping_Model_Rate_Request $request) /** * Collect rates of given carrier * - * @param string $carrierCode + * @param string $carrierCode * @param Mage_Shipping_Model_Rate_Request $request * @return Mage_Shipping_Model_Shipping */ public function collectCarrierRates($carrierCode, $request) { + /* @var $carrier Mage_Shipping_Model_Carrier_Abstract */ $carrier = $this->getCarrierByCode($carrierCode, $request->getStoreId()); if (!$carrier) { return $this; @@ -164,7 +165,7 @@ public function collectCarrierRates($carrierCode, $request) } /* * Result will be false if the admin set not to show the shipping module - * if the devliery country is not within specific countries + * if the delivery country is not within specific countries */ if (false !== $result){ if (!$result instanceof Mage_Shipping_Model_Rate_Result_Error) { @@ -211,6 +212,8 @@ public function collectRatesByAddress(Varien_Object $address, $limitCarrier = nu $request->setPackageCurrency(Mage::app()->getStore()->getCurrentCurrency()); $request->setLimitCarrier($limitCarrier); + $request->setBaseSubtotalInclTax($address->getBaseSubtotalInclTax()); + return $this->collectRates($request); } @@ -297,20 +300,20 @@ public function requestToShipment(Mage_Sales_Model_Order_Shipment $orderShipment $request->setShipperContactCompanyName($storeInfo->getName()); $request->setShipperContactPhoneNumber($storeInfo->getPhone()); $request->setShipperEmail($admin->getEmail()); - $request->setShipperAddressStreet($originStreet1 . ' ' . $originStreet2); + $request->setShipperAddressStreet(trim($originStreet1 . ' ' . $originStreet2)); $request->setShipperAddressStreet1($originStreet1); $request->setShipperAddressStreet2($originStreet2); $request->setShipperAddressCity(Mage::getStoreConfig(self::XML_PATH_STORE_CITY, $shipmentStoreId)); $request->setShipperAddressStateOrProvinceCode($shipperRegionCode); $request->setShipperAddressPostalCode(Mage::getStoreConfig(self::XML_PATH_STORE_ZIP, $shipmentStoreId)); $request->setShipperAddressCountryCode(Mage::getStoreConfig(self::XML_PATH_STORE_COUNTRY_ID, $shipmentStoreId)); - $request->setRecipientContactPersonName($address->getFirstname() . ' ' . $address->getLastname()); + $request->setRecipientContactPersonName(trim($address->getFirstname() . ' ' . $address->getLastname())); $request->setRecipientContactPersonFirstName($address->getFirstname()); $request->setRecipientContactPersonLastName($address->getLastname()); $request->setRecipientContactCompanyName($address->getCompany()); $request->setRecipientContactPhoneNumber($address->getTelephone()); $request->setRecipientEmail($address->getEmail()); - $request->setRecipientAddressStreet($address->getStreetFull()); + $request->setRecipientAddressStreet(trim($address->getStreet1() . ' ' . $address->getStreet2())); $request->setRecipientAddressStreet1($address->getStreet1()); $request->setRecipientAddressStreet2($address->getStreet2()); $request->setRecipientAddressCity($address->getCity()); diff --git a/app/code/core/Mage/Tag/Model/Resource/Indexer/Summary.php b/app/code/core/Mage/Tag/Model/Resource/Indexer/Summary.php index 0a494e0c11..080c8b1928 100755 --- a/app/code/core/Mage/Tag/Model/Resource/Indexer/Summary.php +++ b/app/code/core/Mage/Tag/Model/Resource/Indexer/Summary.php @@ -144,7 +144,7 @@ public function reindexAll() public function aggregate($tagIds = null) { $writeAdapter = $this->_getWriteAdapter(); - $writeAdapter->beginTransaction(); + $this->beginTransaction(); try { if (!empty($tagIds)) { @@ -198,7 +198,8 @@ public function aggregate($tagIds = null) ->group(array( 'tr.tag_id', 'tr.store_id' - )); + )) + ->where('tr.active = 1'); $statusCond = $writeAdapter->quoteInto('=?', Mage_Catalog_Model_Product_Status::STATUS_ENABLED); $this->_addAttributeToSelect($select, 'status', 'e.entity_id', 'cs.store_id', $statusCond); @@ -245,7 +246,8 @@ public function aggregate($tagIds = null) $agregateSelect = $writeAdapter->select(); $agregateSelect->from($this->getTable('tag/relation'), $selectedFields) - ->group('tag_id'); + ->group('tag_id') + ->where('active = 1'); if (!empty($tagIds)) { $agregateSelect->where('tag_id IN(?)', $tagIds); @@ -254,12 +256,12 @@ public function aggregate($tagIds = null) $writeAdapter->query( $agregateSelect->insertFromSelect($this->getTable('tag/summary'), array_keys($selectedFields)) ); + $this->commit(); } catch (Exception $e) { - $writeAdapter->rollBack(); + $this->rollBack(); throw $e; } - $writeAdapter->commit(); return $this; } } diff --git a/app/code/core/Mage/Tag/Model/Resource/Tag.php b/app/code/core/Mage/Tag/Model/Resource/Tag.php index b938fe12d3..2becd70de7 100755 --- a/app/code/core/Mage/Tag/Model/Resource/Tag.php +++ b/app/code/core/Mage/Tag/Model/Resource/Tag.php @@ -203,7 +203,8 @@ protected function _getAggregationPerStoreView($tagId) array() ) ->group('main.store_id') - ->where('main.tag_id = :tag_id'); + ->where('main.tag_id = :tag_id') + ->where('main.active = 1'); $selectHistoricalResult = $readAdapter->fetchAll($selectHistorical, array('tag_id' => $tagId)); @@ -270,7 +271,8 @@ protected function _getGlobalAggregation($tagId) 'product_website.website_id = store.website_id AND product_website.product_id = main.product_id', array() ) - ->where('main.tag_id = :tag_id'); + ->where('main.tag_id = :tag_id') + ->where('main.active = 1'); $result['historical_uses'] = (int) $readAdapter->fetchOne($selectHistoricalGlobal, array('tag_id' => $tagId)); return $result; diff --git a/app/code/core/Mage/Tag/controllers/IndexController.php b/app/code/core/Mage/Tag/controllers/IndexController.php index 0233f0cbe4..cbb1f7035f 100644 --- a/app/code/core/Mage/Tag/controllers/IndexController.php +++ b/app/code/core/Mage/Tag/controllers/IndexController.php @@ -112,11 +112,9 @@ protected function _extractTags($tagNamesInString) */ protected function _cleanTags(array $tagNamesArr) { - $helper = Mage::helper('core'); foreach( $tagNamesArr as $key => $tagName ) { $tagNamesArr[$key] = trim($tagNamesArr[$key], '\''); $tagNamesArr[$key] = trim($tagNamesArr[$key]); - $tagNamesArr[$key] = $helper->escapeHtml($tagNamesArr[$key]); if( $tagNamesArr[$key] == '' ) { unset($tagNamesArr[$key]); } @@ -133,28 +131,35 @@ protected function _cleanTags(array $tagNamesArr) protected function _fillMessageBox($counter) { $session = Mage::getSingleton('catalog/session'); + $helper = Mage::helper('core'); if (count($counter[Mage_Tag_Model_Tag::ADD_STATUS_NEW])) { - $session->addSuccess($this->__('%s tag(s) have been accepted for moderation.', - count($counter[Mage_Tag_Model_Tag::ADD_STATUS_NEW])) + $session->addSuccess( + $this->__('%s tag(s) have been accepted for moderation.', count($counter[Mage_Tag_Model_Tag::ADD_STATUS_NEW])) ); } if (count($counter[Mage_Tag_Model_Tag::ADD_STATUS_EXIST])) { foreach ($counter[Mage_Tag_Model_Tag::ADD_STATUS_EXIST] as $tagName) { - $session->addNotice($this->__('Tag "%s" has already been added to the product.' ,$tagName)); + $session->addNotice( + $this->__('Tag "%s" has already been added to the product.' , $helper->escapeHtml($tagName)) + ); } } if (count($counter[Mage_Tag_Model_Tag::ADD_STATUS_SUCCESS])) { foreach ($counter[Mage_Tag_Model_Tag::ADD_STATUS_SUCCESS] as $tagName) { - $session->addSuccess($this->__('Tag "%s" has been added to the product.' ,$tagName)); + $session->addSuccess( + $this->__('Tag "%s" has been added to the product.' ,$helper->escapeHtml($tagName)) + ); } } if (count($counter[Mage_Tag_Model_Tag::ADD_STATUS_REJECTED])) { foreach ($counter[Mage_Tag_Model_Tag::ADD_STATUS_REJECTED] as $tagName) { - $session->addNotice($this->__('Tag "%s" has been rejected by administrator.' ,$tagName)); + $session->addNotice( + $this->__('Tag "%s" has been rejected by administrator.' ,$helper->escapeHtml($tagName)) + ); } } } diff --git a/app/code/core/Mage/Tag/etc/config.xml b/app/code/core/Mage/Tag/etc/config.xml index 14955ab1ae..be4ccece23 100644 --- a/app/code/core/Mage/Tag/etc/config.xml +++ b/app/code/core/Mage/Tag/etc/config.xml @@ -61,7 +61,7 @@ - Mage_Tag_Model_Customer_Resource + Mage_Tag_Model_Resource_Customer diff --git a/app/code/core/Mage/Tax/Helper/Data.php b/app/code/core/Mage/Tax/Helper/Data.php index fc614c3db0..83f6ecea3b 100644 --- a/app/code/core/Mage/Tax/Helper/Data.php +++ b/app/code/core/Mage/Tax/Helper/Data.php @@ -555,10 +555,10 @@ protected function _calculatePrice($price, $percent, $type) { $calculator = Mage::getSingleton('tax/calculation'); if ($type) { - $taxAmount = $calculator->calcTaxAmount($price, $percent, false); + $taxAmount = $calculator->calcTaxAmount($price, $percent, false, false); return $price + $taxAmount; } else { - $taxAmount = $calculator->calcTaxAmount($price, $percent, true); + $taxAmount = $calculator->calcTaxAmount($price, $percent, true, false); return $price - $taxAmount; } } diff --git a/app/code/core/Mage/Tax/Model/Calculation.php b/app/code/core/Mage/Tax/Model/Calculation.php index 12fcbe3214..117c93bd05 100644 --- a/app/code/core/Mage/Tax/Model/Calculation.php +++ b/app/code/core/Mage/Tax/Model/Calculation.php @@ -70,7 +70,7 @@ public function setCustomer(Mage_Customer_Model_Customer $customer) public function getDefaultCustomerTaxClass($store = null) { if ($this->_defaultCustomerTaxClass === null) { - $defaultCustomerGroup = Mage::getStoreConfig(Mage_Customer_Model_Group::XML_PATH_DEFAULT_ID, $store); + $defaultCustomerGroup = Mage::helper('customer')->getDefaultCustomerGroupId($store); $this->_defaultCustomerTaxClass = Mage::getModel('customer/group')->getTaxClassId($defaultCustomerGroup); } return $this->_defaultCustomerTaxClass; diff --git a/app/code/core/Mage/Tax/Model/Resource/Class.php b/app/code/core/Mage/Tax/Model/Resource/Class.php index 459956edb5..102232ee46 100755 --- a/app/code/core/Mage/Tax/Model/Resource/Class.php +++ b/app/code/core/Mage/Tax/Model/Resource/Class.php @@ -52,7 +52,7 @@ protected function _initUniqueFields() { $this->_uniqueFields = array(array( 'field' => array('class_type', 'class_name'), - 'title' => Mage::helper('tax')->__('An error occurred while saving this tax class. A class with the same name already exists.'), + 'title' => Mage::helper('tax')->__('An error occurred while saving this tax class. A class with the same name'), )); return $this; } diff --git a/app/code/core/Mage/Tax/Model/Sales/Total/Quote/Subtotal.php b/app/code/core/Mage/Tax/Model/Sales/Total/Quote/Subtotal.php index af707fc90d..80c0013e7e 100644 --- a/app/code/core/Mage/Tax/Model/Sales/Total/Quote/Subtotal.php +++ b/app/code/core/Mage/Tax/Model/Sales/Total/Quote/Subtotal.php @@ -64,8 +64,18 @@ class Mage_Tax_Model_Sales_Total_Quote_Subtotal extends Mage_Sales_Model_Quote_A */ protected $_storeTaxRequest = null; + /** + * Quote store + * + * @var Mage_Core_Model_Store + */ protected $_store; + /** + * Rounding deltas for prices + * + * @var array + */ protected $_roundingDeltas = array(); /** @@ -588,10 +598,10 @@ protected function _sameRateAsStore($request) protected function _deltaRound($price, $rate, $direction, $type='regular') { if ($price) { - $rate = (string) $rate; - $type = $type.$direction; - $delta = isset($this->_roundingDeltas[$type][$rate]) ? $this->_roundingDeltas[$type][$rate] : 0; - $price += $delta; + $rate = (string) $rate; + $type = $type . $direction; + $delta = isset($this->_roundingDeltas[$type][$rate]) ? $this->_roundingDeltas[$type][$rate] : 0; + $price += $delta; $this->_roundingDeltas[$type][$rate] = $price - $this->_calculator->round($price); $price = $this->_calculator->round($price); } diff --git a/app/code/core/Mage/Tax/Model/Sales/Total/Quote/Tax.php b/app/code/core/Mage/Tax/Model/Sales/Total/Quote/Tax.php index 8e197ee9d8..f4a4cfa329 100644 --- a/app/code/core/Mage/Tax/Model/Sales/Total/Quote/Tax.php +++ b/app/code/core/Mage/Tax/Model/Sales/Total/Quote/Tax.php @@ -454,6 +454,7 @@ protected function _rowBaseCalculation(Mage_Sales_Model_Quote_Address $address, $child->getBaseTaxAmount(), $rate ); + $child->setTaxRates($applied); } $this->_recalculateParent($item); } @@ -474,6 +475,7 @@ protected function _rowBaseCalculation(Mage_Sales_Model_Quote_Address $address, $item->getBaseTaxAmount(), $rate ); + $item->setTaxRates($applied); } } @@ -693,12 +695,12 @@ protected function _aggregateTaxPerRate($item, $rate, &$taxGroups) protected function _deltaRound($price, $rate, $direction, $type='regular') { if ($price) { - $rate = (string) $rate; - $type = $type.$direction; - $delta = isset($this->_roundingDeltas[$type][$rate]) ? $this->_roundingDeltas[$type][$rate] : 0; - $price += $delta; + $rate = (string) $rate; + $type = $type . $direction; + $delta = isset($this->_roundingDeltas[$type][$rate]) ? $this->_roundingDeltas[$type][$rate] : 0; + $price += $delta; $this->_roundingDeltas[$type][$rate] = $price - $this->_calculator->round($price); - $price = $this->_calculator->round($price); + $price = $this->_calculator->round($price); } return $price; } diff --git a/app/code/core/Mage/Usa/Block/Adminhtml/Dhl/Unitofmeasure.php b/app/code/core/Mage/Usa/Block/Adminhtml/Dhl/Unitofmeasure.php new file mode 100644 index 0000000000..a8d6519d7c --- /dev/null +++ b/app/code/core/Mage/Usa/Block/Adminhtml/Dhl/Unitofmeasure.php @@ -0,0 +1,82 @@ + + */ +class Mage_Usa_Block_Adminhtml_Dhl_Unitofmeasure extends Mage_Adminhtml_Block_System_Config_Form_Field +{ + + /** + * Define params and variables + * + * @return void + */ + public function _construct() + { + parent::_construct(); + + $carrierModel = Mage::getSingleton('usa/shipping_carrier_dhl_international'); + + $this->setInch($this->jsQuoteEscape($carrierModel->getCode('unit_of_dimension_cut', 'I'))); + $this->setCm($this->jsQuoteEscape($carrierModel->getCode('unit_of_dimension_cut', 'C'))); + + $this->setHeight($this->jsQuoteEscape($carrierModel->getCode('dimensions', 'height'))); + $this->setDepth($this->jsQuoteEscape($carrierModel->getCode('dimensions', 'depth'))); + $this->setWidth($this->jsQuoteEscape($carrierModel->getCode('dimensions', 'width'))); + + $kgWeight = 70; + + $this->setDivideOrderWeightNoteKg( + $this->jsQuoteEscape($this->__('Allows breaking total order weight into smaller pieces if it exeeds %s %s to ensure accurate calculation of shipping charges.', $kgWeight, 'kg')) + ); + + $weight = round( + Mage::helper('usa')->convertMeasureWeight( + $kgWeight, Zend_Measure_Weight::KILOGRAM, Zend_Measure_Weight::POUND), 3); + + $this->setDivideOrderWeightNoteLbp( + $this->jsQuoteEscape($this->__('Allows breaking total order weight into smaller pieces if it exeeds %s %s to ensure accurate calculation of shipping charges.', $weight, 'pounds')) + ); + + $this->setTemplate('usa/dhl/unitofmeasure.phtml'); + } + + /** + * Retrieve Element HTML fragment + * + * @param Varien_Data_Form_Element_Abstract $element + * @return string + */ + protected function _getElementHtml(Varien_Data_Form_Element_Abstract $element) + { + return parent::_getElementHtml($element) . $this->renderView(); + } +} diff --git a/app/code/core/Mage/Usa/Model/Shipping/Carrier/Abstract.php b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Abstract.php index 17a4a7e9bc..5cdebbc2f4 100644 --- a/app/code/core/Mage/Usa/Model/Shipping/Carrier/Abstract.php +++ b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Abstract.php @@ -128,7 +128,18 @@ public function isShippingLabelsAvailable() } /** - * Processing additional validation to check is carrier applicable. + * Some carriers need to override this method to get the correct quote + * + * @param Mage_Shipping_Model_Rate_Request $request + * @return array + */ + public function getAllItems(Mage_Shipping_Model_Rate_Request $request) + { + return $request->getAllItems(); + } + + /** + * Processing additional validation to check if carrier applicable. * * @param Mage_Shipping_Model_Rate_Request $request * @return Mage_Shipping_Model_Carrier_Abstract|Mage_Shipping_Model_Rate_Result_Error|boolean @@ -136,7 +147,7 @@ public function isShippingLabelsAvailable() public function proccessAdditionalValidation(Mage_Shipping_Model_Rate_Request $request) { //Skip by item validation if there is no items in request - if(!count($request->getAllItems())) { + if(!count($this->getAllItems($request))) { return $this; } @@ -146,9 +157,9 @@ public function proccessAdditionalValidation(Mage_Shipping_Model_Rate_Request $r $defaultErrorMsg = Mage::helper('shipping')->__('The shipping module is not available.'); $showMethod = $this->getConfigData('showmethod'); - foreach ($request->getAllItems() as $item) { + foreach ($this->getAllItems($request) as $item) { if ($item->getProduct() && $item->getProduct()->getId()) { - if ($item->getProduct()->getWeight() > $maxAllowedWeight) { + if ($item->getProduct()->getWeight() * $item->getProduct()->getQty() > $maxAllowedWeight) { $errorMsg = ($configErrorMsg) ? $configErrorMsg : $defaultErrorMsg; break; } diff --git a/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl.php b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl.php index c56d97e2f0..72f9b1c07f 100644 --- a/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl.php +++ b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl.php @@ -381,6 +381,8 @@ public function setRequest(Varien_Object $request) $r->setPackageId($request->getPackageId()); } + $r->setBaseSubtotalInclTax($request->getBaseSubtotalInclTax()); + $this->_rawRequest = $r; return $this; } @@ -796,8 +798,7 @@ protected function _parseXmlResponse($response) $code = (string)$xml->Faults->Fault->Code; $description = $xml->Faults->Fault->Description; $context = $xml->Faults->Fault->Context; - $this->_errors[$code] = Mage::helper('usa')->__('Error #%s : %s (%s)', $code, $description, - $context); + $this->_errors[$code] = Mage::helper('usa')->__('Error #%s : %s (%s)', $code, $description, $context); } else { if ($r->getDestCountryId() == self::USA_COUNTRY_ID) { if ($xml->Shipment) { @@ -855,14 +856,12 @@ protected function _parseXmlResponse($response) $rate->setPrice($data['price_total']); $result->append($rate); } - } else { - foreach ($this->_errors as $errorText) { - $error = Mage::getModel('shipping/rate_result_error'); - $error->setCarrier('dhl'); - $error->setCarrierTitle($this->getConfigData('title')); - $error->setErrorMessage($this->getConfigData('specificerrmsg')); - $result->append($error); - } + } else if (!empty($this->_errors)) { + $error = Mage::getModel('shipping/rate_result_error'); + $error->setCarrier('dhl'); + $error->setCarrierTitle($this->getConfigData('title')); + $error->setErrorMessage($this->getConfigData('specificerrmsg')); + $result->append($error); } return $result; } @@ -1198,15 +1197,9 @@ protected function _parseXmlTrackingResponse($trackings, $response) } else { $description = (string)$txml->Result->Desc; if ($description) - $errorArr[$tracknum] = Mage::helper('usa')->__( - 'Error #%s: %s', - $code, - $description - ); + $errorArr[$tracknum] = Mage::helper('usa')->__('Error #%s: %s', $code, $description); else - $errorArr[$tracknum] = Mage::helper('usa')->__( - 'Unable to retrieve tracking' - ); + $errorArr[$tracknum] = Mage::helper('usa')->__('Unable to retrieve tracking'); } } else { $errorArr[$tracknum] = Mage::helper('usa')->__('Unable to retrieve tracking'); @@ -1371,6 +1364,8 @@ protected function _mapRequestToShipment(Varien_Object $request) $request->setPackageCustomsValue($customsValue); $request->setFreeMethodWeight(0); $request->setDhlShipmentType($request->getPackagingType()); + + $request->setBaseSubtotalInclTax($request->getBaseSubtotalInclTax()); } /** diff --git a/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International.php b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International.php new file mode 100644 index 0000000000..24456206b8 --- /dev/null +++ b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International.php @@ -0,0 +1,1490 @@ + + */ +class Mage_Usa_Model_Shipping_Carrier_Dhl_International + extends Mage_Usa_Model_Shipping_Carrier_Abstract + implements Mage_Shipping_Model_Carrier_Interface +{ + /** + * Carrier Product indicator + */ + const DHL_CONTENT_TYPE_DOC = 'D'; + const DHL_CONTENT_TYPE_NON_DOC = 'N'; + + /** + * Container types that could be customized + * + * @var array + */ + protected $_customizableContainerTypes = array(self::DHL_CONTENT_TYPE_NON_DOC); + + /** + * Code of the carrier + */ + const CODE = 'dhlint'; + + /** + * Rate request data + * + * @var Mage_Shipping_Model_Rate_Request|null + */ + protected $_request = null; + + /** + * Raw rate request data + * + * @var Varien_Object|null + */ + protected $_rawRequest = null; + + /** + * Rate result data + * + * @var Mage_Shipping_Model_Rate_Result|null + */ + protected $_result = null; + + /** + * Countries parameters data + * + * @var SimpleXMLElement|null + */ + protected $_countryParams = null; + + /** + * Errors placeholder + * + * @var array + */ + protected $_errors = array(); + + /** + * Dhl rates result + * + * @var array + */ + protected $_rates = array(); + + /** + * Store Id + * + * @var int|null + */ + protected $_storeId = null; + + /** + * Carrier's code + * + * @var string + */ + protected $_code = self::CODE; + + /** + * Free Method config path + * + * @var string + */ + protected $_freeMethod = 'free_method_nondoc'; + + /** + * Max weight without fee + * + * @var int + */ + protected $_maxWeight = 70; + + /** + * Request variables array + * + * @var array + */ + protected $_requestVariables = array( + 'id' => array('code' => 'dhl_id', 'setCode' => 'id'), + 'password' => array('code' => 'dhl_password', 'setCode' => 'password'), + 'account' => array('code' => 'dhl_account', 'setCode' => 'account_nbr'), + 'shipping_key' => array('code' => 'dhl_shipping_key', 'setCode' => 'shipping_key'), + 'shipping_intlkey' => array('code' => 'dhl_shipping_intl_key', 'setCode' => 'shipping_intl_key'), + 'shipment_type' => array('code' => 'dhl_shipment_type', 'setCode' => 'shipment_type'), + 'dutiable' => array('code' => 'dhl_dutiable', 'setCode' => 'dutiable'), + 'dutypaymenttype' => array('code' => 'dhl_duty_payment_type', 'setCode' => 'duty_payment_type'), + 'contentdesc' => array('code' => 'dhl_content_desc', 'setCode' => 'content_desc') + ); + + /** + * Dhl International Class constructor + * + * Sets necessary data + */ + protected function _construct() + { + if ($this->getConfigData('content_type') == self::DHL_CONTENT_TYPE_DOC) { + $this->_freeMethod = 'free_method_doc'; + } + } + + /** + * Returns value of given variable + * + * @param mixed $origValue + * @param string $pathToValue + * @return mixed + */ + protected function _getDefaultValue($origValue, $pathToValue) + { + if (!$origValue) { + $origValue = Mage::getStoreConfig( + $pathToValue, + $this->_storeId + ); + } + + return $origValue; + } + + /** + * Collect and get rates + * + * @param Mage_Shipping_Model_Rate_Request $request + * @return bool|Mage_Shipping_Model_Rate_Result|null + */ + public function collectRates(Mage_Shipping_Model_Rate_Request $request) + { + if (!$this->getConfigFlag($this->_activeFlag)) { + return false; + } + + $requestDhl = clone $request; + $this->_storeId = $requestDhl->getStoreId(); + + $origCompanyName = $this->_getDefaultValue( + $requestDhl->getOrigCompanyName(), + Mage_Core_Model_Store::XML_PATH_STORE_STORE_NAME + ); + $origCountryId = $this->_getDefaultValue( + $requestDhl->getOrigCountryId(), + Mage_Shipping_Model_Shipping::XML_PATH_STORE_COUNTRY_ID + ); + $origState = $this->_getDefaultValue( + $requestDhl->getOrigState(), + Mage_Shipping_Model_Shipping::XML_PATH_STORE_REGION_ID + ); + $origCity = $this->_getDefaultValue( + $requestDhl->getOrigCity(), + Mage_Shipping_Model_Shipping::XML_PATH_STORE_CITY + ); + $origPostcode = $this->_getDefaultValue( + $requestDhl->getOrigPostcode(), + Mage_Shipping_Model_Shipping::XML_PATH_STORE_ZIP + ); + + $requestDhl->setOrigCompanyName($origCompanyName) + ->setCountryId($origCountryId) + ->setOrigState($origState) + ->setOrigCity($origCity) + ->setOrigPostal($origPostcode); + $this->setRequest($requestDhl); + + $this->_result = $this->_getQuotes(); + + $this->_updateFreeMethodQuote($request); + + return $this->_result; + } + + /** + * Set Free Method Request + * + * @param string $freeMethod + * @return void + */ + protected function _setFreeMethodRequest($freeMethod) + { + $rawRequest = $this->_rawRequest; + + $rawRequest->setFreeMethodRequest(true); + $freeWeight = $this->getTotalNumOfBoxes($rawRequest->getFreeMethodWeight()); + $rawRequest->setWeight($freeWeight); + $rawRequest->setService($freeMethod); + } + + /** + * Returns request result + * + * @return Mage_Shipping_Model_Rate_Result|null + */ + public function getResult() + { + return $this->_result; + } + + protected function _addParams($requestObject) + { + $request = $this->_request; + foreach ($this->_requestVariables as $code => $objectCode) { + if ($request->getDhlId()) { + $value = $request->getData($objectCode['code']); + } else { + $value = $this->getConfigData($code); + } + $requestObject->setData($objectCode['setCode'], $value); + } + return $requestObject; + } + + /** + * Prepare and set request in property of current instance + * + * @param Varien_Object $request + * @return Mage_Usa_Model_Shipping_Carrier_Dhl + */ + public function setRequest(Varien_Object $request) + { + $this->_request = $request; + $this->_storeId = $request->getStoreId(); + + $requestObject = new Varien_Object(); + + $requestObject->setIsGenerateLabelReturn($request->getIsGenerateLabelReturn()); + + $requestObject->setStoreId($request->getStoreId()); + + if ($request->getLimitMethod()) { + $requestObject->setService($request->getLimitMethod()); + } + + $requestObject = $this->_addParams($requestObject); + + if ($request->getDestPostcode()) { + $requestObject->setDestPostal($request->getDestPostcode()); + } + + $requestObject->setOrigCountry( + $this->_getDefaultValue( + $request->getOrigCountry(), Mage_Shipping_Model_Shipping::XML_PATH_STORE_COUNTRY_ID) + ) + ->setOrigCountryId( + $this->_getDefaultValue( + $request->getOrigCountryId(), Mage_Shipping_Model_Shipping::XML_PATH_STORE_COUNTRY_ID) + ); + + $shippingWeight = $request->getPackageWeight(); + + $requestObject->setValue(round($request->getPackageValue(), 2)) + ->setValueWithDiscount($request->getPackageValueWithDiscount()) + ->setCustomsValue($request->getPackageCustomsValue()) + ->setDestStreet( + Mage::helper('core/string')->substr(str_replace("\n", '', $request->getDestStreet()), 0, 35)) + ->setDestStreetLine2($request->getDestStreetLine2()) + ->setDestCity($request->getDestCity()) + ->setOrigCompanyName($request->getOrigCompanyName()) + ->setOrigCity($request->getOrigCity()) + ->setOrigPhoneNumber($request->getOrigPhoneNumber()) + ->setOrigPersonName($request->getOrigPersonName()) + ->setOrigEmail(Mage::getStoreConfig('trans_email/ident_general/email', $requestObject->getStoreId())) + ->setOrigCity($request->getOrigCity()) + ->setOrigPostal($request->getOrigPostal()) + ->setOrigStreetLine2($request->getOrigStreetLine2()) + ->setDestPhoneNumber($request->getDestPhoneNumber()) + ->setDestPersonName($request->getDestPersonName()) + ->setDestCompanyName($request->getDestCompanyName()); + + $originStreet2 = Mage::getStoreConfig( + Mage_Shipping_Model_Shipping::XML_PATH_STORE_ADDRESS2, $requestObject->getStoreId()); + + $requestObject->setOrigStreet($request->getOrigStreet() ? $request->getOrigStreet() : $originStreet2); + + if (is_numeric($request->getOrigState())) { + $requestObject->setOrigState(Mage::getModel('directory/region')->load($request->getOrigState())->getCode()); + } else { + $requestObject->setOrigState($request->getOrigState()); + } + + if ($request->getDestCountryId()) { + $destCountry = $request->getDestCountryId(); + } else { + $destCountry = self::USA_COUNTRY_ID; + } + + // for DHL, Puerto Rico state for US will assume as Puerto Rico country + // for Puerto Rico, dhl will ship as international + if ($destCountry == self::USA_COUNTRY_ID && ($request->getDestPostcode() == '00912' + || $request->getDestRegionCode() == self::PUERTORICO_COUNTRY_ID) + ) { + $destCountry = self::PUERTORICO_COUNTRY_ID; + } + + $requestObject->setDestCountryId($destCountry) + ->setDestState($request->getDestRegionCode()) + ->setWeight($shippingWeight) + ->setFreeMethodWeight($request->getFreeMethodWeight()) + ->setOrderShipment($request->getOrderShipment()); + + if ($request->getPackageId()) { + $requestObject->setPackageId($request->getPackageId()); + } + + $requestObject->setBaseSubtotalInclTax($request->getBaseSubtotalInclTax()); + + $this->_rawRequest = $requestObject; + return $this; + } + + /** + * Get allowed shipping methods + * + * @return array + */ + public function getAllowedMethods() + { + $contentType = $this->getConfigData('content_type'); + $allowedMethods = array(); + switch ($contentType) { + case self::DHL_CONTENT_TYPE_DOC: + $allowedMethods = explode(',', $this->getConfigData('doc_methods')); + break; + + case self::DHL_CONTENT_TYPE_NON_DOC: + $allowedMethods = explode(',', $this->getConfigData('nondoc_methods')); + break; + default: + Mage::throwException(Mage::helper('usa')->__('Wrong Content Type.')); + } + $methods = array(); + foreach ($allowedMethods as $method) { + $methods[$method] = $this->getDhlProductTitle($method); + } + return $methods; + } + + /** + * Get configuration data of carrier + * + * @param strin $type + * @param string $code + * @return array|bool + */ + public function getCode($type, $code = '') + { + $codes = array( + 'unit_of_measure' => array( + 'L' => Mage::helper('usa')->__('Pounds'), + 'K' => Mage::helper('usa')->__('Kilograms'), + ), + 'unit_of_dimension' => array( + 'I' => Mage::helper('usa')->__('Inches'), + 'C' => Mage::helper('usa')->__('Centimeters'), + ), + 'unit_of_dimension_cut' => array( + 'I' => Mage::helper('usa')->__('inch'), + 'C' => Mage::helper('usa')->__('cm'), + ), + 'dimensions' => array( + 'height' => Mage::helper('usa')->__('Height'), + 'depth' => Mage::helper('usa')->__('Depth'), + 'width' => Mage::helper('usa')->__('Width'), + ), + 'size' => array( + '0' => Mage::helper('usa')->__('Regular'), + '1' => Mage::helper('usa')->__('Specific'), + ), + 'dimensions_variables' => array( + 'L' => Zend_Measure_Weight::POUND, + 'LB' => Zend_Measure_Weight::POUND, + 'POUND' => Zend_Measure_Weight::POUND, + 'K' => Zend_Measure_Weight::KILOGRAM, + 'KG' => Zend_Measure_Weight::KILOGRAM, + 'KILOGRAM' => Zend_Measure_Weight::KILOGRAM, + 'I' => Zend_Measure_Length::INCH, + 'IN' => Zend_Measure_Length::INCH, + 'INCH' => Zend_Measure_Length::INCH, + 'C' => Zend_Measure_Length::CENTIMETER, + 'CM' => Zend_Measure_Length::CENTIMETER, + 'CENTIMETER'=> Zend_Measure_Length::CENTIMETER, + + ) + ); + + if (!isset($codes[$type])) { + return false; + } elseif ('' === $code) { + return $codes[$type]; + } + + if (!isset($codes[$type][$code])) { + return false; + } else { + return $codes[$type][$code]; + } + } + + /** + * Returns DHL shipment methods (depending on package content type, if necessary) + * + * @param string $doc Package content type (doc/non-doc) see DHL_CONTENT_TYPE_* constants + * @return array + */ + public function getDhlProducts($doc) + { + if ($doc == self::DHL_CONTENT_TYPE_DOC) { + // Documents shipping + return array( + '2' => Mage::helper('usa')->__('Easy shop'), + '5' => Mage::helper('usa')->__('Sprintline'), + '6' => Mage::helper('usa')->__('Secureline'), + '7' => Mage::helper('usa')->__('Express easy'), + '9' => Mage::helper('usa')->__('Europack'), + 'B' => Mage::helper('usa')->__('Break bulk express'), + 'C' => Mage::helper('usa')->__('Medical express'), + 'D' => Mage::helper('usa')->__('Express worldwide'), // product content code: DOX + 'U' => Mage::helper('usa')->__('Express worldwide'), // product content code: ECX + 'K' => Mage::helper('usa')->__('Express 9:00'), + 'L' => Mage::helper('usa')->__('Express 10:30'), + 'G' => Mage::helper('usa')->__('Domestic economy select'), + 'W' => Mage::helper('usa')->__('Economy select'), + 'I' => Mage::helper('usa')->__('Break bulk economy'), + 'N' => Mage::helper('usa')->__('Domestic express'), + 'O' => Mage::helper('usa')->__('Others'), + 'R' => Mage::helper('usa')->__('Globalmail business'), + 'S' => Mage::helper('usa')->__('Same day'), + 'T' => Mage::helper('usa')->__('Express 12:00'), + 'X' => Mage::helper('usa')->__('Express envelope'), + ); + } else { + // Services for shipping non-documents cargo + return array( + '1' => Mage::helper('usa')->__('Customer services'), + '3' => Mage::helper('usa')->__('Easy shop'), + '4' => Mage::helper('usa')->__('Jetline'), + '8' => Mage::helper('usa')->__('Express easy'), + 'P' => Mage::helper('usa')->__('Express worldwide'), + 'Q' => Mage::helper('usa')->__('Medical express'), + 'E' => Mage::helper('usa')->__('Express 9:00'), + 'F' => Mage::helper('usa')->__('Freight worldwide'), + 'H' => Mage::helper('usa')->__('Economy select'), + 'J' => Mage::helper('usa')->__('Jumbo box'), + 'M' => Mage::helper('usa')->__('Express 10:30'), + 'V' => Mage::helper('usa')->__('Europack'), + 'Y' => Mage::helper('usa')->__('Express 12:00'), + ); + } + } + + /** + * Returns title of DHL shipping method by its code + * + * @param string $code One-symbol code (see getDhlProducts()) + * @return bool + */ + public function getDhlProductTitle($code) + { + $contentType = $this->getConfigData('content_type'); + $dhlProducts = $this->getDhlProducts($contentType); + return isset($dhlProducts[$code]) ? $dhlProducts[$code] : false; + } + + /** + * Convert item weight to needed weight based on config weight unit dimensions + * + * @param float $weight + * @param bool $maxWeight + * @return float + */ + protected function _getWeight($weight, $maxWeight = false) + { + if ($maxWeight) { + $configWeightUnit = Zend_Measure_Weight::KILOGRAM; + } else { + $configWeightUnit = $this->getCode( + 'dimensions_variables', + strtoupper((string)$this->getConfigData('unit_of_measure')) + ); + } + + $countryWeightUnit = $this->getCode( + 'dimensions_variables', + strtoupper($this->_getWeightUnit()) + ); + + if ($configWeightUnit != $countryWeightUnit) { + $weight = round(Mage::helper('usa')->convertMeasureWeight( + $weight, + $configWeightUnit, + $countryWeightUnit + ), 3); + } + + return round($weight, 3); + } + + /** + * Prepare items to pieces + * + * @return array + */ + protected function _getAllItems() + { + $allItems = $this->_request->getAllItems(); + $fullItems = array(); + + foreach ($allItems as $item) { + if ($item->getProductType() == Mage_Catalog_Model_Product_Type::TYPE_BUNDLE + && $item->getProduct()->getShipmentType() + ) { + continue; + } + + $qty = $item->getQty(); + + if ($item->getParentItem()) { + if (!$item->getParentItem()->getProduct()->getShipmentType()) { + continue; + } + $qty = $item->getIsQtyDecimal() + ? $item->getParentItem()->getQty() + : $item->getParentItem()->getQty() * $item->getQty(); + } + + $itemWeight = $item->getWeight(); + if ($item->getIsQtyDecimal() && $item->getProductType() != Mage_Catalog_Model_Product_Type::TYPE_BUNDLE) { + $itemWeight = $itemWeight * $item->getQty(); + } + + if ($this->_getWeight($itemWeight) > $this->_getWeight($this->_maxWeight, true)) { + return array(); + } + + if (!$item->getParentItem() && $item->getIsQtyDecimal() + && $item->getProductType() != Mage_Catalog_Model_Product_Type::TYPE_BUNDLE + ) { + $qty = 1; + } + $fullItems = array_merge($fullItems, array_fill(0, $qty, $this->_getWeight($itemWeight))); + } + sort($fullItems); + + return $fullItems; + } + + /** + * Make pieces + * + * @param SimpleXMLElement $nodeBkgDetails + * @return void + */ + protected function _makePieces(SimpleXMLElement $nodeBkgDetails) + { + $divideOrderWeight = (string)$this->getConfigData('divide_order_weight'); + $nodePieces = $nodeBkgDetails->addChild('Pieces', '', ''); + $items = $this->_getAllItems(); + $numberOfPieces = 0; + + if ($divideOrderWeight && !empty($items)) { + $maxWeight = $this->_getWeight($this->_maxWeight, true); + $sumWeight = 0; + + $reverseOrderItems = $items; + arsort($reverseOrderItems); + + foreach ($reverseOrderItems as $key => $weight) { + if (!isset($items[$key])) { + continue; + } + unset($items[$key]); + $sumWeight = $weight; + foreach ($items as $key => $weight) { + if (($sumWeight + $weight) < $maxWeight) { + unset($items[$key]); + $sumWeight += $weight; + } elseif (($sumWeight + $weight) > $maxWeight) { + $numberOfPieces++; + $nodePiece = $nodePieces->addChild('Piece', '', ''); + $nodePiece->addChild('PieceID', $numberOfPieces); + $this->_addDimension($nodePiece); + $nodePiece->addChild('Weight', $sumWeight); + break; + } else { + unset($items[$key]); + $numberOfPieces++; + $sumWeight += $weight; + $nodePiece = $nodePieces->addChild('Piece', '', ''); + $nodePiece->addChild('PieceID', $numberOfPieces); + $this->_addDimension($nodePiece); + $nodePiece->addChild('Weight', $sumWeight); + $sumWeight = 0; + break; + } + } + } + if ($sumWeight > 0) { + $numberOfPieces++; + $nodePiece = $nodePieces->addChild('Piece', '', ''); + $nodePiece->addChild('PieceID', $numberOfPieces); + $this->_addDimension($nodePiece); + $nodePiece->addChild('Weight', $sumWeight); + } + } else { + $nodePiece = $nodePieces->addChild('Piece', '', ''); + $nodePiece->addChild('PieceID', 1); + $this->_addDimension($nodePiece); + $nodePiece->addChild('Weight', $this->_getWeight($this->_rawRequest->getWeight())); + } + + $handlingAction = $this->getConfigData('handling_action'); + if ($handlingAction == Mage_Shipping_Model_Carrier_Abstract::HANDLING_ACTION_PERORDER || !$numberOfPieces) { + $numberOfPieces = 1; + } + $this->_numBoxes = $numberOfPieces; + } + + /** + * Convert item dimension to needed dimension based on config dimension unit of measure + * + * @param float $dimension + * @return float + */ + protected function _getDimension($dimension) + { + $configWeightUnit = $this->getCode( + 'dimensions_variables', + strtoupper((string)$this->getConfigData('unit_of_measure')) + ); + + if ($configWeightUnit == Zend_Measure_Weight::POUND) { + $configWeightUnit = Zend_Measure_Length::INCH; + } else { + $configWeightUnit = Zend_Measure_Length::CENTIMETER; + } + + + $countryDimensionUnit = $this->getCode( + 'dimensions_variables', + strtoupper($this->_getDimensionUnit()) + ); + + if ($configWeightUnit != $countryDimensionUnit) { + $dimension = round(Mage::helper('usa')->convertMeasureDimension( + $dimension, + $configWeightUnit, + $countryDimensionUnit + ), 3); + } + + return round($dimension, 3); + } + + /** + * Add dimension to piece + * + * @param SimpleXMLElement $nodePiece + * @return void + */ + protected function _addDimension($nodePiece) + { + $sizeChecker = (string)$this->getConfigData('size'); + + $height = $this->_getDimension((string)$this->getConfigData('height')); + $depth = $this->_getDimension((string)$this->getConfigData('depth')); + $width = $this->_getDimension((string)$this->getConfigData('width')); + + if ($sizeChecker && $height && $depth && $width) { + $nodePiece->addChild('Height', $height); + $nodePiece->addChild('Depth', $depth); + $nodePiece->addChild('Width', $width); + } + } + + /** + * Get shipping quotes + * + * @return Mage_Core_Model_Abstract|Mage_Shipping_Model_Rate_Result + */ + protected function _getQuotes() + { + $rawRequest = $this->_rawRequest; + $xmlStr = '' + . ''; + $xml = new SimpleXMLElement($xmlStr); + $nodeGetQuote = $xml->addChild('GetQuote', '', ''); + $nodeRequest = $nodeGetQuote->addChild('Request'); + + $nodeServiceHeader = $nodeRequest->addChild('ServiceHeader'); + $nodeServiceHeader->addChild('SiteID', (string)$this->getConfigData('id')); + $nodeServiceHeader->addChild('Password', (string)$this->getConfigData('password')); + + $nodeFrom = $nodeGetQuote->addChild('From'); + $nodeFrom->addChild('CountryCode', $rawRequest->getOrigCountryId()); + $nodeFrom->addChild('Postalcode', $rawRequest->getOrigPostal()); + $nodeFrom->addChild('City', $rawRequest->getOrigCity()); + + $nodeBkgDetails = $nodeGetQuote->addChild('BkgDetails'); + $nodeBkgDetails->addChild('PaymentCountryCode', $rawRequest->getOrigCountryId()); + $nodeBkgDetails->addChild('Date', Varien_Date::now(true)); + $nodeBkgDetails->addChild('ReadyTime', 'PT' . (int)(string)$this->getConfigData('ready_time') . 'H00M'); + + $nodeBkgDetails->addChild('DimensionUnit', $this->_getDimensionUnit()); + $nodeBkgDetails->addChild('WeightUnit', $this->_getWeightUnit()); + + $this->_makePieces($nodeBkgDetails); + + $nodeBkgDetails->addChild('PaymentAccountNumber', (string)$this->getConfigData('account')); + + $nodeTo = $nodeGetQuote->addChild('To'); + $nodeTo->addChild('CountryCode', $rawRequest->getDestCountryId()); + $nodeTo->addChild('Postalcode', $rawRequest->getDestPostal()); + $nodeTo->addChild('City', $rawRequest->getDestCity()); + + if ($this->getConfigData('content_type') == self::DHL_CONTENT_TYPE_NON_DOC) { + // IsDutiable flag and Dutiable node indicates that cargo is not a documentation + $nodeBkgDetails->addChild('IsDutiable', 'Y'); + $nodeDutiable = $nodeGetQuote->addChild('Dutiable'); + $baseCurrencyCode = Mage::app()->getWebsite($this->_request->getWebsiteId())->getBaseCurrencyCode(); + $nodeDutiable->addChild('DeclaredCurrency', $baseCurrencyCode); + $nodeDutiable->addChild('DeclaredValue', $rawRequest->getValue()); + } + + $request = $xml->asXML(); + $request = utf8_encode($request); + $responseBody = $this->_getCachedQuotes($request); + if ($responseBody === null) { + $debugData = array('request' => $request); + try { + $client = new Varien_Http_Client(); + $client->setUri((string)$this->getConfigData('gateway_url')); + $client->setConfig(array('maxredirects' => 0, 'timeout' => 30)); + $client->setRawData($request); + $responseBody = $client->request(Varien_Http_Client::POST)->getBody(); + $debugData['result'] = $responseBody; + $this->_setCachedQuotes($request, $responseBody); + } catch (Exception $e) { + $this->_errors[$e->getCode()] = $e->getMessage(); + $responseBody = ''; + } + $this->_debug($debugData); + } + + return $this->_parseResponse($responseBody); + } + + /** + * Parse response from DHL web service + * + * @param string $response + * @return Mage_Shipping_Model_Rate_Result + */ + protected function _parseResponse($response) + { + $htmlTranslationTable = get_html_translation_table(HTML_ENTITIES); + unset($htmlTranslationTable['<'], $htmlTranslationTable['>'], $htmlTranslationTable['"']); + $response = str_replace(array_keys($htmlTranslationTable), array_values($htmlTranslationTable), $response); + + if (strlen(trim($response)) > 0) { + if (strpos(trim($response), 'Response->Status->ActionStatus) && $xml->Response->Status->ActionStatus == 'Error') + || (isset($xml->GetQuoteResponse->Note->Condition)) + ) { + if (isset($xml->Response->Status->Condition)) { + $nodeCondition = $xml->Response->Status->Condition; + } else { + $nodeCondition = $xml->GetQuoteResponse->Note->Condition; + } + + $code = isset($nodeCondition->ConditionCode) ? (string)$nodeCondition->ConditionCode : 0; + $data = isset($nodeCondition->ConditionData) ? (string)$nodeCondition->ConditionData : ''; + $this->_errors[$code] = Mage::helper('usa')->__('Error #%s : %s', $code, $data); + } else { + if (isset($xml->GetQuoteResponse->BkgDetails->QtdShp)) { + foreach ($xml->GetQuoteResponse->BkgDetails->QtdShp as $quotedShipment) { + $this->_addRate($quotedShipment); + } + } elseif (isset($xml->AirwayBillNumber)) { + $result = new Varien_Object(); + $result->setTrackingNumber((string)$xml->AirwayBillNumber); + $result->setShippingLabelContent(base64_decode((string)$xml->Barcodes->OriginDestnBarcode)); + return $result; + } else { + $this->_errors[] = Mage::helper('usa')->__('The response is in wrong format.'); + } + } + } + } else { + $this->_errors[] = Mage::helper('usa')->__('The response is in wrong format.'); + } + } + + /* @var $result Mage_Shipping_Model_Rate_Result */ + $result = Mage::getModel('shipping/rate_result'); + if ($this->_rates) { + foreach ($this->_rates as $rate) { + $method = $rate['service']; + $data = $rate['data']; + /* @var $rate Mage_Shipping_Model_Rate_Result_Method */ + $rate = Mage::getModel('shipping/rate_result_method'); + $rate->setCarrier(self::CODE); + $rate->setCarrierTitle($this->getConfigData('title')); + $rate->setMethod($method); + $rate->setMethodTitle($data['term']); + $rate->setCost($data['price_total']); + $rate->setPrice($data['price_total']); + $result->append($rate); + } + } else if (!empty($this->_errors)) { + return $this->_showError(); + } + return $result; + } + + /** + * Add rate to DHL rates array + * + * @param SimpleXMLElement $shipmentDetails + * @return Mage_Usa_Model_Shipping_Carrier_Dhl_International + */ + protected function _addRate(SimpleXMLElement $shipmentDetails) + { + if (isset($shipmentDetails->ProductShortName) + && isset($shipmentDetails->ShippingCharge) + && isset($shipmentDetails->GlobalProductCode) + && isset($shipmentDetails->CurrencyCode) + && array_key_exists((string)$shipmentDetails->GlobalProductCode, $this->getAllowedMethods()) + ) { + // DHL product code, e.g. '3', 'A', 'Q', etc. + $dhlProduct = (string)$shipmentDetails->GlobalProductCode; + $totalEstimate = (float)(string)$shipmentDetails->ShippingCharge; + $currencyCode = (string)$shipmentDetails->CurrencyCode; + $baseCurrencyCode = Mage::app()->getWebsite($this->_request->getWebsiteId())->getBaseCurrencyCode(); + $dhlProductDescription = $this->getDhlProductTitle($dhlProduct); + + if ($currencyCode != $baseCurrencyCode) { + /* @var $currency Mage_Directory_Model_Currency */ + $currency = Mage::getModel('directory/currency'); + $rates = $currency->getCurrencyRates($currencyCode, array($baseCurrencyCode)); + if (!empty($rates) && isset($rates[$baseCurrencyCode])) { + // Convert to store display currency using store exchange rate + $totalEstimate = $totalEstimate * $rates[$baseCurrencyCode]; + } else { + $rates = $currency->getCurrencyRates($baseCurrencyCode, array($currencyCode)); + if (!empty($rates) && isset($rates[$currencyCode])) { + $totalEstimate = $totalEstimate/$rates[$currencyCode]; + } + if (!isset($rates[$currencyCode]) || !$totalEstimate) { + $totalEstimate = false; + $this->_errors[] = Mage::helper('usa')->__("Exchange rate %s (Base Currency) -> %s not found. DHL method %s skipped", $currencyCode, $baseCurrencyCode, $dhlProductDescription); + } + } + } + if ($totalEstimate) { + $data = array('term' => $dhlProductDescription, + 'price_total' => $this->getMethodPrice($totalEstimate, $dhlProduct)); + if (!empty($this->_rates)) { + foreach ($this->_rates as $product) { + if ($product['data']['term'] == $data['term'] + && $product['data']['price_total'] == $data['price_total'] + ) { + return $this; + } + } + } + $this->_rates[] = array('service' => $dhlProduct, 'data' => $data); + } else { + $this->_errors[] = Mage::helper('usa')->__("Zero shipping charge for '%s'", $dhlProductDescription); + } + } else { + $dhlProductDescription = false; + if (isset($shipmentDetails->GlobalProductCode)) { + $dhlProductDescription = $this->getDhlProductTitle((string)$shipmentDetails->GlobalProductCode); + } + $dhlProductDescription = $dhlProductDescription ? $dhlProductDescription : Mage::helper('usa')->__("DHL"); + $this->_errors[] = Mage::helper('usa')->__("Zero shipping charge for '%s'", $dhlProductDescription); + } + return $this; + } + + /** + * Returns dimension unit (cm or inch) + * + * @return string + */ + protected function _getDimensionUnit() + { + $countryId = $this->_rawRequest->getOrigCountryId(); + $measureUnit = $this->getCountryParams($countryId)->getMeasureUnit(); + if (empty($measureUnit)) { + Mage::throwException(Mage::helper('usa')->__("Cannot identify measure unit for %s", $countryId)); + } + return $measureUnit; + } + + /** + * Returns weight unit (kg or pound) + * + * @return string + */ + protected function _getWeightUnit() + { + $countryId = $this->_rawRequest->getOrigCountryId(); + $weightUnit = $this->getCountryParams($countryId)->getWeightUnit(); + if (empty($weightUnit)) { + Mage::throwException(Mage::helper('usa')->__("Cannot identify weight unit for %s", $countryId)); + } + return $weightUnit; + } + + /** + * Get Country Params by Country Code + * + * @param string $countryCode + * @return Varien_Object + * + * @see $countryCode ISO 3166 Codes (Countries) A2 + */ + protected function getCountryParams($countryCode) + { + if (empty($this->_countryParams)) { + $dhlConfigPath = Mage::getModuleDir('etc', 'Mage_Usa') . DS . 'dhl' . DS; + $countriesXml = file_get_contents($dhlConfigPath . 'international' . DS . 'countries.xml'); + $this->_countryParams = new Varien_Simplexml_Element($countriesXml); + } + if (isset($this->_countryParams->$countryCode)) { + $countryParams = new Varien_Object($this->_countryParams->$countryCode->asArray()); + } + return isset($countryParams) ? $countryParams : new Varien_Object(); + } + + /** + * Do shipment request to carrier web service, obtain Print Shipping Labels and process errors in response + * + * @param Varien_Object $request + * @return Varien_Object + */ + protected function _doShipmentRequest(Varien_Object $request) + { + $this->_prepareShipmentRequest($request); + $this->_mapRequestToShipment($request); + $this->setRequest($request); + + return $this->_doRequest(); + } + + /** + * Processing additional validation to check is carrier applicable. + * + * @param Mage_Shipping_Model_Rate_Request $request + * @return Mage_Shipping_Model_Carrier_Abstract|Mage_Shipping_Model_Rate_Result_Error|boolean + */ + public function proccessAdditionalValidation(Mage_Shipping_Model_Rate_Request $request) + { + //Skip by item validation if there is no items in request + if(!count($this->getAllItems($request))) { + $this->_errors[] = Mage::helper('usa')->__('There is no items in this order'); + } + + if (!empty($this->_errors)) { + return $this->_showError(); + } + + return $this; + } + + /** + * Show default error + * + * @return bool|Mage_Shipping_Model_Rate_Result_Error + */ + protected function _showError() + { + $showMethod = $this->getConfigData('showmethod'); + + if ($showMethod) { + /* @var $error Mage_Shipping_Model_Rate_Result_Error */ + $error = Mage::getModel('shipping/rate_result_error'); + $error->setCarrier(self::CODE); + $error->setCarrierTitle($this->getConfigData('title')); + $error->setErrorMessage($this->getConfigData('specificerrmsg')); + $this->_debug($this->_errors); + return $error; + } else { + return false; + } + } + + /** + * Return container types of carrier + * + * @param Varien_Object|null $params + * @return array + */ + public function getContainerTypes(Varien_Object $params = null) + { + return array( + self::DHL_CONTENT_TYPE_DOC => Mage::helper('usa')->__('Documents'), + self::DHL_CONTENT_TYPE_NON_DOC => Mage::helper('usa')->__('Non Documents') + ); + } + + /** + * Map request to shipment + * + * @param Varien_Object $request + * @return null + */ + protected function _mapRequestToShipment(Varien_Object $request) + { + $customsValue = $request->getPackageParams()->getCustomsValue(); + + $request->getLimitMethod($request->getShippingMethod()); + $request->getPackageValue($customsValue); + $request->getValueWithDiscount($customsValue); + $request->getPackageCustomsValue($customsValue); + $request->getFreeMethodWeight(0); + $request->getDhlShipmentType($request->getPackagingType()); + } + + /** + * Do rate request and handle errors + * + * @return Mage_Shipping_Model_Rate_Result|Varien_Object + */ + protected function _doRequest() + { + $rawRequest = $this->_request; + + $originRegion = (string)$this->getCountryParams( + Mage::getStoreConfig(Mage_Shipping_Model_Shipping::XML_PATH_STORE_COUNTRY_ID, $this->getStore()) + )->region; + + if (!$originRegion) { + Mage::throwException(Mage::helper('usa')->__('Wrong Region.')); + } + + if ($originRegion == 'AM') { + $originRegion = ''; + } + + $xmlStr = '' + . ''; + $xml = new SimpleXMLElement($xmlStr); + + $nodeRequest = $xml->addChild('Request', '', ''); + $nodeServiceHeader = $nodeRequest->addChild('ServiceHeader'); + $nodeServiceHeader->addChild('SiteID', (string)$this->getConfigData('id')); + $nodeServiceHeader->addChild('Password', (string)$this->getConfigData('password')); + + $xml->addChild('LanguageCode', 'EN', ''); + $xml->addChild('PiecesEnabled', 'Y', ''); + + /* Billing */ + $nodeBilling = $xml->addChild('Billing', '', ''); + $nodeBilling->addChild('ShipperAccountNumber', (string)$this->getConfigData('account')); + /* + * Method of Payment: + * S (Shipper) + * R (Receiver) + * T (Third Party) + */ + $nodeBilling->addChild('ShippingPaymentType', 'S'); + + /* + * Shipment bill to account – required if Shipping PaymentType is other than 'S' + */ + //$nodeBilling->addChild('BillingAccountNumber', (string)$this->getConfigData('password')); + //$nodeBilling->addChild('DutyPaymentType', ''); + //$nodeBilling->addChild('DutyAccountNumber', ''); + + /* Receiver */ + $nodeConsignee = $xml->addChild('Consignee', '', ''); + + $companyName = ($rawRequest->getRecipientContactCompanyName()) + ? $rawRequest->getRecipientContactCompanyName() + : $rawRequest->getRecipientContactPersonName(); + + $nodeConsignee->addChild('CompanyName', substr($companyName, 0, 35)); + + $address = $rawRequest->getRecipientAddressStreet1(). ' ' . $rawRequest->getRecipientAddressStreet2(); + $addressLength = strlen($address); + if ($addressLength > 35) { + for ($i = 0; $i < $addressLength; $i += 35) { + $nodeConsignee->addChild('AddressLine', substr($address, $i, 35)); + } + } else { + $nodeConsignee->addChild('AddressLine', $address); + } + + $nodeConsignee->addChild('City', $rawRequest->getRecipientAddressCity()); + $nodeConsignee->addChild('Division', $rawRequest->getRecipientAddressStateOrProvinceCode()); + $nodeConsignee->addChild('PostalCode', $rawRequest->getRecipientAddressPostalCode()); + $nodeConsignee->addChild('CountryCode', $rawRequest->getRecipientAddressCountryCode()); + $nodeConsignee->addChild('CountryName', + (string)$this->getCountryParams($rawRequest->getRecipientAddressCountryCode())->name + ); + $nodeContact = $nodeConsignee->addChild('Contact'); + $nodeContact->addChild('PersonName', substr($rawRequest->getRecipientContactPersonName(), 0, 34)); + $nodeContact->addChild('PhoneNumber', substr($rawRequest->getRecipientContactPhoneNumber(), 0, 24)); + + /* Commodity */ + /* + * The CommodityCode element contains commodity code for shipment contents. Its + * value should lie in between 1 to 9999.This field is mandatory. + */ + $nodeCommodity = $xml->addChild('Commodity', '', ''); + $nodeCommodity->addChild('CommodityCode', '1'); + + /* Dutiable */ + if ($this->getConfigData('content_type') == self::DHL_CONTENT_TYPE_NON_DOC) { + $nodeDutiable = $xml->addChild('Dutiable', '', ''); + $nodeDutiable->addChild('DeclaredValue', + round($rawRequest->getOrderShipment()->getOrder()->getSubtotal(), 2) + ); + $baseCurrencyCode = Mage::app()->getWebsite($rawRequest->getWebsiteId())->getBaseCurrencyCode(); + $nodeDutiable->addChild('DeclaredCurrency', $baseCurrencyCode); + } + + /* Reference */ + /* + * This element identifies the reference information. It is an optional field in the + * shipment validation request. Only the first reference will be taken currently. + */ + $nodeReference = $xml->addChild('Reference', '', ''); + $nodeReference->addChild('ReferenceID', 'shipment reference'); + $nodeReference->addChild('ReferenceType', 'St'); + + /* Shipment Details */ + $nodeShipmentDetails = $xml->addChild('ShipmentDetails', '', ''); + $nodeShipmentDetails->addChild('NumberOfPieces', count($rawRequest->getPackages())); + $nodeShipmentDetails->addChild('CurrencyCode', + Mage::app()->getWebsite($this->_request->getWebsiteId())->getBaseCurrencyCode() + ); + $nodePieces = $nodeShipmentDetails->addChild('Pieces', '', ''); + + /* + * Package type + * EE (DHL Express Envelope), OD (Other DHL Packaging), CP (Custom Packaging) + * DC (Document), DM (Domestic), ED (Express Document), FR (Freight) + * BD (Jumbo Document), BP (Jumbo Parcel), JD (Jumbo Junior Document) + * JP (Jumbo Junior Parcel), PA (Parcel), DF (DHL Flyer) + */ + foreach ($rawRequest->getPackages() as $package) { + $nodePiece = $nodePieces->addChild('Piece', '', ''); + $packageType = 'DC'; + if ($package['params']['container'] == self::DHL_CONTENT_TYPE_NON_DOC) { + $packageType = 'CP'; + } + //$niodePiece->addChild('PieceID', ''); + $nodePiece->addChild('PackageType', $packageType); + $nodePiece->addChild('Weight', $package['params']['weight']); + $nodePiece->addChild('Depth', $package['params']['length']); + $nodePiece->addChild('Width', $package['params']['width']); + $nodePiece->addChild('Height', $package['params']['height']); + } + + if ($package['params']['container'] == self::DHL_CONTENT_TYPE_NON_DOC) { + $packageType = 'CP'; + } + $nodeShipmentDetails->addChild('PackageType', $packageType); + $nodeShipmentDetails->addChild('Weight', $rawRequest->getPackageWeight()); + + $dimensionUnit = $rawRequest->getPackageParams()->getDimensionUnits(); + $weightUnits = $rawRequest->getPackageParams()->getWeightUnits(); + + $nodeShipmentDetails->addChild('DimensionUnit', $dimensionUnit[0]); + $nodeShipmentDetails->addChild('WeightUnit', $weightUnits[0]); + + $nodeShipmentDetails->addChild('GlobalProductCode', $rawRequest->getShippingMethod()); + $nodeShipmentDetails->addChild('LocalProductCode', $rawRequest->getShippingMethod()); + + /* + * The DoorTo Element defines the type of delivery service that applies to the shipment. + * The valid values are DD (Door to Door), DA (Door to Airport) , AA and DC (Door to + * Door non-compliant) + */ + $nodeShipmentDetails->addChild('DoorTo', 'DD'); + $nodeShipmentDetails->addChild('Date', Mage::getModel('core/date')->date('Y-m-d')); + $nodeShipmentDetails->addChild('Contents', 'DHL Parcel TEST'); + if ($this->getConfigData('content_type') == self::DHL_CONTENT_TYPE_NON_DOC) { + $nodeShipmentDetails->addChild('IsDutiable', 'Y'); + } + + /* Shipper */ + $nodeShipper = $xml->addChild('Shipper', '', ''); + $nodeShipper->addChild('ShipperID', (string)$this->getConfigData('account')); + $nodeShipper->addChild('CompanyName', $rawRequest->getShipperContactCompanyName()); + //$nodeShipper->addChild('RegisteredAccount', ''); + + $address = $rawRequest->getShipperAddressStreet1(). ' ' . $rawRequest->getShipperAddressStreet2(); + $addressLength = strlen($address); + if ($addressLength > 35) { + for ($i = 0; $i < $addressLength; $i += 35) { + $nodeShipper->addChild('AddressLine', substr($address, $i, 35)); + } + } else { + $nodeShipper->addChild('AddressLine', $address); + } + + $nodeShipper->addChild('City', $rawRequest->getShipperAddressCity()); + $nodeShipper->addChild('Division', $rawRequest->getShipperAddressStateOrProvinceCode()); + $nodeShipper->addChild('PostalCode', $rawRequest->getShipperAddressPostalCode()); + $nodeShipper->addChild('CountryCode', $rawRequest->getShipperAddressCountryCode()); + $nodeShipper->addChild('CountryName', + (string)$this->getCountryParams($rawRequest->getShipperAddressCountryCode())->name + ); + $nodeContact = $nodeShipper->addChild('Contact', '', ''); + $nodeContact->addChild('PersonName', substr($rawRequest->getShipperContactPersonName(), 0, 34)); + $nodeContact->addChild('PhoneNumber', substr($rawRequest->getShipperContactPhoneNumber(), 0, 24)); + + $request = $xml->asXML(); + $request = utf8_encode($request); + $responseBody = $this->_getCachedQuotes($request); + if ($responseBody === null) { + $debugData = array('request' => $request); + try { + $client = new Varien_Http_Client(); + $client->setUri((string)$this->getConfigData('gateway_url')); + $client->setConfig(array('maxredirects' => 0, 'timeout' => 30)); + $client->setRawData($request); + $responseBody = $client->request(Varien_Http_Client::POST)->getBody(); + $debugData['result'] = $responseBody; + $this->_setCachedQuotes($request, $responseBody); + } catch (Exception $e) { + $this->_errors[$e->getCode()] = $e->getMessage(); + $responseBody = ''; + } + $this->_debug($debugData); + } + + return $this->_parseResponse($responseBody); + } + + /** + * Get tracking + * + * @param mixed $trackings + * @return mixed + */ + public function getTracking($trackings) + { + if (!is_array($trackings)) { + $trackings = array($trackings); + } + $this->_getXMLTracking($trackings); + + return $this->_result; + } + + /** + * Send request for tracking + * + * @param array $trackings + * @return void + */ + protected function _getXMLTracking($trackings) + { + $xmlStr = '' + . ''; + + $xml = new SimpleXMLElement($xmlStr); + + $requestNode = $xml->addChild('Request', '', ''); + $serviceHeaderNode = $requestNode->addChild('ServiceHeader', '', ''); + $serviceHeaderNode->addChild('SiteID', (string)$this->getConfigData('id')); + $serviceHeaderNode->addChild('Password', (string)$this->getConfigData('password')); + + $xml->addChild('LanguageCode', 'EN', ''); + foreach ($trackings as $tracking) { + $xml->addChild('AWBNumber', $tracking, ''); + } + /* + * Checkpoint details selection flag + * LAST_CHECK_POINT_ONLY + * ALL_CHECK_POINTS + */ + $xml->addChild('LevelOfDetails', 'ALL_CHECK_POINTS', ''); + + /* + * Value that indicates for getting the tracking details with the additional + * piece details and its respective Piece Details, Piece checkpoints along with + * Shipment Details if queried. + * + * S-Only Shipment Details + * B-Both Shipment & Piece Details + * P-Only Piece Details + * Default is ‘S’ + */ + //$xml->addChild('PiecesEnabled', 'ALL_CHECK_POINTS'); + + $request = $xml->asXML(); + $request = utf8_encode($request); + + $responseBody = $this->_getCachedQuotes($request); + if ($responseBody === null) { + $debugData = array('request' => $request); + try { + $client = new Varien_Http_Client(); + $client->setUri((string)$this->getConfigData('gateway_url')); + $client->setConfig(array('maxredirects' => 0, 'timeout' => 30)); + $client->setRawData($request); + $responseBody = $client->request(Varien_Http_Client::POST)->getBody(); + $debugData['result'] = $responseBody; + $this->_setCachedQuotes($request, $responseBody); + } catch (Exception $e) { + $this->_errors[$e->getCode()] = $e->getMessage(); + $responseBody = ''; + } + $this->_debug($debugData); + } + + $this->_parseXmlTrackingResponse($trackings, $responseBody); + } + + /** + * Parse xml tracking response + * + * @param array $trackings + * @param string $response + * @return void + */ + protected function _parseXmlTrackingResponse($trackings, $response) + { + $errorTitle = Mage::helper('usa')->__('Unable to retrieve tracking'); + $resultArr = array(); + + $htmlTranslationTable = get_html_translation_table(HTML_ENTITIES); + unset($htmlTranslationTable['<'], $htmlTranslationTable['>'], $htmlTranslationTable['"']); + $response = str_replace(array_keys($htmlTranslationTable), array_values($htmlTranslationTable), $response); + + if (strlen(trim($response)) > 0) { + $xml = simplexml_load_string($response); + if (!is_object($xml)) { + $errorTitle = Mage::helper('usa')->__('Response is in the wrong format'); + } + if (is_object($xml) && ((isset($xml->Response->Status->ActionStatus) + && $xml->Response->Status->ActionStatus == 'Failure') + || isset($xml->GetQuoteResponse->Note->Condition)) + ) { + if (isset($xml->Response->Status->Condition)) { + $nodeCondition = $xml->Response->Status->Condition; + } + + $code = isset($nodeCondition->ConditionCode) ? (string)$nodeCondition->ConditionCode : 0; + $data = isset($nodeCondition->ConditionData) ? (string)$nodeCondition->ConditionData : ''; + $this->_errors[$code] = Mage::helper('usa')->__('Error #%s : %s', $code, $data); + } elseif (is_object($xml) && is_object($xml->AWBInfo)) { + foreach ($xml->AWBInfo as $awbinfo) { + $awbinfoData = array(); + $trackNum = isset($awbinfo->AWBNumber) ? (string)$awbinfo->AWBNumber : ''; + if (!is_object($awbinfo) || !$awbinfo->ShipmentInfo) { + $this->_errors[$trackNum] = Mage::helper('usa')->__('Unable to retrieve tracking'); + continue; + } + $shipmentInfo = $awbinfo->ShipmentInfo; + + if ($shipmentInfo->ShipmentDesc) { + $awbinfoData['service'] = (string)$shipmentInfo->ShipmentDesc; + } + + $awbinfoData['weight'] = (string)$shipmentInfo->Weight . ' ' . (string)$shipmentInfo->WeightUnit; + + $packageProgress = array(); + if (isset($shipmentInfo->ShipmentEvent)) { + foreach ($shipmentInfo->ShipmentEvent as $shipmentEvent) { + $shipmentEventArray = array(); + $shipmentEventArray['activity'] = (string)$shipmentEvent->ServiceEvent->EventCode + . ' ' . (string)$shipmentEvent->ServiceEvent->Description; + $shipmentEventArray['deliverydate'] = (string)$shipmentEvent->Date; + $shipmentEventArray['deliverytime'] = (string)$shipmentEvent->Time; + $shipmentEventArray['deliverylocation'] = (string)$shipmentEvent->ServiceArea->Description + . ' [' . (string)$shipmentEvent->ServiceArea->ServiceAreaCode . ']'; + $packageProgress[] = $shipmentEventArray; + } + $awbinfoData['progressdetail'] = $packageProgress; + } + $resultArr[$trackNum] = $awbinfoData; + } + } + } + + $result = Mage::getModel('shipping/tracking_result'); + + if (!empty($resultArr)) { + foreach ($resultArr as $trackNum => $data) { + $tracking = Mage::getModel('shipping/tracking_result_status'); + $tracking->setCarrier($this->_code); + $tracking->setCarrierTitle($this->getConfigData('title')); + $tracking->setTracking($trackNum); + $tracking->addData($data); + $result->append($tracking); + } + } + + if (!empty($this->_errors) || empty($resultArr)) { + $resultArr = !empty($this->_errors) ? $this->_errors : $trackings; + foreach ($resultArr as $trackNum => $err) { + $error = Mage::getModel('shipping/tracking_result_error'); + $error->setCarrier($this->_code); + $error->setCarrierTitle($this->getConfigData('title')); + $error->setTracking(!empty($this->_errors) ? $trackNum : $err); + $error->setErrorMessage(!empty($this->_errors) ? $err : $errorTitle); + $result->append($error); + } + } + + $this->_result = $result; + } + + /** + * Get final price for shipping method with handling fee per package + * + * @param float $cost + * @param string $handlingType + * @param float $handlingFee + * @return float + */ + protected function _getPerpackagePrice($cost, $handlingType, $handlingFee) + { + if ($handlingType == Mage_Shipping_Model_Carrier_Abstract::HANDLING_TYPE_PERCENT) { + return $cost + ($cost * $this->_numBoxes * $handlingFee / 100); + } + + return $cost + $this->_numBoxes * $handlingFee; + } +} diff --git a/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Contenttype.php b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Contenttype.php new file mode 100644 index 0000000000..755a1c0f64 --- /dev/null +++ b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Contenttype.php @@ -0,0 +1,50 @@ + + */ +class Mage_Usa_Model_Shipping_Carrier_Dhl_International_Source_Contenttype +{ + /** + * Returns array to be used in multiselect on back-end + * + * @return array + */ + public function toOptionArray() + { + return array( + array('label' => Mage::helper('usa')->__('Documents'), + 'value' => Mage_Usa_Model_Shipping_Carrier_Dhl_International::DHL_CONTENT_TYPE_DOC), + array('label' => Mage::helper('usa')->__('Non documents'), + 'value' => Mage_Usa_Model_Shipping_Carrier_Dhl_International::DHL_CONTENT_TYPE_NON_DOC), + ); + } +} diff --git a/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Freemethod.php b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Freemethod.php new file mode 100644 index 0000000000..5e3b4b843d --- /dev/null +++ b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Freemethod.php @@ -0,0 +1,41 @@ + '', 'label' => Mage::helper('shipping')->__('None'))); + return $arr; + } +} diff --git a/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method.php b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method.php new file mode 100644 index 0000000000..8eedc1c94b --- /dev/null +++ b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method.php @@ -0,0 +1,43 @@ +getDhlProducts() as $code => $title) { + $arr[] = array('value' => $code, 'label' => $title); + } + return $arr; + } +} diff --git a/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Abstract.php b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Abstract.php new file mode 100644 index 0000000000..3b2939e7b3 --- /dev/null +++ b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Abstract.php @@ -0,0 +1,72 @@ + + */ +abstract class Mage_Usa_Model_Shipping_Carrier_Dhl_International_Source_Method_Abstract +{ + /** + * Carrier Product Type Indicator + * + * @var string $_contentType + */ + protected $_contentType; + + /** + * Show 'none' in methods list or not; + * + * @var bool + */ + protected $_noneMethod = false; + + /** + * Returns array to be used in multiselect on back-end + * + * @return array + */ + public function toOptionArray() + { + /* @var $carrierModel Mage_Usa_Model_Shipping_Carrier_Dhl_International */ + $carrierModel = Mage::getSingleton('usa/shipping_carrier_dhl_international'); + $dhlProducts = $carrierModel->getDhlProducts($this->_contentType); + + $options = array(); + foreach ($dhlProducts as $code => $title) { + $options[] = array('value' => $code, 'label' => $title); + } + + if ($this->_noneMethod) { + array_unshift($options, array('value' => '', 'label' => Mage::helper('usa')->__('None'))); + } + + return $options; + } +} diff --git a/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/All.php b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/All.php new file mode 100644 index 0000000000..3d6b0bdf12 --- /dev/null +++ b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/All.php @@ -0,0 +1,46 @@ + + */ +class Mage_Usa_Model_Shipping_Carrier_Dhl_International_Source_Method_All +{ + public function toOptionArray() + { + /* @var $dhl Mage_Usa_Model_Shipping_Carrier_Dhl_International */ + $dhl = Mage::getModel('usa/shipping_carrier_dhl_international'); + $options = array(); + foreach ($dhl->getDhlProducts(Mage_Usa_Model_Shipping_Carrier_Dhl_International::DOC_EVERYTHING) as $k => $v) { + $options[] = array('value' => $k, 'label' => $v); + } + return $options; + } +} diff --git a/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Doc.php b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Doc.php new file mode 100644 index 0000000000..e27c6b0842 --- /dev/null +++ b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Doc.php @@ -0,0 +1,43 @@ + + */ +class Mage_Usa_Model_Shipping_Carrier_Dhl_International_Source_Method_Doc + extends Mage_Usa_Model_Shipping_Carrier_Dhl_International_Source_Method_Abstract +{ + /** + * Carrier Product Type Indicator + * + * @var string $_contentType + */ + protected $_contentType = Mage_Usa_Model_Shipping_Carrier_Dhl_International::DHL_CONTENT_TYPE_DOC; +} diff --git a/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Freedoc.php b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Freedoc.php new file mode 100644 index 0000000000..8e190f334f --- /dev/null +++ b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Freedoc.php @@ -0,0 +1,50 @@ + + */ +class Mage_Usa_Model_Shipping_Carrier_Dhl_International_Source_Method_Freedoc + extends Mage_Usa_Model_Shipping_Carrier_Dhl_International_Source_Method_Abstract +{ + /** + * Carrier Product Type Indicator + * + * @var string $_contentType + */ + protected $_contentType = Mage_Usa_Model_Shipping_Carrier_Dhl_International::DHL_CONTENT_TYPE_DOC; + + /** + * Show 'none' in methods list or not; + * + * @var bool + */ + protected $_noneMethod = true; +} diff --git a/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Freenondoc.php b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Freenondoc.php new file mode 100644 index 0000000000..d01a2ef9a8 --- /dev/null +++ b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Freenondoc.php @@ -0,0 +1,50 @@ + + */ +class Mage_Usa_Model_Shipping_Carrier_Dhl_International_Source_Method_Freenondoc + extends Mage_Usa_Model_Shipping_Carrier_Dhl_International_Source_Method_Abstract +{ + /** + * Carrier Product Type Indicator + * + * @var string $_contentType + */ + protected $_contentType = Mage_Usa_Model_Shipping_Carrier_Dhl_International::DHL_CONTENT_TYPE_NON_DOC; + + /** + * Show 'none' in methods list or not; + * + * @var bool + */ + protected $_noneMethod = true; +} diff --git a/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Nondoc.php b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Nondoc.php new file mode 100644 index 0000000000..784098a290 --- /dev/null +++ b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Nondoc.php @@ -0,0 +1,43 @@ + + */ +class Mage_Usa_Model_Shipping_Carrier_Dhl_International_Source_Method_Nondoc + extends Mage_Usa_Model_Shipping_Carrier_Dhl_International_Source_Method_Abstract +{ + /** + * Carrier Product Type Indicator + * + * @var string $_contentType + */ + protected $_contentType = Mage_Usa_Model_Shipping_Carrier_Dhl_International::DHL_CONTENT_TYPE_NON_DOC; +} diff --git a/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Size.php b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Size.php new file mode 100644 index 0000000000..a2f08177e1 --- /dev/null +++ b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Size.php @@ -0,0 +1,51 @@ + + */ +class Mage_Usa_Model_Shipping_Carrier_Dhl_International_Source_Method_Size +{ + /** + * Returns array to be used in multiselect on back-end + * + * @return array + */ + public function toOptionArray() + { + $unitArr = Mage::getSingleton('usa/shipping_carrier_dhl_international')->getCode('size'); + + $returnArr = array(); + foreach ($unitArr as $key => $val) { + $returnArr[] = array('value' => $key, 'label' => $val); + } + return $returnArr; + } +} diff --git a/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Unitofmeasure.php b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Unitofmeasure.php new file mode 100644 index 0000000000..218feb7f64 --- /dev/null +++ b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Dhl/International/Source/Method/Unitofmeasure.php @@ -0,0 +1,51 @@ + + */ +class Mage_Usa_Model_Shipping_Carrier_Dhl_International_Source_Method_Unitofmeasure +{ + /** + * Returns array to be used in multiselect on back-end + * + * @return array + */ + public function toOptionArray() + { + $unitArr = Mage::getSingleton('usa/shipping_carrier_dhl_international')->getCode('unit_of_measure'); + + $returnArr = array(); + foreach ($unitArr as $key => $val) { + $returnArr[] = array('value' => $key, 'label' => $val); + } + return $returnArr; + } +} diff --git a/app/code/core/Mage/Usa/Model/Shipping/Carrier/Fedex.php b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Fedex.php index d567b6e3c3..91833f666a 100644 --- a/app/code/core/Mage/Usa/Model/Shipping/Carrier/Fedex.php +++ b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Fedex.php @@ -261,6 +261,8 @@ public function setRequest(Mage_Shipping_Model_Rate_Request $request) $r->setIsReturn($request->getIsReturn()); + $r->setBaseSubtotalInclTax($request->getBaseSubtotalInclTax()); + $this->_rawRequest = $r; return $this; @@ -395,8 +397,7 @@ protected function _prepareRateResponse($response) foreach ($response->RateReplyDetails as $rate) { $serviceName = (string)$rate->ServiceType; if (in_array($serviceName, $allowedMethods)) { - $amount = (string)$rate->RatedShipmentDetails[0] - ->ShipmentRateDetail->TotalNetCharge->Amount; + $amount = $this->_getRateAmountOriginBased($rate); $costArr[$serviceName] = $amount; $priceArr[$serviceName] = $this->getMethodPrice($amount, $serviceName); } @@ -406,7 +407,7 @@ protected function _prepareRateResponse($response) $rate = $response->RateReplyDetails; $serviceName = (string)$rate->ServiceType; if (in_array($serviceName, $allowedMethods)) { - $amount = (string)$rate->RatedShipmentDetails[0]->ShipmentRateDetail->TotalNetCharge->Amount; + $amount = $this->_getRateAmountOriginBased($rate); $costArr[$serviceName] = $amount; $priceArr[$serviceName] = $this->getMethodPrice($amount, $serviceName); } @@ -438,6 +439,31 @@ protected function _prepareRateResponse($response) return $result; } + /** + * Get origin based amount form response of rate estimation + * + * @param stdClass $rate + * @return null|float + */ + protected function _getRateAmountOriginBased($rate) + { + $amount = null; + if (is_object($rate)) { + foreach($rate->RatedShipmentDetails as $ratedShipmentDetail) { + $shipmentRateDetail = $ratedShipmentDetail->ShipmentRateDetail; + // The "RATED..." rates are expressed in the currency of the origin country + if ((string)$shipmentRateDetail->RateType == 'RATED_ACCOUNT_SHIPMENT') { + $amount = (string)$shipmentRateDetail->TotalNetCharge->Amount; + } + } + if (is_null($amount)) { + $amount = (string)$rate->RatedShipmentDetails[0]->ShipmentRateDetail + ->TotalNetCharge->Amount; + } + } + return $amount; + } + /** * Set free method request * @@ -698,6 +724,11 @@ public function getCode($type, $code='') 'STANDARD_OVERNIGHT', 'PRIORITY_OVERNIGHT', 'FIRST_OVERNIGHT', + 'FEDEX_FREIGHT', + 'FEDEX_1_DAY_FREIGHT', + 'FEDEX_2_DAY_FREIGHT', + 'FEDEX_3_DAY_FREIGHT', + 'FEDEX_NATIONAL_FREIGHT', ) ), 'from_us' => array( diff --git a/app/code/core/Mage/Usa/Model/Shipping/Carrier/Ups.php b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Ups.php index 3819a2e6c8..2ce88521d1 100644 --- a/app/code/core/Mage/Usa/Model/Shipping/Carrier/Ups.php +++ b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Ups.php @@ -272,6 +272,8 @@ public function setRequest(Mage_Shipping_Model_Rate_Request $request) $r->setIsReturn($request->getIsReturn()); + $r->setBaseSubtotalInclTax($request->getBaseSubtotalInclTax()); + $this->_rawRequest = $r; return $this; @@ -703,6 +705,7 @@ public function getCode($type, $code='') '14', // Next Day Air Early AM '02', // 2nd Day Air '59', // 2nd Day Air AM + '13', // Next Day Air Saver ) ), 'from_us' => array( @@ -1006,10 +1009,7 @@ protected function _parseXmlResponse($xmlResponse) if (in_array($responseCurrencyCode, $allowedCurrencies)) { $cost = (float) $cost * $this->_getBaseCurrencyRate($responseCurrencyCode); } else { - $errorTitle = Mage::helper('directory') - ->__('Can\'t convert rate from "%s-%s".', - $responseCurrencyCode, - $this->_request->getPackageCurrency()->getCode()); + $errorTitle = Mage::helper('directory')->__('Can\'t convert rate from "%s-%s".', $responseCurrencyCode, $this->_request->getPackageCurrency()->getCode()); $error = Mage::getModel('shipping/rate_result_error'); $error->setCarrier('ups'); $error->setCarrierTitle($this->getConfigData('title')); @@ -1806,4 +1806,38 @@ protected function _getDeliveryConfirmationLevel($countyDest = null) { return self::DELIVERY_CONFIRMATION_SHIPMENT; } + + /** + * Return items for further shipment rate evaluation. We need to pass children of a bundle instead passing the + * bundle itself, otherwise we may not get a rate at all (e.g. when total weight of a bundle exceeds max weight + * despite each item by itself is not) + * + * @param Mage_Shipping_Model_Rate_Request $request + * @return array + */ + public function getAllItems(Mage_Shipping_Model_Rate_Request $request) + { + $items = array(); + if ($request->getAllItems()) { + foreach ($request->getAllItems() as $item) { + /* @var $item Mage_Sales_Model_Quote_Item */ + if ($item->getProduct()->isVirtual() || $item->getParentItem()) { + // Don't process children here - we will process (or already have processed) them below + continue; + } + + if ($item->getHasChildren() && $item->isShipSeparately()) { + foreach ($item->getChildren() as $child) { + if (!$child->getFreeShipping() && !$child->getProduct()->isVirtual()) { + $items[] = $child; + } + } + } else { + // Ship together - count compound item as one solid + $items[] = $item; + } + } + } + return $items; + } } diff --git a/app/code/core/Mage/Usa/Model/Shipping/Carrier/Usps.php b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Usps.php index 810fbfece2..3685d97bc3 100644 --- a/app/code/core/Mage/Usa/Model/Shipping/Carrier/Usps.php +++ b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Usps.php @@ -287,6 +287,8 @@ public function setRequest(Mage_Shipping_Model_Rate_Request $request) $r->setValue($request->getPackageValue()); $r->setValueWithDiscount($request->getPackageValueWithDiscount()); + $r->setBaseSubtotalInclTax($request->getBaseSubtotalInclTax()); + $this->_rawRequest = $r; return $this; diff --git a/app/code/core/Mage/Usa/etc/config.xml b/app/code/core/Mage/Usa/etc/config.xml index aeed1122db..2f14f1e17b 100644 --- a/app/code/core/Mage/Usa/etc/config.xml +++ b/app/code/core/Mage/Usa/etc/config.xml @@ -59,6 +59,9 @@ Mage_Usa_Model_Shipping_Carrier_Dhl + + Mage_Usa_Model_Shipping_Carrier_Dhl_International + @@ -110,7 +113,7 @@ P - DHL + DHL (Deprecated) This shipping method is currently unavailable. If you would like to ship using this shipping method, please contact us. 150 F @@ -191,6 +194,25 @@ F O + + usa/shipping_carrier_dhl_international + + 0 + DHL + 0 + 1,2,3,4,5,6,7,8,9,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y + G + https://xmlpitest-ea.dhl.com/XMLShippingServlet + + + N + This shipping method is currently unavailable. If you would like to ship using this shipping method, please contact us. + 1 + K + R + F + O + diff --git a/app/code/core/Mage/Usa/etc/dhl/international/countries.xml b/app/code/core/Mage/Usa/etc/dhl/international/countries.xml new file mode 100644 index 0000000000..24deccbff6 --- /dev/null +++ b/app/code/core/Mage/Usa/etc/dhl/international/countries.xml @@ -0,0 +1,1553 @@ + + + + + EUR + KG + CM + Andorra + + + AED + KG + CM + AP + United Arab Emirates + + + AFN + KG + CM + AP + Afghanistan + + + XCD + KG + CM + AM + Antigua + + + XCD + KG + CM + AM + Anguilla + + + EUR + KG + CM + AP + Albania + + + AMD + KG + CM + AP + Armenia + + + ANG + KG + CM + Netherlands Antilles + + + AOA + KG + CM + Angola + + + ARS + KG + CM + AM + Argentina + + + USD + LB + IN + American Samoa + + + EUR + KG + CM + EA + Austria + + + AUD + KG + CM + AP + Australia + + + AWG + KG + CM + AM + Aruba + + + AZM + KG + CM + Azerbaijan + + + BAM + KG + CM + AP + Bosnia and Herzegovina + + + BBD + KG + CM + AM + Barbados + + + BDT + KG + CM + AP + Bangladesh + + + EUR + KG + CM + EA + Belgium + + + XOF + KG + CM + Burkina Faso + + + BGN + KG + CM + EA + Bulgaria + + + BHD + KG + CM + AP + Bahrain + + + BIF + KG + CM + Burundi + + + XOF + KG + CM + Benin + + + BMD + KG + CM + AM + Bermuda + + + BND + KG + CM + Brunei + + + BOB + KG + CM + AM + Bolivia + +
+ BRL + KG + CM + AM + Brazil +
+ + BSD + KG + CM + AM + Bahamas + + + BTN + KG + CM + Bhutan + + + BWP + KG + CM + Botswana + + + BYR + KG + CM + AP + Belarus + + + BZD + KG + CM + Belize + + + CAD + KG + CM + AM + Canada + + + CDF + KG + CM + Congo, The Democratic Republic of + + + XAF + KG + CM + Central African Republic + + + XAF + KG + CM + Congo + + + CHF + KG + CM + EA + Switzerland + + + XOF + KG + CM + AP + Cote d'Ivoire + + + NZD + KG + CM + Cook Islands + + + CLP + KG + CM + AM + Chile + + + XAF + KG + CM + Cameroon + + + CNY + KG + CM + AP + China, People's Republic + + + COP + KG + CM + AM + Colombia + + + CRC + KG + CM + AM + Costa Rica + + + CUP + KG + CM + Cuba + + + CVE + KG + CM + Cape Verde + + + EUR + KG + CM + AP + Cyprus + + + CZK + KG + CM + EA + Czech Republic, The + + + EUR + KG + CM + EA + Germany + + + DJF + KG + CM + Djibouti + + + DKK + KG + CM + EA + Denmark + + + XCD + KG + CM + AM + Dominica + + + DOP + KG + CM + AM + Dominican Rep. + + + DZD + KG + CM + AP + Algeria + + + USD + KG + CM + AM + Ecuador + + + EEK + KG + CM + EA + Estonia + + + EGP + KG + CM + AP + Egypt + + + ERN + KG + CM + Eritrea + + + EUR + KG + CM + EA + Spain + + + ETB + KG + CM + Ethiopia + + + EUR + KG + CM + EA + Finland + + + FJD + KG + CM + AP + Fiji + + + FKP + KG + CM + Falkland Islands + + + USD + LB + IN + MICRONESIA, FEDERATED STATES OF + + + DKK + KG + CM + Faroe Islands + + + EUR + KG + CM + EA + France + + + XAF + KG + CM + Gabon + + + GBP + KG + CM + EA + United Kingdom + + + XCD + KG + CM + AM + Grenada + + + GEL + KG + CM + Georgia + + + EUR + KG + CM + AM + French Guyana + + + GBP + KG + CM + Guernsey + + + GHS + KG + CM + AP + Ghana + + + GIP + KG + CM + Gibraltar + + + DKK + KG + CM + Greenland + + + GMD + KG + CM + Gambia + + + GNF + KG + CM + Guinea Republic + + + EUR + KG + CM + AM + Guadeloupe + + + XAF + KG + CM + Guinea-Equatorial + + + EUR + KG + CM + EA + Greece + + + GTQ + KG + CM + AM + Guatemala + + + USD + LB + IN + AM + Guam + + + GWP + KG + CM + Guinea-Bissau + + + GYD + KG + CM + AM + Guyana (British) + + + HKD + KG + CM + AP + Hong Kong + + + HNL + KG + CM + AM + Honduras + +
+ HRK + KG + CM + AP + Croatia + + + HTG + KG + CM + AM + Haiti + + + HUF + KG + CM + EA + Hungary + + + EUR + KG + CM + Canary Islands, The + + + IDR + KG + CM + AP + Indonesia + + + EUR + KG + CM + EA + Ireland, Republic Of + + + ILS + KG + CM + AP + Israel + + + INR + KG + CM + AP + India + + + IQD + KG + CM + Iraq + + + IRR + KG + CM + AP + Iran (Islamic Republic of) + + + ISK + KG + CM + EA + Iceland + + + EUR + KG + CM + EA + Italy + + + GBP + KG + CM + Jersey + + + JMD + KG + CM + AM + Jamaica + + + JOD + KG + CM + AP + Jordan + + + JPY + KG + CM + AP + Japan + + + KES + KG + CM + AP + Kenya + + + KGS + KG + CM + AP + Kyrgyzstan + + + KHR + KG + CM + Cambodia + + + AUD + KG + CM + Kiribati + + + KMF + KG + CM + Comoros + + + XCD + KG + CM + AM + St. Kitts + + + KPW + KG + CM + Korea, The D.P.R of + + + KRW + KG + CM + AP + Korea, Republic Of + + + EUR + KG + CM + Kosovo + + + KWD + KG + CM + AP + Kuwait + + + KYD + KG + CM + AM + Cayman Islands + + + KZT + KG + CM + AP + Kazakhstan + + + LAK + KG + CM + Lao People's Democratic Republic + + + USD + KG + CM + AP + Lebanon + + + XCD + KG + CM + AM + St. Lucia + +
  • + CHF + KG + CM + Liechtenstein +
  • + + LKR + KG + CM + AP + Sri Lanka + + + LRD + KG + CM + Liberia + + + LSL + KG + CM + Lesotho + + + LTL + KG + CM + EA + Lithuania + + + EUR + KG + CM + EA + Luxembourg + + + LVL + KG + CM + EA + Latvia + + + LYD + KG + CM + Libya + + + MAD + KG + CM + AP + Morocco + + + EUR + KG + CM + Monaco + + + MDL + KG + CM + AP + Moldova, Republic Of + + + EUR + KG + CM + Montenegro, Republic of + + + MGA + KG + CM + Madagascar + + + USD + LB + IN + Marshall Islands + + + MKD + KG + CM + AP + Macedonia, Rep. of (FYROM) + + + XOF + KG + CM + Mali + + + USD + KG + CM + AP + Myanmar + + + MNT + KG + CM + Mongolia + + + MOP + KG + CM + Macau + + + USD + LB + IN + Saipan + + + EUR + KG + CM + AM + Martinique + + + MRO + KG + CM + Mauritania + + + XCD + KG + CM + Montserrat + + + EUR + KG + CM + AP + Malta + + + MUR + KG + CM + AP + Mauritius + + + MVR + KG + CM + Maldives + + + MWK + KG + CM + Malawi + + + MXN + KG + CM + AM + Mexico + + + MYR + KG + CM + AP + Malaysia + + + MZN + KG + CM + Mozambique + + + ZAR + KG + CM + AP + Namibia + + + XPF + KG + CM + New Caledonia + + + XOF + KG + CM + Niger + + + NGN + KG + CM + AP + Nigeria + + + NIO + KG + CM + AM + Nicaragua + + + EUR + KG + CM + EA + Netherlands, The + + + NOK + KG + CM + EA + Norway + + + NPR + KG + CM + AP + Nepal + + + AUD + KG + CM + Nauru, Republic Of + + + NZD + KG + CM + Niue + + + NZD + KG + CM + AP + New Zealand + + + OMR + KG + CM + AP + Oman + + + PAB + KG + CM + AM + Panama + + + PEN + KG + CM + AM + Peru + + + XPF + KG + CM + Tahiti + + + PGK + KG + CM + Papua New Guinea + + + PHP + KG + CM + AP + Philippines, The + + + PKR + KG + CM + AP + Pakistan + + + PLN + KG + CM + EA + Poland + + + USD + LB + IN + AM + Puerto Rico + + + EUR + KG + CM + EA + Portugal + + + USD + KG + CM + Palau + + + PYG + KG + CM + AM + Paraguay + + + QAR + KG + CM + AP + Qatar + + + EUR + KG + CM + AP + Reunion, Island Of + + + RON + KG + CM + EA + Romania + + + RSD + KG + CM + AP + Serbia, Republic of + + + RUB + KG + CM + AP + Russian Federation, The + + + RWF + KG + CM + Rwanda + + + SAR + KG + CM + AP + Saudi Arabia + + + SBD + KG + CM + Solomon Islands + + + SCR + KG + CM + Seychelles + + + SDG + KG + CM + AP + Sudan + + + SEK + KG + CM + EA + Sweden + + + SGD + KG + CM + AP + Singapore + + + EUR + KG + CM + EA + Slovenia + + + EUR + KG + CM + EA + Slovakia + + + SLL + KG + CM + Sierra Leone + + + EUR + KG + CM + San Marino + + + XOF + KG + CM + AP + Senegal + + + SOS + KG + CM + Somalia + + + SRD + KG + CM + AM + Suriname + + + STD + KG + CM + Sao Tome and Principe + + + USD + KG + CM + AM + El Salvador + + + SYP + KG + CM + Syria + + + SZL + KG + CM + Swaziland + + + USD + KG + CM + AM + Turks and Caicos Islands + +
    + XAF + KG + CM + Chad + + THB + KG + CM + AP + Thailand +
    + + + + + + + + + + + + +
      + + +
    + + + + +
    +
    + + +
    +
    + + + + + +getUrl('*/*/rollback'); + $backupUrl = $this->getUrl('*/*/create'); +?> + + diff --git a/app/design/adminhtml/default/default/template/backup/list.phtml b/app/design/adminhtml/default/default/template/backup/list.phtml index bc4dad963d..1a3183d636 100644 --- a/app/design/adminhtml/default/default/template/backup/list.phtml +++ b/app/design/adminhtml/default/default/template/backup/list.phtml @@ -28,8 +28,13 @@ - +

    __('Backups') ?>

    getCreateButtonHtml(); ?> + getCreateSnapshotButtonHtml(); ?> + getCreateMediaBackupButtonHtml(); ?> + getCreateButtonHtml(); ?> +
    getGridHtml() ?> +getDialogsHtml() ?> diff --git a/app/design/adminhtml/default/default/template/captcha/zend.phtml b/app/design/adminhtml/default/default/template/captcha/zend.phtml new file mode 100644 index 0000000000..5ba346aff9 --- /dev/null +++ b/app/design/adminhtml/default/default/template/captcha/zend.phtml @@ -0,0 +1,57 @@ + +getCaptchaModel() ?> + +
    +
    +
    +
    + +
    +
    + <?php echo $this->__('Reload captcha') ?> + +
    +
    +isCaseSensitive()) :?> +
    +

    __('Attention: Captcha is case sensitive.') ?>

    +
    + + diff --git a/app/design/adminhtml/default/default/template/catalog/product/attribute/js.phtml b/app/design/adminhtml/default/default/template/catalog/product/attribute/js.phtml index 6894d7458b..f413e0729f 100644 --- a/app/design/adminhtml/default/default/template/catalog/product/attribute/js.phtml +++ b/app/design/adminhtml/default/default/template/catalog/product/attribute/js.phtml @@ -117,26 +117,33 @@ function bindAttributeInputType() setRowVisibility('is_wysiwyg_enabled', false); setRowVisibility('is_html_allowed_on_front', false); - if ($('frontend_input').value=='textarea') { - setRowVisibility('is_wysiwyg_enabled', true); - if($('is_wysiwyg_enabled').value == '0'){ + + switch ($('frontend_input').value) { + case 'textarea': + setRowVisibility('is_wysiwyg_enabled', true); + if($('is_wysiwyg_enabled').value == '0'){ + setRowVisibility('is_html_allowed_on_front', true); + $('is_html_allowed_on_front').disabled = false; + } + $('frontend_class').value = ''; + $('frontend_class').disabled = true; + break; + case 'text': setRowVisibility('is_html_allowed_on_front', true); $('is_html_allowed_on_front').disabled = false; - } - $('frontend_class').value = ''; - $('frontend_class').disabled = true; - } - else if($('frontend_input').value=='text'){ - setRowVisibility('is_html_allowed_on_front', true); - $('is_html_allowed_on_front').disabled = false; - if (!$('frontend_class').getAttribute('readonly')) { - $('frontend_class').disabled = false; - } - } - else{ - $('frontend_class').value = ''; - $('frontend_class').disabled = true; + if (!$('frontend_class').getAttribute('readonly')) { + $('frontend_class').disabled = false; + } + break; + case 'select': + case 'multiselect': + setRowVisibility('is_html_allowed_on_front', true); + $('is_html_allowed_on_front').disabled = false; + break; + default: + $('frontend_class').value = ''; + $('frontend_class').disabled = true; } switchIsFilterable(); diff --git a/app/design/adminhtml/default/default/template/catalog/product/attribute/set/main.phtml b/app/design/adminhtml/default/default/template/catalog/product/attribute/set/main.phtml index 4438cef1f5..cdb511c6e1 100644 --- a/app/design/adminhtml/default/default/template/catalog/product/attribute/set/main.phtml +++ b/app/design/adminhtml/default/default/template/catalog/product/attribute/set/main.phtml @@ -229,12 +229,12 @@ } if( editSet.SystemNodesExists(editSet.currentNode) ) { - alert('__('This group contains system attributes. Please move system attributes to another group and try again.') ?>'); + alert('jsQuoteEscape(Mage::helper('catalog')->__('This group contains system attributes. Please move system attributes to another group and try again.')) ?>'); return; } if (editSet.ConfigurableNodeExists(editSet.currentNode)) { - alert('__('This group contains attributes, used in configurable products. Please move these attributes to another group and try again.') ?>'); + alert('jsQuoteEscape(Mage::helper('catalog')->__('This group contains attributes, used in configurable products. Please move these attributes to another group and try again.')) ?>'); return; } @@ -362,7 +362,7 @@ }, failure : function(o) { - alert('__('Unable to complete this request.') ?>'); + alert('jsQuoteEscape(Mage::helper('catalog')->__('Unable to complete this request.')) ?>'); }, groupBeforeMove : function(tree, nodeThis, oldParent, newParent) { @@ -377,11 +377,11 @@ rightBeforeAppend : function(tree, nodeThis, node, newParent) { if (node.attributes.is_user_defined == 0) { - alert('__('You cannot remove system attribute from this set.') ?>'); + alert('jsQuoteEscape(Mage::helper('catalog')->__('You cannot remove system attribute from this set.')) ?>'); return false; } else if (node.attributes.is_configurable == 1) { - alert('__('This attribute is used in configurable products. You cannot remove it from the attribute set.') ?>'); + alert('jsQuoteEscape(Mage::helper('catalog')->__('This attribute is used in configurable products. You cannot remove it from the attribute set.')) ?>'); return false; } else { @@ -396,11 +396,11 @@ } if (node.attributes.is_user_defined == 0) { - alert('__('You cannot remove system attribute from this set.') ?>'); + alert('jsQuoteEscape(Mage::helper('catalog')->__('You cannot remove system attribute from this set.')) ?>'); return false; } else if (node.attributes.is_configurable == 1) { - alert('__('This attribute is used in configurable products. You cannot remove it from the attribute set.') ?>'); + alert('jsQuoteEscape(Mage::helper('catalog')->__('This attribute is used in configurable products. You cannot remove it from the attribute set.')) ?>'); return false; } else { diff --git a/app/design/adminhtml/default/default/template/catalog/product/edit/action/inventory.phtml b/app/design/adminhtml/default/default/template/catalog/product/edit/action/inventory.phtml index b62594e751..768e6b28b9 100644 --- a/app/design/adminhtml/default/default/template/catalog/product/edit/action/inventory.phtml +++ b/app/design/adminhtml/default/default/template/catalog/product/edit/action/inventory.phtml @@ -62,7 +62,7 @@ function toggleValueElementsWithCheckbox(checkbox) { - __('[GLOBAL]') ?> + __('[GLOBAL]') ?> @@ -71,7 +71,7 @@ function toggleValueElementsWithCheckbox(checkbox) { - __('[GLOBAL]') ?> + __('[GLOBAL]') ?> @@ -81,7 +81,7 @@ function toggleValueElementsWithCheckbox(checkbox) { - __('[GLOBAL]') ?> + __('[GLOBAL]') ?> @@ -91,7 +91,7 @@ function toggleValueElementsWithCheckbox(checkbox) { - __('[GLOBAL]') ?> + __('[GLOBAL]') ?> @@ -101,7 +101,7 @@ function toggleValueElementsWithCheckbox(checkbox) { - __('[GLOBAL]') ?> + __('[GLOBAL]') ?> @@ -112,7 +112,7 @@ function toggleValueElementsWithCheckbox(checkbox) { - __('[GLOBAL]') ?> + __('[GLOBAL]') ?> @@ -127,7 +127,7 @@ function toggleValueElementsWithCheckbox(checkbox) { - __('[GLOBAL]') ?> + __('[GLOBAL]') ?> @@ -137,7 +137,7 @@ function toggleValueElementsWithCheckbox(checkbox) { - __('[GLOBAL]') ?> + __('[GLOBAL]') ?> @@ -151,7 +151,7 @@ function toggleValueElementsWithCheckbox(checkbox) { - __('[GLOBAL]') ?> + __('[GLOBAL]') ?> @@ -162,7 +162,7 @@ function toggleValueElementsWithCheckbox(checkbox) { - __('[GLOBAL]') ?> + __('[GLOBAL]') ?> @@ -174,7 +174,7 @@ function toggleValueElementsWithCheckbox(checkbox) { - __('[GLOBAL]') ?> + __('[GLOBAL]') ?>
    diff --git a/app/design/adminhtml/default/default/template/catalog/product/edit/options/type/file.phtml b/app/design/adminhtml/default/default/template/catalog/product/edit/options/type/file.phtml index 72b610e328..05c0e499b1 100644 --- a/app/design/adminhtml/default/default/template/catalog/product/edit/options/type/file.phtml +++ b/app/design/adminhtml/default/default/template/catalog/product/edit/options/type/file.phtml @@ -38,7 +38,7 @@ OptionTemplateFile = ''+ ''+ ''+ getCanReadPrice() !== false) : ?> - '' + + '' + '' + '' + diff --git a/app/design/adminhtml/default/default/template/catalog/product/edit/price/group.phtml b/app/design/adminhtml/default/default/template/catalog/product/edit/price/group.phtml new file mode 100644 index 0000000000..38a353cda7 --- /dev/null +++ b/app/design/adminhtml/default/default/template/catalog/product/edit/price/group.phtml @@ -0,0 +1,169 @@ + +getElement()->getHtmlId(); +$_htmlClass = $this->getElement()->getClass(); +$_htmlName = $this->getElement()->getName(); +$_readonly = $this->getElement()->getReadonly(); +$_priceValueValidation = $this->getPriceValidation('validate-greater-than-zero'); + +$_showWebsite= $this->isMultiWebsites(); +?> + + + diff --git a/app/design/adminhtml/default/default/template/catalog/product/edit/price/tier.phtml b/app/design/adminhtml/default/default/template/catalog/product/edit/price/tier.phtml index 7af1fe4e28..877844d540 100644 --- a/app/design/adminhtml/default/default/template/catalog/product/edit/price/tier.phtml +++ b/app/design/adminhtml/default/default/template/catalog/product/edit/price/tier.phtml @@ -80,11 +80,11 @@ var tierPriceRowTemplate = '' + '' + '' + + ' __("and above")?>' + '' + '' + + '' + ''; var tierPriceControl = { diff --git a/app/design/adminhtml/default/default/template/catalog/product/edit/super/config.phtml b/app/design/adminhtml/default/default/template/catalog/product/edit/super/config.phtml index 8bc7ffd9f5..9c679082f6 100644 --- a/app/design/adminhtml/default/default/template/catalog/product/edit/super/config.phtml +++ b/app/design/adminhtml/default/default/template/catalog/product/edit/super/config.phtml @@ -72,7 +72,7 @@
    - isReadonly()):?> disabled="disabled" class="input-text attribute-label required-entry template no-display" value="'{{label}}'" readonly="label" /> + isAttributesConfigurationReadonly()): ?> disabled="disabled" class="input-text attribute-label required-entry template no-display" value="'{{label}}'" readonly="label" />
      @@ -87,17 +87,17 @@
      __('Price:') ?> - isReadonly() || $this->getCanEditPrice() === false):?> disabled="disabled" class="input-text attribute-price validate-number template no-display" value="'{{pricing_value}}'"/> + isAttributesPricesReadonly() || $this->getCanEditPrice() === false): ?> disabled="disabled" class="input-text attribute-price validate-number template no-display" value="'{{pricing_value}}'"/>
      -  isAttributesPricesReadonly() || $this->getCanEditPrice() === false): ?> disabled="disabled" >
      getShowUseDefaultPrice()):?>
      -  isReadonly() || $this->getCanEditPrice() === false):?> disabled="disabled" class="attribute-use-default-value"> +  isAttributesPricesReadonly() || $this->getCanEditPrice() === false): ?> disabled="disabled" class="attribute-use-default-value">
      diff --git a/app/design/adminhtml/default/default/template/catalog/product/price.phtml b/app/design/adminhtml/default/default/template/catalog/product/price.phtml index 991737ddb6..c01b4e541e 100644 --- a/app/design/adminhtml/default/default/template/catalog/product/price.phtml +++ b/app/design/adminhtml/default/default/template/catalog/product/price.phtml @@ -213,13 +213,13 @@ $_inclTax = $_taxHelper->getPrice($_product, $_minimalPriceValue, $includingTax

      __('Special Price:') ?> - __('Excl. Tax:') ?> + helper('tax')->__('Excl. Tax:') ?> currencyByStore($_finalPrice+$_weeeTaxAmount, $_storeId, true, false) ?> - __('Incl. Tax:') ?> + helper('tax')->__('Incl. Tax:') ?> currencyByStore($_finalPriceInclTax+$_weeeTaxAmount, $_storeId, true, false) ?> @@ -245,7 +245,7 @@ $_inclTax = $_taxHelper->getPrice($_product, $_minimalPriceValue, $includingTax

      __('Special Price:') ?> - __('Excl. Tax:') ?> + helper('tax')->__('Excl. Tax:') ?> currencyByStore($_finalPrice+$_weeeTaxAmount, $_storeId, true, false) ?> @@ -275,7 +275,7 @@ $_inclTax = $_taxHelper->getPrice($_product, $_minimalPriceValue, $includingTax

      __('Special Price:') ?> - __('Excl. Tax:') ?> + helper('tax')->__('Excl. Tax:') ?> currencyByStore($_finalPrice+$_weeeTaxAmount, $_storeId, true, false) ?> @@ -305,7 +305,7 @@ $_inclTax = $_taxHelper->getPrice($_product, $_minimalPriceValue, $includingTax

      __('Special Price:') ?> - __('Excl. Tax:') ?> + helper('tax')->__('Excl. Tax:') ?> currencyByStore($_finalPrice, $_storeId, true, false) ?> @@ -316,7 +316,7 @@ $_inclTax = $_taxHelper->getPrice($_product, $_minimalPriceValue, $includingTax - __('Incl. Tax:') ?> + helper('tax')->__('Incl. Tax:') ?> currencyByStore($_finalPriceInclTax+$_weeeTaxAmount, $_storeId, true, false) ?> @@ -334,7 +334,7 @@ $_inclTax = $_taxHelper->getPrice($_product, $_minimalPriceValue, $includingTax

      __('Special Price:') ?> - __('Excl. Tax:') ?> + helper('tax')->__('Excl. Tax:') ?> currencyByStore($_finalPrice, $_storeId, true, false) ?> diff --git a/app/design/adminhtml/default/default/template/catalog/product/tab/inventory.phtml b/app/design/adminhtml/default/default/template/catalog/product/tab/inventory.phtml index 383a16ceef..85798dad02 100644 --- a/app/design/adminhtml/default/default/template/catalog/product/tab/inventory.phtml +++ b/app/design/adminhtml/default/default/template/catalog/product/tab/inventory.phtml @@ -49,7 +49,7 @@ onclick="toggleValueElements(this, this.parentNode);" class="checkbox" /> isReadonly()):?> -

    + getProduct()->isComposite()): ?> @@ -61,7 +61,7 @@ /> - + @@ -72,7 +72,7 @@ onclick="toggleValueElements(this, this.parentNode);" /> isReadonly()):?> - + @@ -83,7 +83,7 @@ onclick="toggleValueElements(this, this.parentNode);" class="checkbox" /> isReadonly()):?> - + @@ -94,7 +94,7 @@ onclick="toggleValueElements(this, this.parentNode);" class="checkbox" /> isReadonly()):?> - + canUseQtyDecimals()): ?> @@ -105,7 +105,7 @@ - + @@ -122,7 +122,7 @@ onclick="toggleValueElements(this, this.parentNode);" class="checkbox" /> isReadonly()):?> - + @@ -132,7 +132,7 @@ onclick="toggleValueElements(this, this.parentNode);" class="checkbox" /> isReadonly()):?> - + @@ -147,7 +147,7 @@ onclick="toggleValueElements(this, this.parentNode);" class="checkbox" /> isReadonly()):?> - + @@ -157,7 +157,7 @@ onclick="toggleValueElements(this, this.parentNode);" class="checkbox" /> isReadonly()):?> - + @@ -168,7 +168,7 @@ - +
    getCanEditPrice() === false) : ?> disabled="disabled">getCanEditPrice() === false) : ?> disabled="disabled">getPriceTypeSelectHtml() ?>
    getElement()->getLabel(); ?> + + + + + + + + + + + + + + + + + + + + + + +
    style="display: none;">__('Website'); ?>__('Customer Group'); ?>getPriceColumnHeader(Mage::helper('catalog')->__('Price')); ?>__('Action'); ?>
    style="display: none;">getAddButtonHtml(); ?>
    + + +
    ' - + ' __("and above")?>' - + '
    __('[GLOBAL]') ?>__('[GLOBAL]') ?>
    __('[GLOBAL]') ?>__('[GLOBAL]') ?>
    __('[GLOBAL]') ?>__('[GLOBAL]') ?>
    __('[GLOBAL]') ?>__('[GLOBAL]') ?>
    __('[GLOBAL]') ?>__('[GLOBAL]') ?>
    __('[GLOBAL]') ?>__('[GLOBAL]') ?>
    __('[GLOBAL]') ?>__('[GLOBAL]') ?>
    __('[GLOBAL]') ?>__('[GLOBAL]') ?>
    __('[GLOBAL]') ?>__('[GLOBAL]') ?>
    __('[GLOBAL]') ?>__('[GLOBAL]') ?>
    __('[GLOBAL]') ?>__('[GLOBAL]') ?>
    + diff --git a/app/design/adminhtml/default/default/template/customer/tab/addresses.phtml b/app/design/adminhtml/default/default/template/customer/tab/addresses.phtml index 355c88f91d..5ef8ccc96f 100644 --- a/app/design/adminhtml/default/default/template/customer/tab/addresses.phtml +++ b/app/design/adminhtml/default/default/template/customer/tab/addresses.phtml @@ -406,8 +406,12 @@ addressesModel.prototype = { var html = this.itemContentTemplate.evaluate(data); html = html.replace(new RegExp('(\\s*){2,}','img'),'
    '); html = html.replace(new RegExp('(\\s*,){1,}\\s*','ig'),'
    '); + html = html.replace(new RegExp('(\\s*,){1,}(.*)','ig'),'
    $2
    '); + html = html.replace(new RegExp('(.*?)(,\\s*){1,}','ig'),'
    $1
    '); + html = html.replace(new RegExp('(.*?)(,\\s*){2,}(.*?)','ig'),'
    $1, $3
    '); html = html.replace(new RegExp('t:\\s*','ig'),''); - html = html.replace(new RegExp('f:\\s*$','ig'),''); + html = html.replace(new RegExp('f:\\s*','ig'),''); + html = html.replace(new RegExp('vat:\\s*$','ig'),''); itemContainer[0].innerHTML = html; } } diff --git a/app/design/adminhtml/default/default/template/customer/tab/cart.phtml b/app/design/adminhtml/default/default/template/customer/tab/cart.phtml index 512e7a16f6..6bde7d40df 100644 --- a/app/design/adminhtml/default/default/template/customer/tab/cart.phtml +++ b/app/design/adminhtml/default/default/template/customer/tab/cart.phtml @@ -65,10 +65,10 @@ removeItem: function (itemId) { if (!itemId) { - alert('__('No item specified.') ?>'); + alert('jsQuoteEscape(Mage::helper('customer')->__('No item specified.')) ?>'); return false; } - if(!confirm('__('Are you sure that you want to remove this item?') ?>')) { + if(!confirm('jsQuoteEscape(Mage::helper('customer')->__('Are you sure that you want to remove this item?')) ?>')) { return false; } diff --git a/app/design/adminhtml/default/default/template/downloadable/product/edit/downloadable.phtml b/app/design/adminhtml/default/default/template/downloadable/product/edit/downloadable.phtml index 188b66fc16..0fa88510e6 100644 --- a/app/design/adminhtml/default/default/template/downloadable/product/edit/downloadable.phtml +++ b/app/design/adminhtml/default/default/template/downloadable/product/edit/downloadable.phtml @@ -230,7 +230,7 @@ Validation.addAllThese([ newFileContainer = element.up(0).down('div.new-file'); if (!alertAlreadyDisplayed && (newFileContainer.empty() || newFileContainer.style.display != 'none')) { alertAlreadyDisplayed = true; - alert('There are files that were selected but not uploaded yet. Please upload or remove them first'); + alert('jsQuoteEscape($this->__('There are files that were selected but not uploaded yet. Please upload or remove them first'));?>'); } return false; } diff --git a/app/design/adminhtml/default/default/template/downloadable/product/edit/downloadable/links.phtml b/app/design/adminhtml/default/default/template/downloadable/product/edit/downloadable/links.phtml index caf70a67fa..2ccd1520f1 100644 --- a/app/design/adminhtml/default/default/template/downloadable/product/edit/downloadable/links.phtml +++ b/app/design/adminhtml/default/default/template/downloadable/product/edit/downloadable/links.phtml @@ -40,7 +40,7 @@ getStoreId() && $this->getUsedDefault())?'disabled="disabled"':'' ?> /> - isSingleStoreMode()): ?>[STORE VIEW] + isSingleStoreMode() ? Mage::helper('adminhtml')->__('[STORE VIEW]') : ''; ?> getStoreId()): ?> getUsedDefault()?'checked="checked"':'' ?> /> @@ -59,7 +59,7 @@ getPurchasedSeparatelySelect()?> - isSingleStoreMode()): ?>[GLOBAL] + isSingleStoreMode() ? Mage::helper('adminhtml')->__('[GLOBAL]') : ''; ?>   diff --git a/app/design/adminhtml/default/default/template/downloadable/product/edit/downloadable/samples.phtml b/app/design/adminhtml/default/default/template/downloadable/product/edit/downloadable/samples.phtml index 3116693f0a..100ef7a240 100644 --- a/app/design/adminhtml/default/default/template/downloadable/product/edit/downloadable/samples.phtml +++ b/app/design/adminhtml/default/default/template/downloadable/product/edit/downloadable/samples.phtml @@ -40,7 +40,7 @@ getStoreId() && $this->getUsedDefault())?'disabled="disabled"':'' ?> /> - isSingleStoreMode()): ?>[STORE VIEW] + isSingleStoreMode() ? Mage::helper('adminhtml')->__('[STORE VIEW]') : ''; ?> getStoreId()): ?> getUsedDefault()?'checked="checked"':'' ?> /> diff --git a/app/design/adminhtml/default/default/template/forgotpassword.phtml b/app/design/adminhtml/default/default/template/forgotpassword.phtml index 4a146cda96..94150e9891 100644 --- a/app/design/adminhtml/default/default/template/forgotpassword.phtml +++ b/app/design/adminhtml/default/default/template/forgotpassword.phtml @@ -28,38 +28,40 @@ - <?php echo Mage::helper('adminhtml')->__('Log into Magento Admin Page') ?> - - - - + <?php echo Mage::helper('adminhtml')->__('Log into Magento Admin Page'); ?> + + + + - + + - - - + + + + + diff --git a/app/design/frontend/default/iphone/template/checkout/onepage/login.phtml b/app/design/frontend/default/iphone/template/checkout/onepage/login.phtml new file mode 100644 index 0000000000..efd3e24dc7 --- /dev/null +++ b/app/design/frontend/default/iphone/template/checkout/onepage/login.phtml @@ -0,0 +1,121 @@ + +
    + getChildHtml('login_before')?> +
    +

    getQuote()->isAllowedGuestCheckout() ): ?>__('Checkout as a Guest or Register') ?>__('Register to Create an Account') ?>

    + getQuote()->isAllowedGuestCheckout() ): ?> +

    __('Register with us for future convenience:') ?>

    + +

    __('Register and save time!') ?>
    + __('Register with us for future convenience:') ?>

    +
      +
    • __('Fast and easy check out') ?>
    • +
    • __('Easy access to your order history and status') ?>
    • +
    + + getQuote()->isAllowedGuestCheckout() ): ?> +
      + getQuote()->isAllowedGuestCheckout() ): ?> +
    • + getQuote()->getCheckoutMethod()==Mage_Checkout_Model_Type_Onepage::METHOD_GUEST): ?> checked="checked" class="radio" /> +
    • + +
    • + getQuote()->getCheckoutMethod()==Mage_Checkout_Model_Type_Onepage::METHOD_REGISTER || !$this->getQuote()->isAllowedGuestCheckout()): ?> checked="checked" class="radio" /> +
    • +
    +

    __('Register and save time!') ?>

    +

    __('Register with us for future convenience:') ?>

    +
      +
    • __('Fast and easy check out') ?>
    • +
    • __('Easy access to your order history and status') ?>
    • +
    + + + +
    +
    +

    __('Login') ?>

    + getMessagesBlock()->getGroupedHtml() ?> +
    +
    +

    __('Already registered?') ?>

    +

    __('Please log in below:') ?>

    +
      +
    • + +
      + +
      +
    • +
    • + +
      + +
      +
    • + getChildHtml('form.additional.info'); ?> +
    +
    +
    +
    +
    +
    +
    +
    +

     

    + +
    +
    +
    +
    +

    __('* Required Fields') ?>

    + __('Forgot your password?') ?> + +
    +
    +
    + diff --git a/app/design/frontend/default/iphone/template/customer/form/edit.phtml b/app/design/frontend/default/iphone/template/customer/form/edit.phtml new file mode 100644 index 0000000000..f71f9b9154 --- /dev/null +++ b/app/design/frontend/default/iphone/template/customer/form/edit.phtml @@ -0,0 +1,127 @@ + + + + + diff --git a/app/design/frontend/default/iphone/template/customer/form/forgotpassword.phtml b/app/design/frontend/default/iphone/template/customer/form/forgotpassword.phtml index e8052bb53e..53d199047b 100644 --- a/app/design/frontend/default/iphone/template/customer/form/forgotpassword.phtml +++ b/app/design/frontend/default/iphone/template/customer/form/forgotpassword.phtml @@ -24,18 +24,20 @@ * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ ?> -
    -

    __('Retrieve your password here') ?>

    -
    +

    + __('Back to Login') ?> + __('Retrieve Password') ?> +

    getMessagesBlock()->getGroupedHtml() ?>
    - - -
    - « __('Back to Login') ?> - -
    +

    __('Retrieve your password here') ?>

    + + +
      + getChildHtml('form.additional.info'); ?> +
    +
    + + +
    +

    __('New Customers') ?>

    + +

    __('By creating an account with our store, you will be able to move through the checkout process faster, store multiple shipping addresses, view and track your orders in your account and more.') ?>

    +
    + diff --git a/app/design/frontend/default/iphone/template/customer/form/register.phtml b/app/design/frontend/default/iphone/template/customer/form/register.phtml new file mode 100644 index 0000000000..95a182d397 --- /dev/null +++ b/app/design/frontend/default/iphone/template/customer/form/register.phtml @@ -0,0 +1,178 @@ + + + +
    +

    __('New Customer') ?>

    +
    +
    + + + getChildHtml('form_fields_before')?> + getMessagesBlock()->getGroupedHtml() ?> +
    +

    __('Personal Information') ?>

    +
      +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • + isNewsletterEnabled()): ?> +
    • + +
    +
    + getShowAddressFields()): ?> +
    + +

    __('Address Information') ?>

    +
      +
    • +
      + +
      + +
      +
      +
      + +
      + +
      +
      +
    • +
    • + +
      + +
      +
    • + helper('customer/address')->getStreetLines(); $_i<=$_n; $_i++): ?> +
    • +
      + +
      +
    • + +
    • +
      + +
      + +
      +
      +
      + +
      + + + +
      +
      +
    • +
    • +
      + +
      + +
      +
      +
      + +
      + getCountryHtmlSelect() ?> +
      +
      +
    • +
    + + +
    + +
    +
      + getLayout()->createBlock('customer/widget_dob') ?> + isEnabled()): ?> +
    • setDate($this->getFormData()->getDob())->toHtml() ?>
    • + + getLayout()->createBlock('customer/widget_taxvat') ?> + isEnabled()): ?> +
    • setTaxvat($this->getFormData()->getTaxvat())->toHtml() ?>
    • + + getLayout()->createBlock('customer/widget_gender') ?> + isEnabled()): ?> +
    • setGender($this->getFormData()->getGender())->toHtml() ?>
    • + +
    +
    + +
    +
    +
    + diff --git a/app/design/frontend/default/iphone/template/page/1column.phtml b/app/design/frontend/default/iphone/template/page/1column.phtml index 57eee7bb78..0ff5cce312 100644 --- a/app/design/frontend/default/iphone/template/page/1column.phtml +++ b/app/design/frontend/default/iphone/template/page/1column.phtml @@ -24,37 +24,24 @@ * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ ?> - - - + + getChildHtml('head') ?> - -getChildHtml('after_body_start') ?> -getChildHtml('global_notices') ?> -
    +getBodyClass()?' class="'.$this->getBodyClass().'"':'' ?>> + getChildHtml('after_body_start') ?> + getChildHtml('global_notices') ?> getChildHtml('header') ?> -
    -
    -
    + +
    getChildHtml('breadcrumbs') ?> -
    -
    - getChildHtml('content') ?> -
    -
    -
    - -getChildHtml('before_body_end') ?> -getAbsoluteFooter() ?> + getChildHtml('content') ?> + + + getChildHtml('footer') ?> + + getChildHtml('before_body_end') ?> + getAbsoluteFooter() ?> diff --git a/app/design/frontend/default/iphone/template/page/html/footer.phtml b/app/design/frontend/default/iphone/template/page/html/footer.phtml index 7f0d407ac2..9719e392dc 100644 --- a/app/design/frontend/default/iphone/template/page/html/footer.phtml +++ b/app/design/frontend/default/iphone/template/page/html/footer.phtml @@ -24,9 +24,7 @@ * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ ?> -
    +
    getChildHtml() ?> -
    -

    - getCopyright() ?> -

    +

    getCopyright() ?>

    + diff --git a/app/design/frontend/default/iphone/template/page/html/head.phtml b/app/design/frontend/default/iphone/template/page/html/head.phtml index 039e486d9f..3ff3587703 100644 --- a/app/design/frontend/default/iphone/template/page/html/head.phtml +++ b/app/design/frontend/default/iphone/template/page/html/head.phtml @@ -24,14 +24,24 @@ * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ ?> - - <?php echo htmlspecialchars(html_entity_decode($this->getTitle())) ?> - - +<?php echo $this->getTitle() ?> + + + + + + + + + getCssJsHtml() ?> getChildHtml() ?> +helper('core/js')->getTranslatorScript() ?> +getIncludes() ?> diff --git a/app/design/frontend/default/iphone/template/page/html/header.phtml b/app/design/frontend/default/iphone/template/page/html/header.phtml index 06f92c23c3..2381c22ae7 100644 --- a/app/design/frontend/default/iphone/template/page/html/header.phtml +++ b/app/design/frontend/default/iphone/template/page/html/header.phtml @@ -27,24 +27,35 @@ * @var Mage_Page_Block_Html_Header $this */ ?> - -getChildHtml('topSearch') ?> -
    -
    -
    -

    <?php echo $this->getLogoAlt() ?>

    -
    -
    -
    - getChildHtml('store_language') ?> -
    -
    - -

    getWelcome()?>

    - getChildHtml('accountLinks') ?> -
    +helper('checkout/cart')->getSummaryCount(); ?> +
    + -
    -
    - getChildHtml('topLinks') ?> -
    + + getChildHtml('accountLinks') ?> + +getChildHtml('topCart') ?> diff --git a/app/design/frontend/default/iphone/template/page/html/pager.phtml b/app/design/frontend/default/iphone/template/page/html/pager.phtml new file mode 100644 index 0000000000..0283083ce1 --- /dev/null +++ b/app/design/frontend/default/iphone/template/page/html/pager.phtml @@ -0,0 +1,113 @@ + + +getCollection()->getSize()): ?> + + getUseContainer()): ?> +
    + + + getShowAmounts()): ?> +

    + + getLastPageNum()>1): ?> + __('Items %s to %s of %s total', $this->getFirstNum(), $this->getLastNum(), $this->getTotalNum()) ?> + + __('%s Item(s)', $this->getTotalNum()) ?> + +

    + + + getShowPerPage()): ?> +
    + + __('per page') ?> +
    + + + getLastPageNum()>1): ?> +
    + __('Page') ?> + + + isFirstPage()): ?> + + +   + + + isLastPage()): ?> +   + +   + + +
    + + + getUseContainer()): ?> +
    + + + diff --git a/app/design/frontend/default/iphone/template/page/switch/languages.phtml b/app/design/frontend/default/iphone/template/page/switch/languages.phtml new file mode 100644 index 0000000000..6ee5459731 --- /dev/null +++ b/app/design/frontend/default/iphone/template/page/switch/languages.phtml @@ -0,0 +1,46 @@ + + +getStores())>1): ?> +
  • + __('Language') ?>: + +
  • + diff --git a/app/design/frontend/default/iphone/template/page/switch/stores.phtml b/app/design/frontend/default/iphone/template/page/switch/stores.phtml new file mode 100644 index 0000000000..a212888862 --- /dev/null +++ b/app/design/frontend/default/iphone/template/page/switch/stores.phtml @@ -0,0 +1,46 @@ + + +getGroups())>1): ?> +
  • + __('Selected Store') ?>: + +
  • + diff --git a/app/design/frontend/default/iphone/template/persistent/checkout/onepage/login.phtml b/app/design/frontend/default/iphone/template/persistent/checkout/onepage/login.phtml new file mode 100644 index 0000000000..54fea170f5 --- /dev/null +++ b/app/design/frontend/default/iphone/template/persistent/checkout/onepage/login.phtml @@ -0,0 +1,154 @@ + + +
    + getChildHtml('login_before')?> + +
    +

    getQuote()->isAllowedGuestCheckout() ): ?>__('Checkout as a Guest or Register') ?>__('Register to Create an Account') ?>

    + getQuote()->isAllowedGuestCheckout() ): ?> +

    __('Register with us for future convenience:') ?>

    + +

    __('Register and save time!') ?>
    + __('Register with us for future convenience:') ?>

    +
      +
    • __('Fast and easy check out') ?>
    • +
    • __('Easy access to your order history and status') ?>
    • +
    + + getQuote()->isAllowedGuestCheckout() ): ?> +
      + getQuote()->isAllowedGuestCheckout() ): ?> +
    • + getQuote()->getCheckoutMethod()==Mage_Checkout_Model_Type_Onepage::METHOD_GUEST): ?> checked="checked" class="radio" /> +
    • + +
    • + getQuote()->getCheckoutMethod()==Mage_Checkout_Model_Type_Onepage::METHOD_REGISTER || !$this->getQuote()->isAllowedGuestCheckout()): ?> checked="checked" class="radio" /> +
    • +
    + + + + +
    +
    +
    +
    +
    + + getQuote()->isAllowedGuestCheckout()): ?> + + +
    + +
    + +
    +
    +
    +
    +
    + +getRequest()->getParam('register'); + if ($registerParam || $registerParam === ''): +?> + + diff --git a/app/design/frontend/default/iphone/template/persistent/customer/form/login.phtml b/app/design/frontend/default/iphone/template/persistent/customer/form/login.phtml index ebbf0d8161..8b6cc0936e 100644 --- a/app/design/frontend/default/iphone/template/persistent/customer/form/login.phtml +++ b/app/design/frontend/default/iphone/template/persistent/customer/form/login.phtml @@ -32,38 +32,59 @@ */ /** @var $this Mage_Customer_Block_Form_Login */ ?> -
    + + + + diff --git a/app/design/frontend/default/iphone/template/sales/order/items.phtml b/app/design/frontend/default/iphone/template/sales/order/items.phtml new file mode 100644 index 0000000000..c8c501ba58 --- /dev/null +++ b/app/design/frontend/default/iphone/template/sales/order/items.phtml @@ -0,0 +1,73 @@ + +getOrder() ?> + + + getChildHtml('order_totals') ?> + + getItemsCollection(); ?> + + count(); ?> + + getParentItem()) continue; ?> + + getItemHtml($_item) ?> + helper('giftmessage/message')->getIsMessagesAvailable('order_item', $_item) && $_item->getGiftMessageId()): ?> + + helper('giftmessage/message')->getGiftMessageForEntity($_item); ?> + + + + + +
    + diff --git a/app/design/frontend/default/iphone/template/sales/order/totals.phtml b/app/design/frontend/default/iphone/template/sales/order/totals.phtml new file mode 100644 index 0000000000..46c1cada7f --- /dev/null +++ b/app/design/frontend/default/iphone/template/sales/order/totals.phtml @@ -0,0 +1,52 @@ + +getTotals() as $_code => $_total): ?> + getBlockName()): ?> + getChildHtml($_total->getBlockName(), false); ?> + + + + getStrong()):?> + getLabel()?> + + getLabel()?> + + + + getStrong()):?> + formatValue($_total) ?> + + formatValue($_total) ?> + + + + + diff --git a/app/design/frontend/default/iphone/template/sendfriend/send.phtml b/app/design/frontend/default/iphone/template/sendfriend/send.phtml new file mode 100644 index 0000000000..d492babe71 --- /dev/null +++ b/app/design/frontend/default/iphone/template/sendfriend/send.phtml @@ -0,0 +1,148 @@ + + + + + +
    + getMessagesBlock()->getGroupedHtml() ?> +
    +
    + getMaxRecipients()): ?> +

    + +

    + +

    __('Recipient:') ?>

    +
      +
    • +
      + +
      + +
      +
      +
      + +
      + +
      +
      +
    • +
    +
    + +
    + getBlockHtml('formkey')?> +

    __('Sender:') ?>

    +
      +
    • +
      + +
      + +
      +
      +
      + +
      + +
      +
      +
    • +
    • + +
      + +
      +
    • +
    +
    +
    + +
    +
    + +
    diff --git a/app/design/frontend/default/iphone/template/tag/list.phtml b/app/design/frontend/default/iphone/template/tag/list.phtml new file mode 100644 index 0000000000..6ec24ad17a --- /dev/null +++ b/app/design/frontend/default/iphone/template/tag/list.phtml @@ -0,0 +1,35 @@ + +getCount() ): ?> +
    +

    __('Product Tags') ?>

    + getChildHtml('list_before')?> +
    + renderTags('%2$s (%3$s)') ?> +
    +
    + diff --git a/app/design/frontend/default/iphone/template/wishlist/view.phtml b/app/design/frontend/default/iphone/template/wishlist/view.phtml index 10e10d8410..787bc378fd 100644 --- a/app/design/frontend/default/iphone/template/wishlist/view.phtml +++ b/app/design/frontend/default/iphone/template/wishlist/view.phtml @@ -27,74 +27,60 @@ ?> helper('wishlist')->isAllow()) : ?>
    -
    - helper('wishlist')->isRssAllow() && $this->hasWishlistItems()): ?> - __('RSS Feed') ?> - -

    __('My Wishlist') ?>

    -
    +

    __('My Wishlist') ?>

    + getMessagesBlock()->getGroupedHtml() ?> hasWishlistItems()): ?> -
    -
    +
    + + <?php echo $this->__('Remove All')?> getBlockHtml('formkey')?> - - - - - - - - - - - - - +
      getWishlistItems() as $item): ?> -
    - - - - - - - + getProduct(); + $isVisibleProduct = $product->isVisibleInSiteVisibility(); + ?> +
  • + <?php echo $this->escapeHtml($product->getName()) ?> +
    +

    + <?php echo $this->__('Remove item')?> + + __('E') ?> + + escapeHtml($product->getName()) ?> +

    + getPriceHtml($product) ?> + getDetailsHtml($item) ?> + canHaveQty() && $isVisibleProduct): ?> + __('Qty') ?>: + + + isSaleable()): ?> + + + + + __('Out of stock') ?> + + +
    +
  • - -
    __('Product') ?>__('Added On') ?>__('Add to Cart') ?>
    - <?php echo $this->escapeHtml($item->getName()) ?> -

    escapeHtml($item->getName()) ?>

    - getPriceHtml($item) ?> -
    - getFormatedDate($item->getAddedAt()) ?> - - canHaveQty()): ?> -

    - - isSaleable()): ?> - - -

    __('Out of stock') ?>

    - -

    __('Edit') ?>

    - __('Remove item')?> -
    - __('Comment:') ?> - -
    - -
    - + +
      +
    • + +
    • isSaleable()):?> - +
    • __('Add All to Cart') ?>
    • - -
    -
    -
    + + +
    -

    __('You have no items in your wishlist.') ?>

    +

    __('You have no items in your wishlist.') ?>

    - +« __('Back') ?> diff --git a/app/design/frontend/default/modern/layout/checkout.xml b/app/design/frontend/default/modern/layout/checkout.xml index 92c30467ca..8a1224a9d3 100644 --- a/app/design/frontend/default/modern/layout/checkout.xml +++ b/app/design/frontend/default/modern/layout/checkout.xml @@ -99,7 +99,7 @@ Default layout, loads most of the pages - + diff --git a/app/design/frontend/default/modern/layout/customer.xml b/app/design/frontend/default/modern/layout/customer.xml index 6dbc270645..8c24150fcc 100644 --- a/app/design/frontend/default/modern/layout/customer.xml +++ b/app/design/frontend/default/modern/layout/customer.xml @@ -91,7 +91,7 @@ Layout for customer login page - + @@ -152,7 +152,7 @@ New customer registration - + @@ -190,7 +190,7 @@ Customer account pages, rendered for all tabs in dashboard - + diff --git a/app/design/frontend/default/modern/template/catalogsearch/form.mini.phtml b/app/design/frontend/default/modern/template/catalogsearch/form.mini.phtml index 1283d1b909..06711d9620 100644 --- a/app/design/frontend/default/modern/template/catalogsearch/form.mini.phtml +++ b/app/design/frontend/default/modern/template/catalogsearch/form.mini.phtml @@ -23,17 +23,20 @@ * @copyright Copyright (c) 2011 Magento Inc. (http://www.magentocommerce.com) * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ +/* @var $this Mage_Core_Block_Template */ +/* @var $catalogSearchHelper Mage_Catalogsearch_Helper_Data */ +$catalogSearchHelper = $this->helper('catalogsearch'); ?> -
    + diff --git a/app/design/frontend/default/modern/template/checkout/cart.phtml b/app/design/frontend/default/modern/template/checkout/cart.phtml index 7f12065a07..e1469f5bdc 100644 --- a/app/design/frontend/default/modern/template/checkout/cart.phtml +++ b/app/design/frontend/default/modern/template/checkout/cart.phtml @@ -99,7 +99,8 @@ getContinueShoppingUrl()): ?> - + + diff --git a/app/design/install/default/default/template/install/config.phtml b/app/design/install/default/default/template/install/config.phtml index 6d0e7a2d3c..4e13c93c61 100644 --- a/app/design/install/default/default/template/install/config.phtml +++ b/app/design/install/default/default/template/install/config.phtml @@ -99,9 +99,6 @@
  • getFormData()->getUseSecureAdmin()): ?>checked="checked" />
    -
  • getSkipUrlValidation()): ?>checked="checked" /> diff --git a/app/etc/local.xml.additional b/app/etc/local.xml.additional index 207131bc05..c491fb81dd 100644 --- a/app/etc/local.xml.additional +++ b/app/etc/local.xml.additional @@ -58,6 +58,20 @@ to app/etc/local.xml manually. + + + + + full_page_cache + + + 1 + 0777 + fpc + + + + HTTP_X_REAL_IP HTTP_X_FORWARDED_FOR diff --git a/app/etc/modules/Mage_Captcha.xml b/app/etc/modules/Mage_Captcha.xml new file mode 100755 index 0000000000..80b850309d --- /dev/null +++ b/app/etc/modules/Mage_Captcha.xml @@ -0,0 +1,39 @@ + + + + + + true + core + + + + + + + diff --git a/app/locale/en_US/Mage_Adminhtml.csv b/app/locale/en_US/Mage_Adminhtml.csv index 3c533d9f6e..dc2719c2e9 100644 --- a/app/locale/en_US/Mage_Adminhtml.csv +++ b/app/locale/en_US/Mage_Adminhtml.csv @@ -1,7 +1,9 @@ " The customer does not exist in the system anymore."," The customer does not exist in the system anymore." +" You will need to navigate to your "," You will need to navigate to your " " [deleted]"," [deleted]" " and "," and " -" note that the URLs provided below are the correct values for your current website): "," note that the URLs provided below are the correct values for your current website): " +" and go to the "," and go to the " +" sections for your Hosted Checkout Pages."," sections for your Hosted Checkout Pages." "%s (Default Template from Locale)","%s (Default Template from Locale)" "%s cache type(s) disabled.","%s cache type(s) disabled." "%s cache type(s) enabled.","%s cache type(s) enabled." @@ -30,14 +32,17 @@ "-- Please select --","-- Please select --" "--Please Select--","--Please Select--" "1 Hour","1 Hour" +"1. set up","1. set up" "12 Hours","12 Hours" "12h AM/PM","12h AM/PM" "2 Hours","2 Hours" +"2. customize","2. customize" "24 Hours","24 Hours" "24h","24h" "2YTD","2YTD" "6 Hours","6 Hours" "

    404 Error

    Page not found.

    ","

    404 Error

    Page not found.

    " +"Attention: Captcha is case sensitive.","Attention: Captcha is case sensitive." "A user with the same user name or email aleady exists.","A user with the same user name or email aleady exists." "API Key","API Key" "API Key Confirmation","API Key Confirmation" @@ -92,11 +97,11 @@ "All possible rates were fetched, please click on ""Save"" to apply","All possible rates were fetched, please click on ""Save"" to apply" "All rates were fetched, please click on ""Save"" to apply","All rates were fetched, please click on ""Save"" to apply" "All valid rates have been saved.","All valid rates have been saved." +"Always (during development)","Always (during development)" "Amounts","Amounts" "An error has occured while syncronizing media storages.","An error has occured while syncronizing media storages." "An error occurred while clearing the JavaScript/CSS cache.","An error occurred while clearing the JavaScript/CSS cache." "An error occurred while clearing the image cache.","An error occurred while clearing the image cache." -"An error occurred while creating the backup.","An error occurred while creating the backup." "An error occurred while deleting URL Rewrite.","An error occurred while deleting URL Rewrite." "An error occurred while deleting email template data. Please review log and try again.","An error occurred while deleting email template data. Please review log and try again." "An error occurred while deleting record(s).","An error occurred while deleting record(s)." @@ -128,10 +133,12 @@ "Any Store","Any Store" "Any Type","Any Type" "Any Visibility","Any Visibility" +"Any data created since the backup was made will be lost including admin users, customers and orders.","Any data created since the backup was made will be lost including admin users, customers and orders." "Archive file name:","Archive file name:" "Are you sure that you want to delete this template?","Are you sure that you want to delete this template?" "Are you sure that you want to strip tags?","Are you sure that you want to strip tags?" "Are you sure you want to do this?","Are you sure you want to do this?" +"Are you sure you want to proceed?","Are you sure you want to proceed?" "Area","Area" "As low as:","As low as:" "Assigned","Assigned" @@ -145,7 +152,6 @@ "Back","Back" "Back to Login","Back to Login" "Backup","Backup" -"Backup record was deleted.","Backup record was deleted." "Backups","Backups" "Base currency","Base currency" "Bcc","Bcc" @@ -180,7 +186,6 @@ "Cannot initialize shipment for delete tracking number.","Cannot initialize shipment for delete tracking number." "Cannot load track with retrieving identifier.","Cannot load track with retrieving identifier." "Cannot retrieve tracking number detail.","Cannot retrieve tracking number detail." -"Cannot save a new password.","Cannot save a new password." "Cannot save shipment.","Cannot save shipment." "Cannot save the credit memo.","Cannot save the credit memo." "Cannot send shipment information.","Cannot send shipment information." @@ -230,6 +235,7 @@ "Credit memo #%s created","Credit memo #%s created" "Credit memo\'s total must be positive.","Credit memo\'s total must be positive." "Currency","Currency" +"Currency ""%s"" is used as %s in %s.","Currency ""%s"" is used as %s in %s." "Currency Information","Currency Information" "Currency Setup Section","Currency Setup Section" "Current Configuration Scope:","Current Configuration Scope:" @@ -294,9 +300,8 @@ "Display %s first","Display %s first" "Display default currency","Display default currency" "Distributed under GNU LGPL. See %s for details.","Distributed under GNU LGPL. See %s for details." -"Do not enable AVS or CSC options. The do not work when using Payflow Link Silent Mode.","Do not enable AVS or CSC options. The do not work when using Payflow Link Silent Mode." -"Do not set any fields in the Billing and Shipping Information block as editable in your Payflow account.","Do not set any fields in the Billing and Shipping Information block as editable in your Payflow account." "Do you really want to KILL parallel process and start new indexing process?","Do you really want to KILL parallel process and start new indexing process?" +"Do you really want to proceed?","Do you really want to proceed?" "Download","Download" "Downloads","Downloads" "Drag to move","Drag to move" @@ -321,14 +326,12 @@ "Email to a Friend","Email to a Friend" "Email:","Email:" "Enable","Enable" -"Enable Secure Token:","Enable Secure Token:" "Enabled","Enabled" "Enclose Values In:","Enclose Values In:" "Entity Attributes","Entity Attributes" "Entity Type","Entity Type" "Entity type:","Entity type:" "Error","Error" -"Error URL: ","Error URL: " "Excel XML","Excel XML" "Excl. Tax","Excl. Tax" "Expiration Date","Expiration Date" @@ -338,7 +341,11 @@ "Export Filters","Export Filters" "Export to:","Export to:" "Export:","Export:" +"FTP Host","FTP Host" "FTP Host[:Port]","FTP Host[:Port]" +"FTP Login","FTP Login" +"FTP Password","FTP Password" +"FTP credentials","FTP credentials" "Failed to add a product to cart by id ""%s"".","Failed to add a product to cart by id ""%s""." "Failed to cancel the billing agreement.","Failed to cancel the billing agreement." "Failed to clear the JavaScript/CSS cache.","Failed to clear the JavaScript/CSS cache." @@ -399,7 +406,6 @@ "IPN (Instant Payment Notification) Only","IPN (Instant Payment Notification) Only" "If there is an account associated with %s you will receive an email with a link to reset your password.","If there is an account associated with %s you will receive an email with a link to reset your password." "If this message persists, please contact the store owner.","If this message persists, please contact the store owner." -"If your Magento instance is used for multiple websites, you must configure a separate Payflow Link account for each website.","If your Magento instance is used for multiple websites, you must configure a separate Payflow Link account for each website." "Images (.gif, .jpg, .png)","Images (.gif, .jpg, .png)" "Images Cache","Images Cache" "Import","Import" @@ -424,9 +430,10 @@ "Invalid Import Service Specified","Invalid Import Service Specified" "Invalid POST data (please check post_max_size and upload_max_filesize settings in your php.ini file).","Invalid POST data (please check post_max_size and upload_max_filesize settings in your php.ini file)." "Invalid Secret Key. Please refresh the page.","Invalid Secret Key. Please refresh the page." -"Invalid Username or Password.","Invalid Username or Password." +"Invalid User Name or Password.","Invalid User Name or Password." "Invalid directory: %s","Invalid directory: %s" "Invalid email address ""%s"".","Invalid email address ""%s""." +"Invalid email address.","Invalid email address." "Invalid file: %s","Invalid file: %s" "Invalid input data for %s => %s rate","Invalid input data for %s => %s rate" "Invalid parent block for this block","Invalid parent block for this block" @@ -447,6 +454,7 @@ "Is Closed","Is Closed" "Issue Number","Issue Number" "Items","Items" +"JavaScript seems to be disabled in your browser.","JavaScript seems to be disabled in your browser." "JavaScript/CSS","JavaScript/CSS" "JavaScript/CSS Cache","JavaScript/CSS Cache" "Kb","Kb" @@ -493,6 +501,7 @@ "Magento Connect Manager","Magento Connect Manager" "Magento Logo","Magento Logo" "Magento is a trademark of Magento Inc. Copyright © %s Magento Inc.","Magento is a trademark of Magento Inc. Copyright © %s Magento Inc." +"Magento root directory","Magento root directory" "Magento ver. %s","Magento ver. %s" "Magento™ is a trademark of Magento Inc.
    Copyright © %s Magento Inc.","Magento™ is a trademark of Magento Inc.
    Copyright © %s Magento Inc." "Make sure that data encoding in the file is consistent and saved in one of supported encodings (UTF-8 or ANSI).","Make sure that data encoding in the file is consistent and saved in one of supported encodings (UTF-8 or ANSI)." @@ -523,6 +532,7 @@ "Name on Card","Name on Card" "Name on the Card: %s","Name on the Card: %s" "Name:","Name:" +"Never (production)","Never (production)" "New ","New " "New API Key","New API Key" "New Accounts","New Accounts" @@ -590,13 +600,14 @@ "Notifications","Notifications" "Number of Orders","Number of Orders" "Number of Uses","Number of Uses" +"Number of Views","Number of Views" "Number of records:","Number of records:" +"OK","OK" "Old rate:","Old rate:" -"On my website","On my website" -"Once you log into your Payflow Link account, navigate to the Service Settings - Hosted Checkout Pages - Set Up menu and set the options described below","Once you log into your Payflow Link account, navigate to the Service Settings - Hosted Checkout Pages - Set Up menu and set the options described below" "One or more media files failed to be synchronized during the media storages syncronization process. Refer to the log file for details.","One or more media files failed to be synchronized during the media storages syncronization process. Refer to the log file for details." "One or more of the Cache Types are invalidated:","One or more of the Cache Types are invalidated:" "Online Customers","Online Customers" +"Only Once (version upgrade)","Only Once (version upgrade)" "Only attributes with scope ""Global"", input type ""Dropdown"" and Use To Create Configurable Product ""Yes"" are available.","Only attributes with scope ""Global"", input type ""Dropdown"" and Use To Create Configurable Product ""Yes"" are available." "Only mapped fields","Only mapped fields" "Optional","Optional" @@ -625,6 +636,7 @@ "Password must include both numeric and alphabetic characters.","Password must include both numeric and alphabetic characters." "Password:","Password:" "Path:","Path:" +"PayPal Manager","PayPal Manager" "Payment method instance is not available.","Payment method instance is not available." "Payment method is not available.","Payment method is not available." "Payment method must be specified.","Payment method must be specified." @@ -658,11 +670,12 @@ "Please enter a valid zip code.","Please enter a valid zip code." "Please enter a valid zip code. For example 90602 or 90602-1234.","Please enter a valid zip code. For example 90602 or 90602-1234." "Please enter another credit card number to complete your purchase.","Please enter another credit card number to complete your purchase." +"Please enter password","Please enter password" +"Please enter password to confirm rollback.","Please enter password to confirm rollback." "Please enter valid password.","Please enter valid password." "Please make sure that all global admin search modules are installed and activated.","Please make sure that all global admin search modules are installed and activated." "Please make sure that your changes were saved before running the profile.","Please make sure that your changes were saved before running the profile." "Please make sure your passwords match.","Please make sure your passwords match." -"Please navigate to Hosted Checkout Pages - Customize menu and select Layout C.","Please navigate to Hosted Checkout Pages - Customize menu and select Layout C." "Please select State/Province.","Please select State/Province." "Please select a customer.","Please select a customer." "Please select a store.","Please select a store." @@ -674,9 +687,9 @@ "Please select one of the options.","Please select one of the options." "Please select review(s).","Please select review(s)." "Please select tag(s).","Please select tag(s)." -"Please specify at least start or end date.","Please specify at least start or end date." "Please specify the admin custom URL.","Please specify the admin custom URL." "Please try to logout and sign in again.","Please try to logout and sign in again." +"Please type the letters from the image:","Please type the letters from the image:" "Please use in this field only ""a-z,0-9,_"".","Please use in this field only ""a-z,0-9,_""." "Please use letters only (a-z) in this field.","Please use letters only (a-z) in this field." "Please use numbers only in this field. Please avoid spaces or other characters such as dots or commas.","Please use numbers only in this field. Please avoid spaces or other characters such as dots or commas." @@ -697,10 +710,12 @@ "Prev. year (hold for menu)","Prev. year (hold for menu)" "Preview","Preview" "Preview Template","Preview Template" +"Price","Price" "Price alert subscription was saved.","Price alert subscription was saved." "Price:","Price:" "Processed %s%% %s/%d records","Processed %s%% %s/%d records" "Product","Product" +"Product Name","Product Name" "Product Reviews","Product Reviews" "Product Tax Classes","Product Tax Classes" "Product Thumbnail Itself","Product Thumbnail Itself" @@ -708,6 +723,7 @@ "Product:","Product:" "Products","Products" "Products Bestsellers Report","Products Bestsellers Report" +"Products Most Viewed Report","Products Most Viewed Report" "Products Ordered","Products Ordered" "Products in Carts","Products in Carts" "Profile Action","Profile Action" @@ -723,6 +739,8 @@ "Promo","Promo" "Promotions","Promotions" "Purchased Item","Purchased Item" +"Put store on the maintenance mode while backup creation","Put store on the maintenance mode while backup creation" +"Put store on the maintenance mode while rollback processing","Put store on the maintenance mode while rollback processing" "Quantity","Quantity" "Queue Refresh","Queue Refresh" "Queued... Cancel","Queued... Cancel" @@ -748,12 +766,12 @@ "Release","Release" "Release Stability","Release Stability" "Release Version","Release Version" +"Reload captcha","Reload captcha" "Remote FTP","Remote FTP" "Remove","Remove" "Reports","Reports" "Request Path","Request Path" "Required","Required" -"Required settings","Required settings" "Reset","Reset" "Reset Filter","Reset Filter" "Reset Password","Reset Password" @@ -764,7 +782,6 @@ "Results","Results" "Retrieve Password","Retrieve Password" "Return Html Version","Return Html Version" -"Return URL: ","Return URL: " "Revenue","Revenue" "Reviews","Reviews" "Reviews and Ratings","Reviews and Ratings" @@ -806,12 +823,9 @@ "Search Term","Search Term" "Search Terms","Search Terms" "Select","Select" -"Select $0 Auth if your credit card processor supports $0 Auth capability and Reference Transactions, or if you are unsure what to select. This setting provides the best experience for shoppers.","Select $0 Auth if your credit card processor supports $0 Auth capability and Reference Transactions, or if you are unsure what to select. This setting provides the best experience for shoppers." -"Select $1 Auth if your credit card processor does not support $0 Auth, but does support Reference Transactions. This will provide a very good shopper experience, but might require you to pay a small additional authorization fee from your merchant account provider for any cart abandoned after payment details are entered. If you select $0 Auth, but your credit card processor does not support $0 Auth, your transaction will run as a $1 Auth instead.","Select $1 Auth if your credit card processor does not support $0 Auth, but does support Reference Transactions. This will provide a very good shopper experience, but might require you to pay a small additional authorization fee from your merchant account provider for any cart abandoned after payment details are entered. If you select $0 Auth, but your credit card processor does not support $0 Auth, your transaction will run as a $1 Auth instead." "Select All","Select All" "Select Category","Select Category" "Select Date","Select Date" -"Select Full Auth if you want to minimize your credit card processing fees, or if your credit card processor does not permit reference transactions. Please note that in some cases, shoppers who abandon your cart late in the process may find that there is a payment authorization outstanding from your company, which will go away on its own in a few days/weeks. This authorization can be reversal by voiding it, however, there is no guarantee the card-issuing bank will accept this request.","Select Full Auth if you want to minimize your credit card processing fees, or if your credit card processor does not permit reference transactions. Please note that in some cases, shoppers who abandon your cart late in the process may find that there is a payment authorization outstanding from your company, which will go away on its own in a few days/weeks. This authorization can be reversal by voiding it, however, there is no guarantee the card-issuing bank will accept this request." "Select Range","Select Range" "Select date","Select date" "Selected allowed currency ""%s"" is not available in installed currencies.","Selected allowed currency ""%s"" is not available in installed currencies." @@ -821,6 +835,8 @@ "Self-assigned roles cannot be deleted.","Self-assigned roles cannot be deleted." "Sender","Sender" "Separate Email","Separate Email" +"Service Settings","Service Settings" +"Set up & Customize","Set up & Customize" "Shipment #%s comment added","Shipment #%s comment added" "Shipment #%s created","Shipment #%s created" "Shipment Comments","Shipment Comments" @@ -840,8 +856,6 @@ "Show By","Show By" "Show Report For","Show Report For" "Show Reviews","Show Reviews" -"Show confirmation page: ","Show confirmation page: " -"Silent Post URL:","Silent Post URL:" "Sitemap Information","Sitemap Information" "Size for %s","Size for %s" "Skip Category Selection","Skip Category Selection" @@ -914,7 +928,7 @@ "The account has been saved.","The account has been saved." "The archive can be uncompressed with %s on Windows systems","The archive can be uncompressed with %s on Windows systems" "The attribute set has been removed.","The attribute set has been removed." -"The backup has been created.","The backup has been created." +"The backup's creation process will take time.","The backup's creation process will take time." "The billing agreement has been canceled.","The billing agreement has been canceled." "The billing agreement has been deleted.","The billing agreement has been deleted." "The cache storage has been flushed.","The cache storage has been flushed." @@ -945,6 +959,7 @@ "The invoice has been created.","The invoice has been created." "The invoice has been voided.","The invoice has been voided." "The invoice no longer exists.","The invoice no longer exists." +"The item %s (SKU %s) does not exist in the catalog anymore.","The item %s (SKU %s) does not exist in the catalog anymore." "The order does not allow creating an invoice.","The order does not allow creating an invoice." "The order no longer exists.","The order no longer exists." "The poll has been deleted.","The poll has been deleted." @@ -961,10 +976,11 @@ "The role has been successfully saved.","The role has been successfully saved." "The search index has been rebuilt.","The search index has been rebuilt." "The shipment has been created.","The shipment has been created." -"The shipment has been created. The shipping label has been created.","The shipment has been created. The shipping label has been created." "The shipment has been sent.","The shipment has been sent." +"The shipping label has been created.","The shipping label has been created." "The tag has been deleted.","The tag has been deleted." "The tag has been saved.","The tag has been saved." +"The transaction details have been updated.","The transaction details have been updated." "The user has been deleted.","The user has been deleted." "The user has been saved.","The user has been saved." "Themes JavaScript and CSS files combined to one file.","Themes JavaScript and CSS files combined to one file." @@ -975,6 +991,7 @@ "This Role no longer exists.","This Role no longer exists." "This account is","This account is" "This account is inactive.","This account is inactive." +"This action cannot be undone.","This action cannot be undone." "This attribute set does not have attributes which we can use for configurable product","This attribute set does not have attributes which we can use for configurable product" "This attribute shares the same value in all the stores","This attribute shares the same value in all the stores" "This is a demo store. Any orders placed through this store will not be honored or fulfilled.","This is a demo store. Any orders placed through this store will not be honored or fulfilled." @@ -989,7 +1006,8 @@ "Timeout limit for response from synchronize process was reached.","Timeout limit for response from synchronize process was reached." "To","To" "To cancel pending authorizations and release amounts that have already been processed during this payment, click Cancel.","To cancel pending authorizations and release amounts that have already been processed during this payment, click Cancel." -"To use Payflow Link, you must configure your Payflow Link account on the Paypal website.","To use Payflow Link, you must configure your Payflow Link account on the Paypal website." +"To use PayPal Payflow Link you must configure some settings in your Payflow account by logging into","To use PayPal Payflow Link you must configure some settings in your Payflow account by logging into" +"To use PayPal Payments Advanced you must configure some settings in your PayPal Payments Advanced account by logging into","To use PayPal Payments Advanced you must configure some settings in your PayPal Payments Advanced account by logging into" "Toggle Editor","Toggle Editor" "Tools","Tools" "Top 5 Search Terms","Top 5 Search Terms" @@ -1000,7 +1018,10 @@ "Total of %d record(s) have been deleted.","Total of %d record(s) have been deleted." "Total of %d record(s) have been updated.","Total of %d record(s) have been updated." "Total of %d record(s) were canceled.","Total of %d record(s) were canceled." +"Total of %d record(s) were deleted","Total of %d record(s) were deleted" "Total of %d record(s) were deleted.","Total of %d record(s) were deleted." +"Total of %d record(s) were updated","Total of %d record(s) were updated" +"Total of %d record(s) were updated.","Total of %d record(s) were updated." "Track Order","Track Order" "Track this shipment","Track this shipment" "Tracking number %s for %s assigned","Tracking number %s for %s assigned" @@ -1029,6 +1050,7 @@ "Unable to save the invoice.","Unable to save the invoice." "Unable to send the invoice email.","Unable to send the invoice email." "Unable to send the shipment email.","Unable to send the shipment email." +"Unable to update transaction details.","Unable to update transaction details." "Unable to void the credit memo.","Unable to void the credit memo." "Unknown","Unknown" "Unlimited","Unlimited" @@ -1045,7 +1067,7 @@ "Use Default","Use Default" "Use Default Value","Use Default Value" "Use Default Variable Values","Use Default Variable Values" -"Use Silent Post:","Use Silent Post:" +"Use FTP Connection","Use FTP Connection" "Use Website","Use Website" "Used Currently For","Used Currently For" "Used as Default For","Used as Default For" @@ -1056,11 +1078,14 @@ "User Name","User Name" "User Name is required field.","User Name is required field." "User Name:","User Name:" +"User Password","User Password" "User Role","User Role" "User Roles","User Roles" "User Roles Information","User Roles Information" "User name","User name" "Users","Users" +"VAT Number is Invalid","VAT Number is Invalid" +"VAT Number is Valid","VAT Number is Valid" "Validation Results","Validation Results" "Value","Value" "Value Delimiter:","Value Delimiter:" @@ -1077,20 +1102,19 @@ "View Shipment","View Shipment" "View Statistics For","View Statistics For" "Visibility:","Visibility:" +"Warning","Warning" "Warning! Empty value can cause problems with CSV format.","Warning! Empty value can cause problems with CSV format." "Warning!\r\nThis action will remove this user from already assigned role\r\nAre you sure?","Warning!\r\nThis action will remove this user from already assigned role\r\nAre you sure?" "Warning!\r\nThis action will remove those users from already assigned roles\r\nAre you sure?","Warning!\r\nThis action will remove those users from already assigned roles\r\nAre you sure?" "Warning: Please do not close the window during importing/exporting data","Warning: Please do not close the window during importing/exporting data" "Watermark File for %s","Watermark File for %s" "We appreciate our merchants\' feedback, please take our survey to provide insight on the features you would like included in Magento. Remove this notification","We appreciate our merchants\' feedback, please take our survey to provide insight on the features you would like included in Magento. Remove this notification" -"We detected that your JavaScript seem to be disabled.","We detected that your JavaScript seem to be disabled." "We\'re in our typing table, coding away more features for Magento. Thank you for your patience.","We\'re in our typing table, coding away more features for Magento. Thank you for your patience." "Web Section","Web Section" "Web Services","Web Services" "Web services","Web services" "Website","Website" "What is this?","What is this?" -"When using Payflow Link in Magento, a payment authorization transaction must be performed after the shopper enters their credit card information on the Payment page of checkout. If a full authorization is performed for the entire dollar amount of the transaction, then in some cases, the transaction amount might be reserved on the shopper\'s credit card for up to 30 days, even if they abandon their cart. This is not an ideal customer experience. Using this advanced setting in Magento, you can configure key details of this authorization.","When using Payflow Link in Magento, a payment authorization transaction must be performed after the shopper enters their credit card information on the Payment page of checkout. If a full authorization is performed for the entire dollar amount of the transaction, then in some cases, the transaction amount might be reserved on the shopper\'s credit card for up to 30 days, even if they abandon their cart. This is not an ideal customer experience. Using this advanced setting in Magento, you can configure key details of this authorization." "Wishlist Report","Wishlist Report" "Wishlist item is not loaded.","Wishlist item is not loaded." "Wrong account specified.","Wrong account specified." @@ -1112,10 +1136,13 @@ "Yes (only price with tax)","Yes (only price with tax)" "You cannot delete your own account.","You cannot delete your own account." "You have %s unread message(s).","You have %s unread message(s)." +"You have %s unread message(s). Go to messages inbox.","You have %s unread message(s). Go to messages inbox." +"You have %s, %s and %s unread messages. Go to messages inbox.","You have %s, %s and %s unread messages. Go to messages inbox." "You have logged out.","You have logged out." "You have not enough permissions to use this functionality.","You have not enough permissions to use this functionality." "You must have JavaScript enabled in your browser to utilize the functionality of this website.","You must have JavaScript enabled in your browser to utilize the functionality of this website." "You need to specify order items.","You need to specify order items." +"You will need to wait when the action ends.","You will need to wait when the action ends." "Your answers contain duplicates.","Your answers contain duplicates." "Your password has been updated.","Your password has been updated." "Your password reset link has expired.","Your password reset link has expired." @@ -1132,11 +1159,13 @@ "critical","critical" "example: ""sitemap/"" or ""/"" for base path (path must be writeable)","example: ""sitemap/"" or ""/"" for base path (path must be writeable)" "example: sitemap.xml","example: sitemap.xml" +"failed","failed" "from","from" "major","major" "minor","minor" "notice","notice" "store(%s) scope","store(%s) scope" +"successful","successful" "to","to" "website(%s) scope","website(%s) scope" "{{base_url}} is not recommended to use in a production environment to declare the Base Unsecure URL / Base Secure URL. It is highly recommended to change this value in your Magento configuration.","{{base_url}} is not recommended to use in a production environment to declare the Base Unsecure URL / Base Secure URL. It is highly recommended to change this value in your Magento configuration." diff --git a/app/locale/en_US/Mage_Backup.csv b/app/locale/en_US/Mage_Backup.csv index 6248e8ae5c..ce622aa51f 100644 --- a/app/locale/en_US/Mage_Backup.csv +++ b/app/locale/en_US/Mage_Backup.csv @@ -1,19 +1,49 @@ "Action","Action" +"An error occurred while creating the backup.","An error occurred while creating the backup." "An error occurred while writing to the backup file ""%s"".","An error occurred while writing to the backup file ""%s""." +"Are you sure you want to delete the selected backup(s)?","Are you sure you want to delete the selected backup(s)?" +"Backup Type","Backup Type" "Backup file ""%s"" cannot be read from or written to.","Backup file ""%s"" cannot be read from or written to." "Backup file ""%s"" does not exist.","Backup file ""%s"" does not exist." "Backup file does not exist.","Backup file does not exist." "Backup file handler was unspecified.","Backup file handler was unspecified." +"Backup file not found","Backup file not found" "Backup file path was not specified.","Backup file path was not specified." "Backups","Backups" "Cannot read backup file.","Cannot read backup file." "Create Backup","Create Backup" -"DB","DB" +"Database Backup","Database Backup" +"Database and Media Backup","Database and Media Backup" "Database was successfuly backed up.","Database was successfuly backed up." "Download","Download" +"Enable Scheduled Backup","Enable Scheduled Backup" +"Failed to connect to FTP","Failed to connect to FTP" +"Failed to delete one or several backups.","Failed to delete one or several backups." +"Failed to rollback","Failed to rollback" +"Failed to validate FTP","Failed to validate FTP" +"Frequency","Frequency" +"Invalid Password.","Invalid Password." +"Maintenance Mode","Maintenance Mode" +"Not enough free space to create backup.","Not enough free space to create backup." +"Not enough permissions to create backup.","Not enough permissions to create backup." +"Not enough permissions to perform rollback","Not enough permissions to perform rollback" +"Please deselect the sufficient check-box, if you want to continue backup's creation","Please deselect the sufficient check-box, if you want to continue backup's creation" +"Please deselect the sufficient check-box, if you want to continue rollback processing","Please deselect the sufficient check-box, if you want to continue rollback processing" +"Put store on the maintenance mode while backup's creation","Put store on the maintenance mode while backup's creation" +"Rollback","Rollback" +"Scheduled Backup Settings","Scheduled Backup Settings" "Size, Bytes","Size, Bytes" +"Start Time","Start Time" +"System Backup","System Backup" +"System couldn't put store on the maintenance mode","System couldn't put store on the maintenance mode" +"The database and media backup has been created.","The database and media backup has been created." +"The database backup has been created.","The database backup has been created." "The file was compressed with Zlib, but this extension is not installed on server.","The file was compressed with Zlib, but this extension is not installed on server." +"The selected backup(s) has been deleted.","The selected backup(s) has been deleted." +"The system backup has been created.","The system backup has been created." "Time","Time" "Type","Type" "Unable to create backup. Please, try again later.","Unable to create backup. Please, try again later." +"Unable to save the cron expression.","Unable to save the cron expression." +"Warning! System couldn't put store on the maintenance mode.","Warning! System couldn't put store on the maintenance mode." "Wrong order of creation for new backup.","Wrong order of creation for new backup." diff --git a/app/locale/en_US/Mage_Captcha.csv b/app/locale/en_US/Mage_Captcha.csv new file mode 100644 index 0000000000..e5a7814ab6 --- /dev/null +++ b/app/locale/en_US/Mage_Captcha.csv @@ -0,0 +1,22 @@ +"Attention: Captcha is case sensitive.","Attention: Captcha is case sensitive." +"After number of attempts to login","After number of attempts to login" +"Always","Always" +"CAPTCHA","CAPTCHA" +"CAPTCHA Timeout (minutes)","CAPTCHA Timeout (minutes)" +"CAPTCHA for ""Create user"" and ""Forgot password"" forms is always enabled if chosen","CAPTCHA for ""Create user"" and ""Forgot password"" forms is always enabled if chosen" +"Case Sensitive","Case Sensitive" +"Displaying Mode","Displaying Mode" +"Enable CAPTCHA in Admin","Enable CAPTCHA in Admin" +"Enable CAPTCHA on Frontend","Enable CAPTCHA on Frontend" +"Font","Font" +"Forms","Forms" +"If 0 is specified, CAPTCHA on the Login form will be always available.","If 0 is specified, CAPTCHA on the Login form will be always available." +"Incorrect CAPTCHA.","Incorrect CAPTCHA." +"Number of Symbols","Number of Symbols" +"Number of Unsuccessful Attempts to Login","Number of Unsuccessful Attempts to Login" +"Please specify 8 symbols at the most. Range allowed (e.g. 3-5)","Please specify 8 symbols at the most. Range allowed (e.g. 3-5)" +"Please type the letters below","Please type the letters below" +"Please type the letters from the image:","Please type the letters from the image:" +"Please use only letters (a-z or A-Z) or numbers (0-9) in this field. No spaces or other characters are allowed.
    Similar looking characters (e.g. ""i"", ""l"", ""1"") decrease chance of correct recognition by customer.","Please use only letters (a-z or A-Z) or numbers (0-9) in this field. No spaces or other characters are allowed.
    Similar looking characters (e.g. ""i"", ""l"", ""1"") decrease chance of correct recognition by customer." +"Reload captcha","Reload captcha" +"Symbols Used in CAPTCHA","Symbols Used in CAPTCHA" diff --git a/app/locale/en_US/Mage_Catalog.csv b/app/locale/en_US/Mage_Catalog.csv index 7c10041ff9..698def10c6 100644 --- a/app/locale/en_US/Mage_Catalog.csv +++ b/app/locale/en_US/Mage_Catalog.csv @@ -2,9 +2,9 @@ "%1$s incl tax.","%1$s incl tax." "%s - %s","%s - %s" "%s Item(s)","%s Item(s)" +"%s and above","%s and above" "(%d)","(%d)" "(Copy data from: %s)","(Copy data from: %s)" -"(Will make search for the query above return results for this search.)","(Will make search for the query above return results for this search.)" "* Required Fields","* Required Fields" "-- Please Select --","-- Please Select --" "86400 by default, if not set. To refresh instantly, Clear the Blocks HTML Output Cache.","86400 by default, if not set. To refresh instantly, Clear the Blocks HTML Output Cache." @@ -20,6 +20,7 @@ "Add Attribute","Add Attribute" "Add Design Change","Add Design Change" "Add Group","Add Group" +"Add Group Price","Add Group Price" "Add New","Add New" "Add New Attribute","Add New Attribute" "Add New Attribute Set","Add New Attribute Set" @@ -60,6 +61,7 @@ "An error occurred while removing products from websites.","An error occurred while removing products from websites." "An error occurred while saving the URL rewrite","An error occurred while saving the URL rewrite" "An error occurred while saving the attribute set.","An error occurred while saving the attribute set." +"An error occurred while saving the collection, aborting. Error message: %s","An error occurred while saving the collection, aborting. Error message: %s" "An error occurred while saving the product. ","An error occurred while saving the product. " "An error occurred while saving the search query.","An error occurred while saving the search query." "An error occurred while saving this group.","An error occurred while saving this group." @@ -95,6 +97,7 @@ "Attribute Set","Attribute Set" "Attribute Sets","Attribute Sets" "Attribute add","Attribute add" +"Attribute code is invalid. Please use only letters (a-z), numbers (0-9) or underscore(_) in this field, first character should be a letter.","Attribute code is invalid. Please use only letters (a-z), numbers (0-9) or underscore(_) in this field, first character should be a letter." "Attribute group with the \"/name/\" name already exists","Attribute group with the \"/name/\" name already exists" "Attribute remove","Attribute remove" "Attribute with the same code already exists","Attribute with the same code already exists" @@ -233,10 +236,12 @@ "Delete Attribute","Delete Attribute" "Delete Attribute Set","Delete Attribute Set" "Delete Category","Delete Category" +"Delete Group Price","Delete Group Price" "Delete Option","Delete Option" "Delete Row","Delete Row" "Delete Search","Delete Search" "Delete Selected Group","Delete Selected Group" +"Delete Tier","Delete Tier" "Delete attribute","Delete attribute" "Delete category","Delete category" "Delete product","Delete product" @@ -251,6 +256,7 @@ "Double click on above image to view full picture","Double click on above image to view full picture" "Downloadable Information","Downloadable Information" "Duplicate","Duplicate" +"Duplicate website group price customer group.","Duplicate website group price customer group." "Duplicate website tier price customer group and quantity.","Duplicate website tier price customer group and quantity." "Duplicate website, country and state tax found.","Duplicate website, country and state tax found." "E.g. {{media url=""path/to/image.jpg""}} {{skin url=""path/to/picture.gif""}}. Dynamic directives parsing impacts catalog performance.","E.g. {{media url=""path/to/image.jpg""}} {{skin url=""path/to/picture.gif""}}. Dynamic directives parsing impacts catalog performance." @@ -285,6 +291,7 @@ "First Name","First Name" "Fixed","Fixed" "For internal use.","For internal use." +"For the last time placed.","For the last time placed." "Frontend","Frontend" "Frontend Model","Frontend Model" "Frontend Properties","Frontend Properties" @@ -377,10 +384,13 @@ "Max Characters","Max Characters" "Maximal Depth","Maximal Depth" "Maximum Image Size","Maximum Image Size" +"Maximum Number of Price Intervals","Maximum Number of Price Intervals" "Maximum Qty Allowed in Shopping Cart","Maximum Qty Allowed in Shopping Cart" +"Maximum allowed image size for '%s' is %sx%s px.","Maximum allowed image size for '%s' is %sx%s px." "Maximum image height","Maximum image height" "Maximum image width","Maximum image width" "Maximum number of characters:","Maximum number of characters:" +"Maximum number of price intervals is 100","Maximum number of price intervals is 100" "Media Image","Media Image" "Minimum Advertised Price","Minimum Advertised Price" "Minimum Lines per Page","Minimum Lines per Page" @@ -415,7 +425,8 @@ "Notify for Quantity Below","Notify for Quantity Below" "Number of Products to Display","Number of Products to Display" "Number of Uses","Number of Uses" -"Number of results
    (For the last time placed)","Number of results
    (For the last time placed)" +"Number of results","Number of results" +"Number of results (For the last time placed)","Number of results (For the last time placed)" "OK","OK" "OR","OR" "Old Price:","Old Price:" @@ -613,8 +624,12 @@ "Simple Product","Simple Product" "Site Map","Site Map" "Sitemap","Sitemap" +"Skip import row, is not valid value ""%s"" for field ""%s""","Skip import row, is not valid value ""%s"" for field ""%s""" "Skip import row, required field ""%s"" for the new customer is not defined.","Skip import row, required field ""%s"" for the new customer is not defined." +"Skip import row, the value ""%s"" is invalid for field ""%s""","Skip import row, the value ""%s"" is invalid for field ""%s""" +"Skipping import row, required field ""%s"" for new products is not defined.","Skipping import row, required field ""%s"" for new products is not defined." "Skipping import row, required field ""%s"" is not defined.","Skipping import row, required field ""%s"" is not defined." +"Skipping import row, store ""%s"" field does not exist.","Skipping import row, store ""%s"" field does not exist." "Skipping import row, the value ""%s"" is not valid for the ""%s"" field.","Skipping import row, the value ""%s"" is not valid for the ""%s"" field." "Some of the processed products have no SKU value defined. Please fill it prior to performing operations on these products.","Some of the processed products have no SKU value defined. Please fill it prior to performing operations on these products." "Some of the products below do not have all the required options. Please edit them and configure all the required options.","Some of the products below do not have all the required options. Please edit them and configure all the required options." @@ -648,6 +663,8 @@ "The category has been deleted.","The category has been deleted." "The category has been saved.","The category has been saved." "The comparison list was cleared.","The comparison list was cleared." +"The file '%s' for '%s' has an invalid extension","The file '%s' for '%s' has an invalid extension" +"The file '%s' you uploaded is larger than %s Megabytes allowed by server","The file '%s' you uploaded is larger than %s Megabytes allowed by server" "The file you uploaded is larger than %s Megabytes allowed by server","The file you uploaded is larger than %s Megabytes allowed by server" "The filters must be an array.","The filters must be an array." "The image contents is not valid base64 data.","The image contents is not valid base64 data." @@ -698,6 +715,7 @@ "Total incl. Tax: %1$s","Total incl. Tax: %1$s" "Total of %d record(s) have been deleted.","Total of %d record(s) have been deleted." "Total of %d record(s) have been updated.","Total of %d record(s) have been updated." +"Total of %d record(s) were updated","Total of %d record(s) were updated" "Type","Type" "URL Rewrite Management","URL Rewrite Management" "Unable to complete this request.","Unable to complete this request." @@ -766,6 +784,7 @@ "Websites","Websites" "What's this?","What's this?" "Whether to show ""All"" option in the ""Show X Per Page"" dropdown.","Whether to show ""All"" option in the ""Show X Per Page"" dropdown." +"Will make search for the query above return results for this search.","Will make search for the query above return results for this search." "Wrong BuyRequest instance in options group.","Wrong BuyRequest instance in options group." "Wrong configuration item instance in options group.","Wrong configuration item instance in options group." "Wrong configuration item option instance in options group.","Wrong configuration item option instance in options group." @@ -781,8 +800,8 @@ "You may also be interested in the following product(s)","You may also be interested in the following product(s)" "Zoom In","Zoom In" "Zoom Out","Zoom Out" -"[GLOBAL]","[GLOBAL]" "and","and" +"and above","and above" "categories","categories" "each","each" "ex. http://domain.com","ex. http://domain.com" diff --git a/app/locale/en_US/Mage_CatalogInventory.csv b/app/locale/en_US/Mage_CatalogInventory.csv index b5e39e4dc4..9f785b5578 100644 --- a/app/locale/en_US/Mage_CatalogInventory.csv +++ b/app/locale/en_US/Mage_CatalogInventory.csv @@ -1,3 +1,4 @@ +"""%s"" is not available in the requested quantity. %s of the items will be backordered.","""%s"" is not available in the requested quantity. %s of the items will be backordered." "%s is available for purchase in increments of %s only.","%s is available for purchase in increments of %s only." "%s is not a correct comparsion method.","%s is not a correct comparsion method." "Note that these settings are applicable to cart line items, not the whole cart.","Note that these settings are applicable to cart line items, not the whole cart." @@ -11,6 +12,7 @@ "Cannot specify product identifier for the order item.","Cannot specify product identifier for the order item." "Catalog Inventory","Catalog Inventory" "Decrease Stock When Order is Placed","Decrease Stock When Order is Placed" +"Display Out of Stock Products","Display Out of Stock Products" "Enable Qty Increments","Enable Qty Increments" "In Stock","In Stock" "Index Product Stock Status","Index Product Stock Status" @@ -33,12 +35,19 @@ "Retrieve stock data by product ids","Retrieve stock data by product ids" "Rule price","Rule price" "Set Items' Status to be In Stock When Order is Cancelled","Set Items' Status to be In Stock When Order is Cancelled" +"Some of the products are currently out of stock","Some of the products are currently out of stock" +"Some of the products cannot be ordered in requested quantity.","Some of the products cannot be ordered in requested quantity." "Some of the products cannot be ordered in the requested quantity.","Some of the products cannot be ordered in the requested quantity." "Stock Options","Stock Options" "Stock Status","Stock Status" +"The maximum quantity allowed for purchase is %s.","The maximum quantity allowed for purchase is %s." +"The minimum quantity allowed for purchase is %s.","The minimum quantity allowed for purchase is %s." +"The requested quantity for ""%s"" is not available.","The requested quantity for ""%s"" is not available." "The stock item for Product in option is not valid.","The stock item for Product in option is not valid." "The stock item for Product is not valid.","The stock item for Product is not valid." "This product is available for purchase in increments of %s only.","This product is available for purchase in increments of %s only." +"This product is currently out of stock.","This product is currently out of stock." +"This product is not available in the requested quantity. %s of the items will be backordered.","This product is not available in the requested quantity. %s of the items will be backordered." "To Fixed Value","To Fixed Value" "To Percentage","To Percentage" "Undefined product type.","Undefined product type." diff --git a/app/locale/en_US/Mage_CatalogSearch.csv b/app/locale/en_US/Mage_CatalogSearch.csv index 705f59c1f6..9149c0114e 100644 --- a/app/locale/en_US/Mage_CatalogSearch.csv +++ b/app/locale/en_US/Mage_CatalogSearch.csv @@ -21,6 +21,7 @@ "Maximum Query Length","Maximum Query Length" "Maximum Query Words Count","Maximum Query Words Count" "Maximum Search query length is %s. Your query was cut.","Maximum Search query length is %s. Your query was cut." +"Maximum words count is %1$s. In your search query was cut next part: %2$s.","Maximum words count is %1$s. In your search query was cut next part: %2$s." "Minimal Query Length","Minimal Query Length" "Minimum Search query length is %s","Minimum Search query length is %s" "Modify your search","Modify your search" diff --git a/app/locale/en_US/Mage_Checkout.csv b/app/locale/en_US/Mage_Checkout.csv index 34f9742a36..6617d2a6ee 100644 --- a/app/locale/en_US/Mage_Checkout.csv +++ b/app/locale/en_US/Mage_Checkout.csv @@ -58,6 +58,7 @@ "Checkout as a Guest or Register","Checkout as a Guest or Register" "Checkout with Multiple Addresses","Checkout with Multiple Addresses" "City","City" +"Clear Shopping Cart","Clear Shopping Cart" "Click here to print a copy of your order confirmation.","Click here to print a copy of your order confirmation." "Click here to continue shopping.","Click here to continue shopping." "Close","Close" @@ -82,6 +83,7 @@ "Create Shipping Address","Create Shipping Address" "Create an order from shopping cart","Create an order from shopping cart" "Create shopping cart","Create shopping cart" +"Customer Address is not valid.","Customer Address is not valid." "Customer Information","Customer Information" "Customer's information","Customer's information" "Data saving problem","Data saving problem" @@ -100,6 +102,7 @@ "Edit","Edit" "Edit Address","Edit Address" "Edit Billing Address","Edit Billing Address" +"Edit Condition","Edit Condition" "Edit Items","Edit Items" "Edit Shipping Address","Edit Shipping Address" "Edit Terms and Conditions","Edit Terms and Conditions" @@ -131,7 +134,11 @@ "HTML","HTML" "ID","ID" "Incl. Tax","Incl. Tax" +"Invalid checkout type.","Invalid checkout type." +"Invalid data.","Invalid data." +"Invalid email address ""%s""","Invalid email address ""%s""" "Invalid method: %s","Invalid method: %s" +"Invalid shipping method.","Invalid shipping method." "Item not found or already ordered","Item not found or already ordered" "Items","Items" "Items After","Items After" @@ -140,7 +147,7 @@ "Loading next step...","Loading next step..." "Login","Login" "Login/Registration Before","Login/Registration Before" -"Manage Checkout Terms and Conditions","Manage Checkout Terms and Conditions" +"Manage Terms and Conditions","Manage Terms and Conditions" "Maximum Display Recently Added Item(s)","Maximum Display Recently Added Item(s)" "Maximum qty allowed for Shipping to multiple addresses is %s","Maximum qty allowed for Shipping to multiple addresses is %s" "Move to Wishlist","Move to Wishlist" @@ -163,6 +170,7 @@ "My Cart Link","My Cart Link" "New Address","New Address" "New Condition","New Condition" +"New Terms and Conditions","New Terms and Conditions" "ORDER TOTAL WILL BE DISPLAYED BEFORE YOU SUBMIT THE ORDER","ORDER TOTAL WILL BE DISPLAYED BEFORE YOU SUBMIT THE ORDER" "One Page Checkout","One Page Checkout" "One Page Checkout Failure","One Page Checkout Failure" @@ -193,10 +201,17 @@ "Please agree to all Terms and Conditions before placing the order.","Please agree to all Terms and Conditions before placing the order." "Please agree to all Terms and Conditions before placing the orders.","Please agree to all Terms and Conditions before placing the orders." "Please agree to all the terms and conditions before placing the order.","Please agree to all the terms and conditions before placing the order." +"Please check billing address information.","Please check billing address information." +"Please check shipping address information.","Please check shipping address information." +"Please check shipping addresses information.","Please check shipping addresses information." "Please log in below:","Please log in below:" "Please select region, state or province","Please select region, state or province" "Please select shipping address for applicable items","Please select shipping address for applicable items" "Please select shipping methods for all addresses","Please select shipping methods for all addresses" +"Please select valid payment method.","Please select valid payment method." +"Please specify payment method.","Please specify payment method." +"Please specify shipping method.","Please specify shipping method." +"Please specify shipping methods for all addresses.","Please specify shipping methods for all addresses." "Price","Price" "Proceed to Checkout","Proceed to Checkout" "Product","Product" @@ -219,6 +234,7 @@ "Remove item","Remove item" "Remove product from shopping cart","Remove product from shopping cart" "Remove product(s) from shopping cart","Remove product(s) from shopping cart" +"Require Customer To Be Logged In To Checkout","Require Customer To Be Logged In To Checkout" "Retrieve information about shopping cart","Retrieve information about shopping cart" "Review Order","Review Order" "Review Order - %s","Review Order - %s" @@ -262,6 +278,8 @@ "Some of the requested products are not available in the desired quantity.","Some of the requested products are not available in the desired quantity." "Some of the requested products are unavailable.","Some of the requested products are unavailable." "Some products quantities were recalculated because of quantity increment mismatch","Some products quantities were recalculated because of quantity increment mismatch" +"Sorry, guest checkout is not enabled. Please try again or contact store owner.","Sorry, guest checkout is not enabled. Please try again or contact store owner." +"Sorry, guest checkout is not enabled. Please try again or contact the store owner.","Sorry, guest checkout is not enabled. Please try again or contact the store owner." "Sorry, no quotes are available for this order at this time.","Sorry, no quotes are available for this order at this time." "State/Province","State/Province" "Status","Status" @@ -283,6 +301,7 @@ "The product does not exist.","The product does not exist." "There are %s items in your cart.","There are %s items in your cart." "There is 1 item in your cart.","There is 1 item in your cart." +"There is already a customer registered using this email address. Please login using this email address or enter a different email address to register your account.","There is already a customer registered using this email address. Please login using this email address or enter a different email address to register your account." "There was an error processing your order. Please contact us or try again later.","There was an error processing your order. Please contact us or try again later." "This condition no longer exists.","This condition no longer exists." "Total","Total" @@ -296,6 +315,7 @@ "Update product quantities in shopping cart","Update product quantities in shopping cart" "Update product(s) quantities in shopping cart","Update product(s) quantities in shopping cart" "Use Billing Address","Use Billing Address" +"VAT Number","VAT Number" "We are processing your order and you will soon receive an email with details of the order. Once the order has shipped you will receive another email with a link to track its progress.","We are processing your order and you will soon receive an email with details of the order. Once the order has shipped you will receive another email with a link to track its progress." "What's this?","What's this?" "You have no items in your shopping cart.","You have no items in your shopping cart." diff --git a/app/locale/en_US/Mage_Core.csv b/app/locale/en_US/Mage_Core.csv index 9e8b14263d..fedcaa2752 100644 --- a/app/locale/en_US/Mage_Core.csv +++ b/app/locale/en_US/Mage_Core.csv @@ -35,8 +35,10 @@ "Base JavaScript URL","Base JavaScript URL" "Base Link URL","Base Link URL" "Base Media URL","Base Media URL" +"Base Secure URL","Base Secure URL" "Base Skin URL","Base Skin URL" "Base URL","Base URL" +"Base Unsecure URL","Base Unsecure URL" "Before modifying the store view code please make sure that it is not used in index.php.","Before modifying the store view code please make sure that it is not used in index.php." "Before modifying the website code please make sure that it is not used in index.php.","Before modifying the website code please make sure that it is not used in index.php." "Block with name ""%s"" already exists","Block with name ""%s"" already exists" @@ -58,6 +60,7 @@ "Copyright","Copyright" "Core","Core" "Countries Options","Countries Options" +"Country","Country" "Create Store","Create Store" "Create Store View","Create Store View" "Create Website","Create Website" @@ -69,6 +72,10 @@ "Custom Email 1","Custom Email 1" "Custom Email 2","Custom Email 2" "Custom Variables","Custom Variables" +"Custom1 Contact Email","Custom1 Contact Email" +"Custom1 Contact Name","Custom1 Contact Name" +"Custom2 Contact Email","Custom2 Contact Email" +"Custom2 Contact Name","Custom2 Contact Name" "Customer Support","Customer Support" "Dashboard","Dashboard" "Date From","Date From" @@ -112,12 +119,14 @@ "Environment Update Time","Environment Update Time" "Error in file: ""%s"" - %s","Error in file: ""%s"" - %s" "Error: Passwords do not match","Error: Passwords do not match" +"European Union Countries","European Union Countries" "Exceptions Log File Name","Exceptions Log File Name" "Favicon Icon","Favicon Icon" "File %s does not exist","File %s does not exist" "File %s is not readable","File %s is not readable" "File not found","File not found" "File system","File system" +"File with an extension ""%value%"" is protected and cannot be uploaded","File with an extension ""%value%"" is protected and cannot be uploaded" "First Day of Week","First Day of Week" "Flush Cache Storage","Flush Cache Storage" "Flush Magento Cache","Flush Magento Cache" @@ -126,6 +135,8 @@ "Forgot Password Email Template","Forgot Password Email Template" "General","General" "General Contact","General Contact" +"General Contact Email","General Contact Email" +"General Contact Name","General Contact Name" "General Settings","General Settings" "Global","Global" "HTML Head","HTML Head" @@ -170,6 +181,7 @@ "Miscellaneous Scripts","Miscellaneous Scripts" "Model class does not exist: %s.","Model class does not exist: %s." "Model collection resource name is not defined.","Model collection resource name is not defined." +"Module ""%1$s"" cannot depend on ""%2$s"".","Module ""%1$s"" cannot depend on ""%2$s""." "Module ""%1$s"" requires module ""%2$s"".","Module ""%1$s"" requires module ""%2$s""." "Name","Name" "New Design Change","New Design Change" @@ -178,6 +190,7 @@ "New Website","New Website" "No","No" "Offloader header","Offloader header" +"PHP SOAP extension is required.","PHP SOAP extension is required." "Package","Package" "Pagination","Pagination" "Pagination Frame","Pagination Frame" @@ -253,6 +266,8 @@ "Return-Path Email","Return-Path Email" "Root Category","Root Category" "Sales Representative","Sales Representative" +"Sales Representative Contact Email","Sales Representative Contact Email" +"Sales Representative Contact Name","Sales Representative Contact Name" "Save","Save" "Save Store","Save Store" "Save Store View","Save Store View" @@ -318,6 +333,7 @@ "This is a required field.","This is a required field." "This store cannot be deleted.","This store cannot be deleted." "This store view cannot be deleted.","This store view cannot be deleted." +"This value must be greater than 0.","This value must be greater than 0." "This website cannot be deleted.","This website cannot be deleted." "This will be displayed just before body closing tag.","This will be displayed just before body closing tag." "This will be included before head closing tag in page HTML.","This will be included before head closing tag in page HTML." @@ -348,6 +364,7 @@ "Use Secure URLs in Admin","Use Secure URLs in Admin" "Use Secure URLs in Frontend","Use Secure URLs in Frontend" "Use Web Server Rewrites","Use Web Server Rewrites" +"VAT Number","VAT Number" "Validate HTTP_USER_AGENT","Validate HTTP_USER_AGENT" "Validate HTTP_VIA","Validate HTTP_VIA" "Validate HTTP_X_FORWARDED_FOR","Validate HTTP_X_FORWARDED_FOR" diff --git a/app/locale/en_US/Mage_Customer.csv b/app/locale/en_US/Mage_Customer.csv index 728689aac0..93723adcd1 100644 --- a/app/locale/en_US/Mage_Customer.csv +++ b/app/locale/en_US/Mage_Customer.csv @@ -47,6 +47,7 @@ "Back - link to the previously viewed page","Back - link to the previously viewed page" "Back to Login","Back to Login" "Bad request.","Bad request." +"Based on the VAT ID, the customer would belong to Customer Group %s.","Based on the VAT ID, the customer would belong to Customer Group %s." "Bill to Name","Bill to Name" "Bought From","Bought From" "By creating an account with our store, you will be able to move through the checkout process faster, store multiple shipping addresses, view and track your orders in your account and more.","By creating an account with our store, you will be able to move through the checkout process faster, store multiple shipping addresses, view and track your orders in your account and more." @@ -125,6 +126,7 @@ "Delete customer address","Delete customer address" "Deleted Stores","Deleted Stores" "Details","Details" +"Disable Automatic Group Changes Based on VAT ID Default Value","Disable Automatic Group Changes Based on VAT ID Default Value" "Disabled","Disabled" "Edit","Edit" "Edit Account Info","Edit Account Info" @@ -140,6 +142,7 @@ "Email Address","Email Address" "Email Sender","Email Sender" "Email:","Email:" +"Enable Automatic Assignment to Customer Group","Enable Automatic Assignment to Customer Group" "Entity collection is expected.","Entity collection is expected." "Excel XML","Excel XML" "Failed to confirm customer account.","Failed to confirm customer account." @@ -153,6 +156,7 @@ "Form Fields Before","Form Fields Before" "From your My Account Dashboard you have the ability to view a snapshot of your recent account activity and update your account information. Select a link below to view or edit information.","From your My Account Dashboard you have the ability to view a snapshot of your recent account activity and update your account information. Select a link below to view or edit information." "Gender","Gender" +"Gender is required.","Gender is required." "General Subscription","General Subscription" "Generate Human-Friendly Customer ID","Generate Human-Friendly Customer ID" "Global","Global" @@ -160,21 +164,30 @@ "Group","Group" "Group Information","Group Information" "Group Name","Group Name" +"Group for Invalid VAT ID","Group for Invalid VAT ID" +"Group for Valid VAT ID - Domestic","Group for Valid VAT ID - Domestic" +"Group for Valid VAT ID - Intra-Union","Group for Valid VAT ID - Intra-Union" "Guest","Guest" "HTML","HTML" "Hello, %s!","Hello, %s!" "ID","ID" "IP Address","IP Address" "If there is an account associated with %s you will receive an email with a link to reset your password.","If there is an account associated with %s you will receive an email with a link to reset your password." +"If you are a registered VAT customer, please click here to enter you billing address to see proper VAT calculated","If you are a registered VAT customer, please click here to enter you billing address to see proper VAT calculated" +"If you believe this is an error, please contact us at %s","If you believe this is an error, please contact us at %s" "If you have an account with us, please log in.","If you have an account with us, please log in." "Invalid attribute option specified for attribute %s (%s), skipping the record.","Invalid attribute option specified for attribute %s (%s), skipping the record." "Invalid attribute set specified, skipping the record.","Invalid attribute set specified, skipping the record." +"Invalid billing address for (%s)","Invalid billing address for (%s)" "Invalid current password","Invalid current password" "Invalid customer data","Invalid customer data" +"Invalid email address ""%s"".","Invalid email address ""%s""." "Invalid email address.","Invalid email address." "Invalid login or password.","Invalid login or password." "Invalid password reset token.","Invalid password reset token." +"Invalid shipping address for (%s)","Invalid shipping address for (%s)" "Invalid store specified, skipping the record.","Invalid store specified, skipping the record." +"Invalid website, skipping the record, line: %s","Invalid website, skipping the record, line: %s" "Last Activity","Last Activity" "Last Date Subscribed","Last Date Subscribed" "Last Date Unsubscribed","Last Date Unsubscribed" @@ -193,9 +206,14 @@ "MM","MM" "Manage Addresses","Manage Addresses" "Manage Customers","Manage Customers" +"Maximum length must be less then %s symbols","Maximum length must be less then %s symbols" +"Missing email, skipping the record, line: %s","Missing email, skipping the record, line: %s" "Missing email, skipping the record.","Missing email, skipping the record." +"Missing first name, skipping the record, line: %s","Missing first name, skipping the record, line: %s" "Missing firstname, skipping the record.","Missing firstname, skipping the record." +"Missing last name, skipping the record, line: %s","Missing last name, skipping the record, line: %s" "Missing lastname, skipping the record.","Missing lastname, skipping the record." +"Missing website, skipping the record, line: %s","Missing website, skipping the record, line: %s" "Month","Month" "My Account","My Account" "My Account Wrapper","My Account Wrapper" @@ -250,10 +268,23 @@ "Pending","Pending" "Per Website","Per Website" "Personal Information","Personal Information" +"Please enter a valid date between %s and %s at %s.","Please enter a valid date between %s and %s at %s." +"Please enter a valid date equal to or greater than %s at %s.","Please enter a valid date equal to or greater than %s at %s." +"Please enter a valid date less than or equal to %s at %s.","Please enter a valid date less than or equal to %s at %s." +"Please enter the city.","Please enter the city." +"Please enter the country.","Please enter the country." +"Please enter the first name.","Please enter the first name." +"Please enter the last name.","Please enter the last name." +"Please enter the state/province.","Please enter the state/province." +"Please enter the street.","Please enter the street." +"Please enter the telephone number.","Please enter the telephone number." +"Please enter the zip/postal code.","Please enter the zip/postal code." "Please enter your email address below. You will receive a link to reset your password.","Please enter your email address below. You will receive a link to reset your password." "Please enter your email below and we\'ll send you confirmation link for it.","Please enter your email below and we\'ll send you confirmation link for it." "Please enter your email.","Please enter your email." +"Please make sure your passwords match.","Please make sure your passwords match." "Please select","Please select" +"Please select a website which contains store view","Please select a website which contains store view" "Please select region, state or province","Please select region, state or province" "Please, check your email for confirmation key.","Please, check your email for confirmation key." "Prefix Dropdown Options","Prefix Dropdown Options" @@ -312,6 +343,7 @@ "Shipped to Name","Shipped to Name" "Shopping Cart","Shopping Cart" "Shopping Cart - %d item(s)","Shopping Cart - %d item(s)" +"Shopping Cart of %1$s - %2$d item(s)","Shopping Cart of %1$s - %2$d item(s)" "Show Date of Birth","Show Date of Birth" "Show Gender","Show Gender" "Show Middle Name (initial)","Show Middle Name (initial)" @@ -340,20 +372,33 @@ "Text","Text" "Text One Line","Text One Line" "Thank you for registering with %s.","Thank you for registering with %s." +"The Date of Birth is required.","The Date of Birth is required." +"The TAX/VAT number is required.","The TAX/VAT number is required." +"The VAT ID entered (%s) is not a valid VAT ID.","The VAT ID entered (%s) is not a valid VAT ID." +"The VAT ID entered (%s) is not valid VAT ID.","The VAT ID entered (%s) is not valid VAT ID." +"The VAT ID is valid. The current Customer Group will be used.","The VAT ID is valid. The current Customer Group will be used." "The account information has been saved.","The account information has been saved." "The address does not belong to this customer.","The address does not belong to this customer." "The address has been deleted.","The address has been deleted." "The address has been saved.","The address has been saved." +"The customer ID and email did not match, skipping the record, line: %s","The customer ID and email did not match, skipping the record, line: %s" "The customer does not have default billing address.","The customer does not have default billing address." +"The customer email (%s) already exists, skipping the record, line: %s","The customer email (%s) already exists, skipping the record, line: %s" "The customer group has been deleted.","The customer group has been deleted." "The customer group has been saved.","The customer group has been saved." +"The customer is currently assigned to Customer Group %s.","The customer is currently assigned to Customer Group %s." +"The first name cannot be empty.","The first name cannot be empty." "The group ""%s"" cannot be deleted","The group ""%s"" cannot be deleted" +"The last name cannot be empty.","The last name cannot be empty." +"The minimum password length is %s","The minimum password length is %s" +"The password cannot be empty.","The password cannot be empty." "The password must have at least 6 characters. Leading or trailing spaces will be ignored.","The password must have at least 6 characters. Leading or trailing spaces will be ignored." "The suffix that goes after name (Jr., Sr., etc.)","The suffix that goes after name (Jr., Sr., etc.)" "The title that goes before name (Mr., Mrs., etc.)","The title that goes before name (Mr., Mrs., etc.)" "There are no items in customer's wishlist at the moment","There are no items in customer's wishlist at the moment" "There are no items in customer\'s shopping cart at the moment","There are no items in customer\'s shopping cart at the moment" "There is already an account with this email address. If you are sure that it is your email address, click here to get your password and access your account.","There is already an account with this email address. If you are sure that it is your email address, click here to get your password and access your account." +"There was an error validating the VAT ID. Please try again later.","There was an error validating the VAT ID. Please try again later." "This account is not confirmed.","This account is not confirmed." "This account is not confirmed. Click here to resend confirmation email.","This account is not confirmed. Click here to resend confirmation email." "This customer email already exists","This customer email already exists" @@ -361,6 +406,7 @@ "This email does not require confirmation.","This email does not require confirmation." "This email will be sent instead of default welcome email, after account confirmation.","This email will be sent instead of default welcome email, after account confirmation." "This is My Default %s Address","This is My Default %s Address" +"This value must be greater than 0.","This value must be greater than 0." "To Cart","To Cart" "Total","Total" "Type","Type" @@ -374,6 +420,12 @@ "Use as My Default Shipping Address","Use as My Default Shipping Address" "Use as my default billing address","Use as my default billing address" "Use as my default shipping address","Use as my default shipping address" +"VAT Number","VAT Number" +"VAT Request Date","VAT Request Date" +"VAT Request Identifier","VAT Request Identifier" +"Validate VAT Number","Validate VAT Number" +"Validate on Each Transaction","Validate on Each Transaction" +"Validation Error Group","Validation Error Group" "View","View" "View Order","View Order" "Visitor","Visitor" @@ -382,6 +434,7 @@ "Welcome Email","Welcome Email" "Wishlist","Wishlist" "Wishlist - %d item(s)","Wishlist - %d item(s)" +"Would you like to change the Customer Group for this order?","Would you like to change the Customer Group for this order?" "Wrong confirmation key.","Wrong confirmation key." "Wrong customer account specified.","Wrong customer account specified." "Wrong email.","Wrong email." @@ -403,6 +456,10 @@ "You have not set a default shipping address.","You have not set a default shipping address." "You have placed no orders yet.","You have placed no orders yet." "You have placed no products yet.","You have placed no products yet." +"You will be charged tax.","You will be charged tax." +"You will not be charged tax.","You will not be charged tax." +"Your Tax ID cannot be validated.","Your Tax ID cannot be validated." +"Your VAT ID was successfully validated.","Your VAT ID was successfully validated." "Your account balance is: %s","Your account balance is: %s" "Your password has been updated.","Your password has been updated." "Your password reset link has expired.","Your password reset link has expired." diff --git a/app/locale/en_US/Mage_Dataflow.csv b/app/locale/en_US/Mage_Dataflow.csv index 60bb9f8ee4..7169faed85 100644 --- a/app/locale/en_US/Mage_Dataflow.csv +++ b/app/locale/en_US/Mage_Dataflow.csv @@ -1,4 +1,5 @@ "Link","Link" +"Actions XML is not valid.","Actions XML is not valid." "An error occurred while opening file: ""%s"".","An error occurred while opening file: ""%s""." "Could not load file: ""%s"".","Could not load file: ""%s""." "Could not save file: %s.","Could not save file: %s." @@ -18,6 +19,7 @@ "Method ""%s"" was not defined in adapter %s.","Method ""%s"" was not defined in adapter %s." "Please declare ""adapter"" and ""method"" nodes first.","Please declare ""adapter"" and ""method"" nodes first." "Processed records: %s","Processed records: %s" +"Profile with the same name already exists.","Profile with the same name already exists." "Saved successfully: ""%s"" [%d byte(s)].","Saved successfully: ""%s"" [%d byte(s)]." "Sheet 1","Sheet 1" "Skip undefined row.","Skip undefined row." diff --git a/app/locale/en_US/Mage_Directory.csv b/app/locale/en_US/Mage_Directory.csv index fcd4d9f899..7f77011cca 100644 --- a/app/locale/en_US/Mage_Directory.csv +++ b/app/locale/en_US/Mage_Directory.csv @@ -2,6 +2,7 @@ "Allowed Currencies","Allowed Currencies" "Base Currency","Base Currency" "Base currency is used for all online payment transactions. Scope is defined by the catalog price scope (""Catalog"" > ""Price"" > ""Catalog Price Scope"").","Base currency is used for all online payment transactions. Scope is defined by the catalog price scope (""Catalog"" > ""Price"" > ""Catalog Price Scope"")." +"Can\'t convert rate from ""%s-%s"".","Can\'t convert rate from ""%s-%s""." "Cannot retrieve rate from %s.","Cannot retrieve rate from %s." "Connection Timeout in Seconds","Connection Timeout in Seconds" "Continue »","Continue »" diff --git a/app/locale/en_US/Mage_Downloadable.csv b/app/locale/en_US/Mage_Downloadable.csv index 762ebb2527..ded3b4ff22 100644 --- a/app/locale/en_US/Mage_Downloadable.csv +++ b/app/locale/en_US/Mage_Downloadable.csv @@ -51,6 +51,7 @@ "Ordered","Ordered" "Out of stock","Out of stock" "Pending","Pending" +"Please log in to download your product or purchase %s.","Please log in to download your product or purchase %s." "Please log in to download your product.","Please log in to download your product." "Please set resource file and link type.","Please set resource file and link type." "Please specify product link(s).","Please specify product link(s)." @@ -74,6 +75,7 @@ "The file does not exist.","The file does not exist." "The link has expired.","The link has expired." "The link is not available.","The link is not available." +"There are files that were selected but not uploaded yet. Please upload or remove them first","There are files that were selected but not uploaded yet. Please upload or remove them first" "Title","Title" "To:","To:" "U","U" diff --git a/app/locale/en_US/Mage_Eav.csv b/app/locale/en_US/Mage_Eav.csv index 46bacd588c..d8af90a97d 100644 --- a/app/locale/en_US/Mage_Eav.csv +++ b/app/locale/en_US/Mage_Eav.csv @@ -61,6 +61,7 @@ "Entity is not initialized","Entity is not initialized" "Entity object is undefined","Entity object is undefined" "Failed to load node %s from config","Failed to load node %s from config" +"For internal use. Must be unique with no spaces. Maximum length of attribute code must be less then %s symbols","For internal use. Must be unique with no spaces. Maximum length of attribute code must be less then %s symbols" "Form Element with the same attribute","Form Element with the same attribute" "Form Fieldset with the same code","Form Fieldset with the same code" "Form Type with the same code","Form Type with the same code" diff --git a/app/locale/en_US/Mage_GoogleBase.csv b/app/locale/en_US/Mage_GoogleBase.csv index daa8fcf337..964c4a94ff 100644 --- a/app/locale/en_US/Mage_GoogleBase.csv +++ b/app/locale/en_US/Mage_GoogleBase.csv @@ -12,6 +12,7 @@ "AuthSub","AuthSub" "Available Products","Available Products" "Base Currency should be set to %s for %s in system configuration. Otherwise item prices won't be correct in Google Base.","Base Currency should be set to %s for %s in system configuration. Otherwise item prices won't be correct in Google Base." +"Cannot update Google Base Item for Store '%s'","Cannot update Google Base Item for Store '%s'" "Captcha confirmation error: %s","Captcha confirmation error: %s" "Captcha has been confirmed.","Captcha has been confirmed." "Catalog","Catalog" diff --git a/app/locale/en_US/Mage_GoogleCheckout.csv b/app/locale/en_US/Mage_GoogleCheckout.csv index 59144438c1..87b363f9ec 100644 --- a/app/locale/en_US/Mage_GoogleCheckout.csv +++ b/app/locale/en_US/Mage_GoogleCheckout.csv @@ -1,4 +1,8 @@ "* Select shipping method","* Select shipping method" +"2Day","2Day" +"2nd Day Air","2nd Day Air" +"2nd Day Air AM","2nd Day Air AM" +"3 Day Select","3 Day Select" "
    Warning: %s
    ","
    Warning: %s
    " "Warning! This option disables the merchant calculated shipping. With this option, Google API ignores any attempt to affect shipping prices.","Warning! This option disables the merchant calculated shipping. With this option, Google API ignores any attempt to affect shipping prices." "A virtual item to reflect the discount total","A virtual item to reflect the discount total" @@ -15,6 +19,7 @@ "Carrier Calculated Methods","Carrier Calculated Methods" "Cart Discount","Cart Discount" "Checkout Image Style","Checkout Image Style" +"Commercial","Commercial" "Continue Shopping URL","Continue Shopping URL" "Credit memo has been created automatically","Credit memo has been created automatically" "Debug","Debug" @@ -25,9 +30,11 @@ "Default price:","Default price:" "Delivery Address Category","Delivery Address Category" "Delivery Schedule","Delivery Schedule" +"Description-based delivery","Description-based delivery" "Disable Default Tax Tables","Disable Default Tax Tables" "Discount Tax","Discount Tax" "Eligible for Protection: %s","Eligible for Protection: %s" +"Email delivery","Email delivery" "Enable","Enable" "Enable Carrier Calculated","Enable Carrier Calculated" "Enable Digital Delivery","Enable Digital Delivery" @@ -35,7 +42,11 @@ "Enable Merchant Calculated","Enable Merchant Calculated" "Enable this if your checkout request is too big and being cut off.","Enable this if your checkout request is too big and being cut off." "Expiration: %s","Expiration: %s" +"Express Mail","Express Mail" +"Express Saver","Express Saver" +"FedEx","FedEx" "Financial: %s -> %s","Financial: %s -> %s" +"First Overnight","First Overnight" "Free Shipping","Free Shipping" "Fulfillment: %s -> %s","Fulfillment: %s -> %s" "Google API","Google API" @@ -53,24 +64,39 @@ "Google Order Status Change:","Google Order Status Change:" "Google Refund:","Google Refund:" "Google Risk Information:","Google Risk Information:" +"Ground","Ground" "Hide Cart Contents","Hide Cart Contents" +"Home Delivery","Home Delivery" "IP Address: %s","IP Address: %s" "If enabled, cart contents will be hidden after clicking on the Google Checkout button in the shopping cart, and restored if ""Edit Cart"" link was activated.","If enabled, cart contents will be hidden after clicking on the Google Checkout button in the shopping cart, and restored if ""Edit Cart"" link was activated." +"Inches","Inches" "Invoice Auto-Created: %s","Invoice Auto-Created: %s" "Is Buyer Willing to Receive Marketing Emails: %s","Is Buyer Willing to Receive Marketing Emails: %s" +"Key/URL delivery","Key/URL delivery" "Large - %s","Large - %s" "Latest Charge: %s","Latest Charge: %s" "Latest Chargeback: %s","Latest Chargeback: %s" "Latest Refund: %s","Latest Refund: %s" "Location","Location" +"Media Mail","Media Mail" "Medium - %s","Medium - %s" "Merchant ID","Merchant ID" "Merchant Key","Merchant Key" "Method","Method" "New Order Status","New Order Status" +"Next Day Air","Next Day Air" +"Next Day Air Early AM","Next Day Air Early AM" +"Next Day Air Saver","Next Day Air Saver" "No","No" +"No Comment","No Comment" +"No Reason","No Reason" +"Optimistic","Optimistic" "Optional, leave empty for home page.","Optional, leave empty for home page." "Order creation error","Order creation error" +"Parcel Post","Parcel Post" +"Pessimistic","Pessimistic" +"Priority Mail","Priority Mail" +"Priority Overnight","Priority Overnight" "Rate 1 Amount","Rate 1 Amount" "Rate 1 Ship To Applicable Countries","Rate 1 Ship To Applicable Countries" "Rate 1 Ship to Specific Countries","Rate 1 Ship to Specific Countries" @@ -85,17 +111,22 @@ "Rate 3 Title","Rate 3 Title" "Remove","Remove" "Required for live Google Checkout transactions.","Required for live Google Checkout transactions." +"Residential","Residential" "Sandbox","Sandbox" "Secure Callback URL","Secure Callback URL" "Small - %s","Small - %s" +"Standard Overnight","Standard Overnight" "The tax amount has been applied based on the information received from Google Checkout, because tax amount received from Google Checkout is different from the calculated tax amount","The tax amount has been applied based on the information received from Google Checkout, because tax amount received from Google Checkout is different from the calculated tax amount" "Title","Title" "Total Chargeback: %s","Total Chargeback: %s" "Total Charged: %s","Total Charged: %s" "Total Refunded: %s","Total Refunded: %s" "Transparent","Transparent" +"UPS","UPS" +"USPS","USPS" "United Kingdom","United Kingdom" "United States","United States" +"Unknown Reason","Unknown Reason" "White Background","White Background" "Yes","Yes" "You will be redirected to GoogleCheckout in a few seconds.","You will be redirected to GoogleCheckout in a few seconds." diff --git a/app/locale/en_US/Mage_Index.csv b/app/locale/en_US/Mage_Index.csv index a185a13bae..4d02a5509c 100644 --- a/app/locale/en_US/Mage_Index.csv +++ b/app/locale/en_US/Mage_Index.csv @@ -19,12 +19,11 @@ "Indexer code is not defined.","Indexer code is not defined." "Indexer model is not defined.","Indexer model is not defined." "Indexer model should extend Mage_Index_Model_Indexer_Abstract.","Indexer model should extend Mage_Index_Model_Indexer_Abstract." -"Last Run","Last Run" "Manual Update","Manual Update" "Mode","Mode" "Never","Never" +"No","No" "One or more of the Indexes are not up to date:","One or more of the Indexes are not up to date:" -"Pending Events","Pending Events" "Please select Index(es)","Please select Index(es)" "Please select Indexes","Please select Indexes" "Process Information","Process Information" @@ -40,4 +39,7 @@ "There was a problem with saving process.","There was a problem with saving process." "Total of %d index(es) have changed index mode.","Total of %d index(es) have changed index mode." "Total of %d index(es) have reindexed data.","Total of %d index(es) have reindexed data." +"Update Required","Update Required" "Update on Save","Update on Save" +"Updated At","Updated At" +"Yes","Yes" diff --git a/app/locale/en_US/Mage_Install.csv b/app/locale/en_US/Mage_Install.csv index e2abbd0d2a..a26c853d3a 100644 --- a/app/locale/en_US/Mage_Install.csv +++ b/app/locale/en_US/Mage_Install.csv @@ -27,6 +27,7 @@ "Database Type","Database Type" "Database connection error.","Database connection error." "Database host","Database host" +"Database server does not support the InnoDB storage engine.","Database server does not support the InnoDB storage engine." "Database user name","Database user name" "Database user password","Database user password" "Default Currency","Default Currency" @@ -59,6 +60,7 @@ "In other cases you will need to make Magento files writeable for the user that the web server process is running under.","In other cases you will need to make Magento files writeable for the user that the web server process is running under." "Installation","Installation" "Installation Guide","Installation Guide" +"Installer does not exist for %s database type","Installer does not exist for %s database type" "Last Name","Last Name" "License Agreement","License Agreement" "Locale","Locale" @@ -74,6 +76,7 @@ "No resource for %s DB model.","No resource for %s DB model." "One of PHP Extensions ""%s"" must be loaded.","One of PHP Extensions ""%s"" must be loaded." "PHP Extension '%s' loaded","PHP Extension '%s' loaded" +"PHP Extensions ""%s"" must be loaded.","PHP Extensions ""%s"" must be loaded." "PHP extension ""%s"" must be loaded.","PHP extension ""%s"" must be loaded." "Package Management through the Web","Package Management through the Web" "Password","Password" @@ -103,6 +106,8 @@ "Tables Prefix","Tables Prefix" "The URL ""%s"" is invalid.","The URL ""%s"" is invalid." "The URL ""%s"" is not accessible.","The URL ""%s"" is not accessible." +"The database server version doesn\'t match system requirements (required: %s, actual: %s).","The database server version doesn\'t match system requirements (required: %s, actual: %s)." +"The table prefix should contain only letters (a-z), numbers (0-9) or underscores (_), the first character should be a letter.","The table prefix should contain only letters (a-z), numbers (0-9) or underscores (_), the first character should be a letter." "There was a problem installing Magento packages. Please read the log, correct errors and try again.","There was a problem installing Magento packages. Please read the log, correct errors and try again." "Time Zone","Time Zone" "Use Secure URLs (SSL)","Use Secure URLs (SSL)" diff --git a/app/locale/en_US/Mage_Page.csv b/app/locale/en_US/Mage_Page.csv index b53a79eee8..8035808bf7 100644 --- a/app/locale/en_US/Mage_Page.csv +++ b/app/locale/en_US/Mage_Page.csv @@ -39,7 +39,7 @@ "Help Us to Keep Magento Healthy","Help Us to Keep Magento Healthy" "Interface Language","Interface Language" "Items %s to %s of %s total","Items %s to %s of %s total" -"JavaScript seem to be disabled in your browser.","JavaScript seem to be disabled in your browser." +"JavaScript seems to be disabled in your browser.","JavaScript seems to be disabled in your browser." "Left Column","Left Column" "Main Content Area","Main Content Area" "Navigation Bar","Navigation Bar" diff --git a/app/locale/en_US/Mage_Paygate.csv b/app/locale/en_US/Mage_Paygate.csv index e447bf42a4..841297c09b 100644 --- a/app/locale/en_US/Mage_Paygate.csv +++ b/app/locale/en_US/Mage_Paygate.csv @@ -1,4 +1,3 @@ -"%s %s %s - %s. %s. %s","%s %s %s - %s. %s. %s" "3D Secure","3D Secure" "3D Secure Card Validation","3D Secure Card Validation" "API Login ID","API Login ID" diff --git a/app/locale/en_US/Mage_Payment.csv b/app/locale/en_US/Mage_Payment.csv index 779511ac83..809565db45 100644 --- a/app/locale/en_US/Mage_Payment.csv +++ b/app/locale/en_US/Mage_Payment.csv @@ -6,6 +6,7 @@ "A value is required for live mode. Refer to your CardinalCommerce agreement.","A value is required for live mode. Refer to your CardinalCommerce agreement." "Allow Initial Fee Failure","Allow Initial Fee Failure" "An internal reference ID is required to save the payment profile.","An internal reference ID is required to save the payment profile." +"Authorize action is not available.","Authorize action is not available." "Auto Bill on Next Cycle","Auto Bill on Next Cycle" "Automatically Invoice All Items","Automatically Invoice All Items" "Automatically bill the outstanding balance amount in the next billing cycle (if there were failed payments).","Automatically bill the outstanding balance amount in the next billing cycle (if there were failed payments)." @@ -17,7 +18,10 @@ "Billing period unit is not defined or wrong.","Billing period unit is not defined or wrong." "Cannot retrieve payment method instance.","Cannot retrieve payment method instance." "Cannot retrieve the payment info model object.","Cannot retrieve the payment info model object." +"Cannot retrieve the payment information object instance.","Cannot retrieve the payment information object instance." +"Cannot retrieve the payment method code.","Cannot retrieve the payment method code." "Cannot retrieve the payment method model object.","Cannot retrieve the payment method model object." +"Capture action is not available.","Capture action is not available." "Card Verification Number","Card Verification Number" "Centinel API URL","Centinel API URL" "Check / Money Order","Check / Money Order" @@ -26,6 +30,8 @@ "Credit Card Type","Credit Card Type" "Credit Card Type: %s","Credit Card Type: %s" "Credit Card Types","Credit Card Types" +"Credit card number mismatch with credit card type.","Credit card number mismatch with credit card type." +"Credit card type is not allowed for this payment method.","Credit card type is not allowed for this payment method." "Currency","Currency" "Currency code is undefined.","Currency code is undefined." "Customer ID is not set.","Customer ID is not set." @@ -35,9 +41,11 @@ "Expiration Date","Expiration Date" "Expiration Date: %s/%s","Expiration Date: %s/%s" "Full name of the person receiving the product or service paid for by the recurring payment.","Full name of the person receiving the product or service paid for by the recurring payment." +"Incorrect credit card expiration date.","Incorrect credit card expiration date." "Initial Fee","Initial Fee" "Initial non-recurring payment amount due immediately upon profile creation.","Initial non-recurring payment amount due immediately upon profile creation." "Internal Reference ID","Internal Reference ID" +"Invalid Credit Card Number","Invalid Credit Card Number" "Issue Number","Issue Number" "Make Check Payable to","Make Check Payable to" "Make Check payable to:","Make Check payable to:" @@ -55,6 +63,7 @@ "Name on the Card: %s","Name on the Card: %s" "New Order Status","New Order Status" "Number of billing periods that make up one billing cycle.","Number of billing periods that make up one billing cycle." +"Order action is not available.","Order action is not available." "Overrides API URL that may be specified by a payment method.","Overrides API URL that may be specified by a payment method." "Password","Password" "Payment ID: %s","Payment ID: %s" @@ -69,6 +78,7 @@ "Payment method code is undefined.","Payment method code is undefined." "Payment profile is invalid:\n%s","Payment profile is invalid:\n%s" "Period frequency is wrong.","Period frequency is wrong." +"Please enter a valid credit card verification number.","Please enter a valid credit card verification number." "Processor ID","Processor ID" "Purchase Order","Purchase Order" "Purchase Order Number","Purchase Order Number" @@ -77,12 +87,14 @@ "Recurring Profile Start Date","Recurring Profile Start Date" "Recurring profile start date has invalid format.","Recurring profile start date has invalid format." "Reference ID is not set.","Reference ID is not set." +"Refund action is not available.","Refund action is not available." "Repeats %s time(s).","Repeats %s time(s)." "Repeats until suspended or canceled.","Repeats until suspended or canceled." "Request Card Security Code","Request Card Security Code" "Saved CC","Saved CC" "Schedule Description","Schedule Description" "Schedule description must be not empty.","Schedule description must be not empty." +"Selected payment type is not allowed for billing country.","Selected payment type is not allowed for billing country." "Send Check to","Send Check to" "Send Check to:","Send Check to:" "Severe 3D Secure Card Validation","Severe 3D Secure Card Validation" @@ -116,6 +128,7 @@ "Unable to save Billing Agreement:","Unable to save Billing Agreement:" "Unit for billing during the subscription period.","Unit for billing during the subscription period." "Unsupported currency code: %s.","Unsupported currency code: %s." +"Void action is not available.","Void action is not available." "Week","Week" "What is this?","What is this?" "Whether to suspend the payment profile if the initial fee fails or add it to the outstanding balance.","Whether to suspend the payment profile if the initial fee fails or add it to the outstanding balance." diff --git a/app/locale/en_US/Mage_Paypal.csv b/app/locale/en_US/Mage_Paypal.csv index 4460f85387..62d39b9b8a 100644 --- a/app/locale/en_US/Mage_Paypal.csv +++ b/app/locale/en_US/Mage_Paypal.csv @@ -1,5 +1,3 @@ -"$0 Auth","$0 Auth" -"$1 Auth","$1 Auth" "-OR-","-OR-" "2-pixel perimeter around the header space.","2-pixel perimeter around the header space." "3D Secure","3D Secure" @@ -21,6 +19,8 @@ "API/Integration Settings","API/Integration Settings" "Ability for buyer to purchase without PayPal account.","Ability for buyer to purchase without PayPal account." "Accept PayPal payments in your shopping cart. PayPal will process your credit card payments through the Payflow Pro Gateway.","Accept PayPal payments in your shopping cart. PayPal will process your credit card payments through the Payflow Pro Gateway." +"Accept payments with a PCI-compliant checkout that keeps customers on your site. Includes a merchant account from PayPal.","Accept payments with a PCI-compliant checkout that keeps customers on your site. Includes a merchant account from PayPal." +"Accept payments with a PCI-compliant checkout that keeps customers on your site. For use with your own merchant account.","Accept payments with a PCI-compliant checkout that keeps customers on your site. For use with your own merchant account." "Accept payments without customers leaving your website. Many popular web-hosting services and shopping carts come with the Payflow payment gateways built in, so they are easy to set up.","Accept payments without customers leaving your website. Many popular web-hosting services and shopping carts come with the Payflow payment gateways built in, so they are easy to set up." "Acceptance Mark","Acceptance Mark" "Acceptance Mark Image","Acceptance Mark Image" @@ -59,6 +59,7 @@ "CNPJ","CNPJ" "CPF","CPF" "CVV2 Check Result by PayPal","CVV2 Check Result by PayPal" +"Can not send new order email.","Can not send new order email." "Cannot create target file for reading reports.","Cannot create target file for reading reports." "Centinel API URL","Centinel API URL" "Centinel Custom API URL","Centinel Custom API URL" @@ -128,7 +129,6 @@ "Fetch Updates","Fetch Updates" "Fetched %s report rows from '%s@%s'.","Fetched %s report rows from '%s@%s'." "Frontend Experience Settings","Frontend Experience Settings" -"Full Auth","Full Auth" "Funding","Funding" "General (Authorization)","General (Authorization)" "General (Dividend)","General (Dividend)" @@ -169,7 +169,6 @@ "Initiation Date","Initiation Date" "Integral Evolution","Integral Evolution" "Integral Evolution Settings","Integral Evolution Settings" -"Invalid Amount","Invalid Amount" "Invoice ID","Invoice ID" "Issuer Liability","Issuer Liability" "It is recommended to set this value to ""Debit or Credit Card"" per store views.","It is recommended to set this value to ""Debit or Credit Card"" per store views." @@ -178,7 +177,6 @@ "Last Correlation ID","Last Correlation ID" "Last Transaction ID","Last Transaction ID" "Learn More","Learn More" -"Loading payment information...","Loading payment information..." "Login","Login" "Looking for PayPal? We have our own tab to the left under Sales.","Looking for PayPal? We have our own tab to the left under Sales." "Mass Pay Payment","Mass Pay Payment" @@ -254,6 +252,7 @@ "PayPal Express Order Review Form","PayPal Express Order Review Form" "PayPal Fee Information","PayPal Fee Information" "PayPal Merchant Pages Style","PayPal Merchant Pages Style" +"PayPal Payments Advanced","PayPal Payments Advanced" "PayPal Pro","PayPal Pro" "PayPal Pro Settings","PayPal Pro Settings" "PayPal Product Logo","PayPal Product Logo" @@ -306,7 +305,6 @@ "Proxy Host","Proxy Host" "Proxy Port","Proxy Port" "Qty","Qty" -"Quick set-up service lets your customers securely complete transactions.","Quick set-up service lets your customers securely complete transactions." "Reauthorization","Reauthorization" "Reference Information","Reference Information" "Refund issued by merchant.","Refund issued by merchant." @@ -331,6 +329,7 @@ "Schedule description is too long.","Schedule description is too long." "Scheduled Fetching","Scheduled Fetching" "Select a PayPal Solution","Select a PayPal Solution" +"Server Error. Please try again.","Server Error. Please try again." "Settlement Consolidation","Settlement Consolidation" "Settlement Report Settings","Settlement Report Settings" "Settlement of a chargeback.","Settlement of a chargeback." @@ -394,6 +393,7 @@ "Unable to process Express Checkout approval.","Unable to process Express Checkout approval." "Unable to start Express Checkout.","Unable to start Express Checkout." "Unable to update shipping method.","Unable to update shipping method." +"Unknown Error. Please try again later.","Unknown Error. Please try again later." "Unknown reason. Please contact PayPal customer service.","Unknown reason. Please contact PayPal customer service." "Update Shipping Method","Update Shipping Method" "Use Proxy","Use Proxy" @@ -403,7 +403,6 @@ "Uses store frontend name by default.","Uses store frontend name by default." "Vendor","Vendor" "Vendor / Merchant Login","Vendor / Merchant Login" -"Verification Authorization Amount","Verification Authorization Amount" "View","View" "View Demo","View Demo" "View PayPal solutions.","View PayPal solutions." @@ -431,7 +430,6 @@ "WorldLink Withdrawal","WorldLink Withdrawal" "Would you like to sign a billing agreement to streamline further purchases with PayPal?","Would you like to sign a billing agreement to streamline further purchases with PayPal?" "Wrong PayPal Express Checkout Token specified.","Wrong PayPal Express Checkout Token specified." -"Wrong configuration of Payment Method","Wrong configuration of Payment Method" "Yes","Yes" "Yes. Matched Address and five-didgit ZIP","Yes. Matched Address and five-didgit ZIP" "You cannot void a verification transaction","You cannot void a verification transaction" diff --git a/app/locale/en_US/Mage_Persistent.csv b/app/locale/en_US/Mage_Persistent.csv index 7c5ab9e1dc..eae5d2aa2b 100644 --- a/app/locale/en_US/Mage_Persistent.csv +++ b/app/locale/en_US/Mage_Persistent.csv @@ -56,6 +56,7 @@ "Street Address %s","Street Address %s" "Submit","Submit" "Telephone","Telephone" +"VAT Number","VAT Number" "Welcome, %s!","Welcome, %s!" "What\'s this?","What\'s this?" "Zip/Postal Code","Zip/Postal Code" diff --git a/app/locale/en_US/Mage_Reports.csv b/app/locale/en_US/Mage_Reports.csv index 0c055c4619..c1b5626d86 100644 --- a/app/locale/en_US/Mage_Reports.csv +++ b/app/locale/en_US/Mage_Reports.csv @@ -68,7 +68,6 @@ "Name","Name" "New Accounts","New Accounts" "No","No" -"No records found for this period.","No records found for this period." "Number Added","Number Added" "Number Fulfilled","Number Fulfilled" "Number Of Reviews","Number Of Reviews" @@ -132,6 +131,7 @@ "Select day of the month.","Select day of the month." "Shipping","Shipping" "Shopping Cart","Shopping Cart" +"Shopping Cart Price Rule","Shopping Cart Price Rule" "Show Actual Values","Show Actual Values" "Show Report","Show Report" "Show for Current","Show for Current" diff --git a/app/locale/en_US/Mage_Review.csv b/app/locale/en_US/Mage_Review.csv index 9eef53f76e..ca5ab989d9 100644 --- a/app/locale/en_US/Mage_Review.csv +++ b/app/locale/en_US/Mage_Review.csv @@ -6,6 +6,7 @@ "3 stars","3 stars" "4 stars","4 stars" "5 stars","5 stars" +"%2$s %3$s (%4$s)","%2$s %3$s (%4$s)" "Add New Review","Add New Review" "Add Your Review","Add Your Review" "Administrator","Administrator" @@ -43,6 +44,7 @@ "Name in Store","Name in Store" "New Review","New Review" "Nickname","Nickname" +"Nickname can\'t be empty","Nickname can\'t be empty" "Only registered users can write reviews. Please, log in or register","Only registered users can write reviews. Please, log in or register" "Pending Reviews","Pending Reviews" "Pending Reviews of Customer `%s`","Pending Reviews of Customer `%s`" @@ -65,6 +67,8 @@ "Review Details","Review Details" "Review Form Fields Before","Review Form Fields Before" "Review by %s","Review by %s" +"Review can\'t be empty","Review can\'t be empty" +"Review summary can\'t be empty","Review summary can\'t be empty" "Reviews and Ratings","Reviews and Ratings" "SKU","SKU" "Save Review","Save Review" diff --git a/app/locale/en_US/Mage_Rss.csv b/app/locale/en_US/Mage_Rss.csv index 64ae2f3a63..7b5dbe4e26 100644 --- a/app/locale/en_US/Mage_Rss.csv +++ b/app/locale/en_US/Mage_Rss.csv @@ -21,6 +21,7 @@ "Get Feed","Get Feed" "Gift Message","Gift Message" "Grand Total","Grand Total" +"Low Stock Products","Low Stock Products" "Message:","Message:" "Miscellaneous Feeds","Miscellaneous Feeds" "New Orders","New Orders" @@ -29,6 +30,7 @@ "Notified Date: %s
    ","Notified Date: %s
    " "Order # %s Notification(s)","Order # %s Notification(s)" "Order #%s created at %s","Order #%s created at %s" +"Pending product review(s)","Pending product review(s)" "Product: ""%s"" review By: %s","Product: ""%s"" review By: %s" "Product: %s
    ","Product: %s
    " "Products tagged with %s","Products tagged with %s" diff --git a/app/locale/en_US/Mage_Rule.csv b/app/locale/en_US/Mage_Rule.csv index fe9e316556..868d5c3245 100644 --- a/app/locale/en_US/Mage_Rule.csv +++ b/app/locale/en_US/Mage_Rule.csv @@ -9,10 +9,6 @@ "FALSE","FALSE" "If %s of these conditions are %s:","If %s of these conditions are %s:" "Invalid discount amount.","Invalid discount amount." -"MATCHING ALL","MATCHING ALL" -"MATCHING ANY","MATCHING ANY" -"NOT MATCHING ALL","NOT MATCHING ALL" -"NOT MATCHING ANY","NOT MATCHING ANY" "Name: %s","Name: %s" "Open Chooser","Open Chooser" "Perform following actions","Perform following actions" diff --git a/app/locale/en_US/Mage_Sales.csv b/app/locale/en_US/Mage_Sales.csv index ac04eb30b6..db2f4c1493 100644 --- a/app/locale/en_US/Mage_Sales.csv +++ b/app/locale/en_US/Mage_Sales.csv @@ -126,6 +126,7 @@ "Changing address information will not recalculate shipping, tax or other order amount.","Changing address information will not recalculate shipping, tax or other order amount." "Checkout Totals Sort Order","Checkout Totals Sort Order" "City","City" +"Clear Shopping Cart","Clear Shopping Cart" "Click to change shipping method","Click to change shipping method" "Close","Close" "Closed","Closed" @@ -340,12 +341,15 @@ "Logo for PDF Print-outs (200x50)","Logo for PDF Print-outs (200x50)" "Make Check payable to:","Make Check payable to:" "Manage","Manage" +"Maximum amount available to refund is %s","Maximum amount available to refund is %s" "Maximum shipping amount allowed to refund is: %s","Maximum shipping amount allowed to refund is: %s" "Message","Message" "Message:","Message:" "Minimum Amount","Minimum Amount" "Minimum Order Amount","Minimum Order Amount" "Mixed","Mixed" +"Most Viewed","Most Viewed" +"Most Viewed Products Report","Most Viewed Products Report" "Move to Shopping Cart","Move to Shopping Cart" "Move to Wishlist","Move to Wishlist" "Multi-address Description Message","Multi-address Description Message" @@ -358,6 +362,8 @@ "New Billing Agreement","New Billing Agreement" "New Credit Memo","New Credit Memo" "New Credit Memo for Guest","New Credit Memo for Guest" +"New Credit Memo for Invoice #%s","New Credit Memo for Invoice #%s" +"New Credit Memo for Order #%s","New Credit Memo for Order #%s" "New Invoice","New Invoice" "New Invoice and Shipment for Order #%s","New Invoice and Shipment for Order #%s" "New Invoice for Guest","New Invoice for Guest" @@ -493,8 +499,12 @@ "Please Select Products to Add","Please Select Products to Add" "Please Select a Customer","Please Select a Customer" "Please Select a Store","Please Select a Store" +"Please check billing address information. %s","Please check billing address information. %s" +"Please check shipping address information. %s","Please check shipping address information. %s" "Please define PDF object before using.","Please define PDF object before using." "Please enter positive number in this field.","Please enter positive number in this field." +"Please select a valid payment method.","Please select a valid payment method." +"Please specify a shipping method.","Please specify a shipping method." "Please specify a valid grid column alias name that exists in grid table.","Please specify a valid grid column alias name that exists in grid table." "Price","Price" "Print","Print" @@ -792,6 +802,7 @@ "Tracking Number","Tracking Number" "Tracking Number(s):","Tracking Number(s):" "Transaction ""%s"" was already processed.","Transaction ""%s"" was already processed." +"Transaction # %s | %s","Transaction # %s | %s" "Transaction ID","Transaction ID" "Transaction ID must not be empty.","Transaction ID must not be empty." "Transaction ID: ""%s"".","Transaction ID: ""%s""." diff --git a/app/locale/en_US/Mage_SalesRule.csv b/app/locale/en_US/Mage_SalesRule.csv index f79ae3b048..88e4935ecc 100644 --- a/app/locale/en_US/Mage_SalesRule.csv +++ b/app/locale/en_US/Mage_SalesRule.csv @@ -1,39 +1,51 @@ "%d Shopping Cart Price Rules based on ""%s"" attribute have been disabled.","%d Shopping Cart Price Rules based on ""%s"" attribute have been disabled." +"%s Coupon(s) generated successfully","%s Coupon(s) generated successfully" "Actions","Actions" "Active","Active" "Add New Rule","Add New Rule" +"Alphabetical","Alphabetical" +"Alphanumeric","Alphanumeric" +"An error occurred while generating coupons. Please review the log and try again.","An error occurred while generating coupons. Please review the log and try again." "Apply","Apply" "Apply To","Apply To" "Apply the rule only if the following conditions are met (leave blank for all products)","Apply the rule only if the following conditions are met (leave blank for all products)" "Apply the rule only to cart items matching the following conditions (leave blank for all items)","Apply the rule only to cart items matching the following conditions (leave blank for all items)" "Apply the rule to cart items matching the following conditions","Apply the rule to cart items matching the following conditions" "Apply to Shipping Amount","Apply to Shipping Amount" +"Are you sure you want to delete the selected coupon(s)?","Are you sure you want to delete the selected coupon(s)?" "Auto","Auto" +"Auto Generated Specific Coupon Codes","Auto Generated Specific Coupon Codes" "Buy X get Y free (discount amount is Y)","Buy X get Y free (discount amount is Y)" "By Fixed value","By Fixed value" "By Percentage","By Percentage" "Can\'t acquire coupon.","Can\'t acquire coupon." "Cart Attribute","Cart Attribute" "Catalog","Catalog" +"Code Format","Code Format" +"Code Length","Code Length" +"Code Prefix","Code Prefix" +"Code Suffix","Code Suffix" "Conditions","Conditions" "Conditions combination","Conditions combination" "Coupon","Coupon" "Coupon Code","Coupon Code" +"Coupon Qty","Coupon Qty" +"Coupon with the same code","Coupon with the same code" +"Coupons Information","Coupons Information" +"Created On","Created On" "Customer Groups","Customer Groups" -"Customer is new buyer: %s","Customer is new buyer: %s" -"Customer registered: %s","Customer registered: %s" +"Dash Every X Characters","Dash Every X Characters" "Date Expire","Date Expire" "Date Start","Date Start" "Default Label","Default Label" "Default Rule Label for All Store Views","Default Rule Label for All Store Views" "Delete Rule","Delete Rule" "Description","Description" -"Description: %s","Description: %s" "Discount Amount","Discount Amount" "Discount Qty Step (Buy X)","Discount Qty Step (Buy X)" "Edit Rule","Edit Rule" "Edit Rule '%s'","Edit Rule '%s'" -"Expire at: %s","Expire at: %s" +"Excluding prefix, suffix and separators.","Excluding prefix, suffix and separators." "FOUND","FOUND" "Fixed amount discount","Fixed amount discount" "Fixed amount discount for whole cart","Fixed amount discount for whole cart" @@ -42,18 +54,25 @@ "Free Shipping","Free Shipping" "From Date","From Date" "General Information","General Information" +"Generate","Generate" "ID","ID" +"If %s %s %s for a subselection of items in cart matching %s of these conditions:","If %s %s %s for a subselection of items in cart matching %s of these conditions:" +"If an item is %s in the cart with %s of these conditions true:","If an item is %s in the cart with %s of these conditions true:" +"If empty no separation.","If empty no separation." +"If you select and save the rule you will be able to generate multiple coupon codes","If you select and save the rule you will be able to generate multiple coupon codes" "Inactive","Inactive" "Item totals are not set for rule.","Item totals are not set for rule." "Labels","Labels" +"Manage Coupons Codes","Manage Coupons Codes" "Maximum Qty Discount is Applied To","Maximum Qty Discount is Applied To" "NOT FOUND","NOT FOUND" "NOT LOGGED IN","NOT LOGGED IN" -"Name: %s","Name: %s" "New Rule","New Rule" "No","No" "No Coupon","No Coupon" +"Not valid data provided","Not valid data provided" "Number of Uses","Number of Uses" +"Numeric","Numeric" "Payment Method","Payment Method" "Percent of product price discount","Percent of product price discount" "Period","Period" @@ -67,6 +86,7 @@ "Row total in cart","Row total in cart" "Rule Information","Rule Information" "Rule Name","Rule Name" +"Rule is not defined","Rule is not defined" "Sales Discount Amount","Sales Discount Amount" "Sales Subtotal Amount","Sales Subtotal Amount" "Sales Total Amount","Sales Total Amount" @@ -81,7 +101,6 @@ "Shopping Cart Price Rules","Shopping Cart Price Rules" "Special Price","Special Price" "Specific Coupon","Specific Coupon" -"Start at: %s","Start at: %s" "Status","Status" "Stop Further Rules Processing","Stop Further Rules Processing" "Store View Specific Labels","Store View Specific Labels" @@ -90,6 +109,7 @@ "The rule has been deleted.","The rule has been deleted." "The rule has been saved.","The rule has been saved." "This rule no longer exists.","This rule no longer exists." +"Times Used","Times Used" "To Date","To Date" "To Fixed Value","To Fixed Value" "To Percentage","To Percentage" @@ -97,11 +117,16 @@ "Total Amount","Total Amount" "Total Items Quantity","Total Items Quantity" "Total Weight","Total Weight" +"Unable to create requested Coupon Qty. Please check settings and try again.","Unable to create requested Coupon Qty. Please check settings and try again." "Unable to find a rule to delete.","Unable to find a rule to delete." "Update prices using the following information","Update prices using the following information" "Update product's %s %s: %s","Update product's %s %s: %s" "Update the Product","Update the Product" +"Use Auto Generation","Use Auto Generation" +"Used","Used" "Uses per Coupon","Uses per Coupon" "Uses per Customer","Uses per Customer" "Wrong rule specified.","Wrong rule specified." "Yes","Yes" +"total amount","total amount" +"total quantity","total quantity" diff --git a/app/locale/en_US/Mage_Shipping.csv b/app/locale/en_US/Mage_Shipping.csv index 839b245531..3b132907ae 100644 --- a/app/locale/en_US/Mage_Shipping.csv +++ b/app/locale/en_US/Mage_Shipping.csv @@ -1,6 +1,7 @@ " for more information or "," for more information or " "# of Items (and above)","# of Items (and above)" "# of Items vs. Destination","# of Items vs. Destination" +"%1$d records have been imported. See the following list of errors for each record that has not been imported: %2$s","%1$d records have been imported. See the following list of errors for each record that has not been imported: %2$s" "Allow Shipping to Multiple Addresses","Allow Shipping to Multiple Addresses" "An error occurred while import table rates.","An error occurred while import table rates." "Calculate Handling Fee","Calculate Handling Fee" @@ -14,6 +15,7 @@ "Delivered to:","Delivered to:" "Description","Description" "Displayed Error Message","Displayed Error Message" +"Duplicate Row #%s (Country ""%s"", Region/State ""%s"", Zip ""%s"" and Value ""%s"").","Duplicate Row #%s (Country ""%s"", Region/State ""%s"", Zip ""%s"" and Value ""%s"")." "Enabled","Enabled" "Error:","Error:" "Export","Export" @@ -25,9 +27,14 @@ "Import","Import" "Include Virtual Products in Price Calculation","Include Virtual Products in Price Calculation" "Info:","Info:" +"Invalid %s ""%s"" in the Row #%s.","Invalid %s ""%s"" in the Row #%s." +"Invalid Country ""%s"" in the Row #%s.","Invalid Country ""%s"" in the Row #%s." +"Invalid Region/State ""%s"" in the Row #%s.","Invalid Region/State ""%s"" in the Row #%s." +"Invalid Shipping Price ""%s"" in the Row #%s.","Invalid Shipping Price ""%s"" in the Row #%s." "Invalid Table Rate code for type %s: %s","Invalid Table Rate code for type %s: %s" "Invalid Table Rate code type: %s","Invalid Table Rate code type: %s" "Invalid Table Rates File Format","Invalid Table Rates File Format" +"Invalid Table Rates format in the Row #%s","Invalid Table Rates format in the Row #%s" "Local Time","Local Time" "Location","Location" "Maximum Qty Allowed for Shipping to Multiple Addresses","Maximum Qty Allowed for Shipping to Multiple Addresses" diff --git a/app/locale/en_US/Mage_Tag.csv b/app/locale/en_US/Mage_Tag.csv index 752777ffcb..948e0cf189 100644 --- a/app/locale/en_US/Mage_Tag.csv +++ b/app/locale/en_US/Mage_Tag.csv @@ -1,4 +1,5 @@ "# of Uses","# of Uses" +"%s tag(s) have been accepted for moderation.","%s tag(s) have been accepted for moderation." "Tag Name: %s","Tag Name: %s" "Action","Action" "Add","Add" @@ -97,3 +98,4 @@ "XML","XML" "You have not tagged any products yet.","You have not tagged any products yet." "ZIP/Post Code","ZIP/Post Code" +"[STORE VIEW]","[STORE VIEW]" diff --git a/app/locale/en_US/Mage_Tax.csv b/app/locale/en_US/Mage_Tax.csv index 0669355af1..3aa98d793d 100644 --- a/app/locale/en_US/Mage_Tax.csv +++ b/app/locale/en_US/Mage_Tax.csv @@ -10,7 +10,7 @@ "An error occurred while deleting this tax rule.","An error occurred while deleting this tax rule." "An error occurred while saving this rate.","An error occurred while saving this rate." "An error occurred while saving this tax class.","An error occurred while saving this tax class." -"An error occurred while saving this tax class. A class with the same name already exists.","An error occurred while saving this tax class. A class with the same name already exists." +"An error occurred while saving this tax class. A class with the same name","An error occurred while saving this tax class. A class with the same name" "An error occurred while saving this tax rule.","An error occurred while saving this tax rule." "Apply Customer Tax","Apply Customer Tax" "Apply Discount On Prices","Apply Discount On Prices" diff --git a/app/locale/en_US/Mage_Usa.csv b/app/locale/en_US/Mage_Usa.csv index d3f46d78d7..3c4f297830 100644 --- a/app/locale/en_US/Mage_Usa.csv +++ b/app/locale/en_US/Mage_Usa.csv @@ -1,4 +1,5 @@ " was not delivered nor scanned"," was not delivered nor scanned" +"""Per Order"" allows single handling fee for entire order. ""Per Package"" allows individual handling fee for each package.","""Per Order"" allows single handling fee for entire order. ""Per Package"" allows individual handling fee for each package." "1 Day Freight","1 Day Freight" "2 Day","2 Day" "2 Day Freight","2 Day Freight" @@ -21,43 +22,68 @@ "Adult","Adult" "Adult Signature Required","Adult Signature Required" "Allowed Methods","Allowed Methods" +"Allows breaking total order weight into smaller pieces if it exeeds %s %s to ensure accurate calculation of shipping charges.","Allows breaking total order weight into smaller pieces if it exeeds %s %s to ensure accurate calculation of shipping charges." +"Allows breaking total order weight into smaller pieces if it exeeds 70 kg to ensure accurate calculation of shipping charges.","Allows breaking total order weight into smaller pieces if it exeeds 70 kg to ensure accurate calculation of shipping charges." "Bound Printed Matter","Bound Printed Matter" +"Break bulk economy","Break bulk economy" +"Break bulk express","Break bulk express" "Business Service Center","Business Service Center" "Calculate Handling Fee","Calculate Handling Fee" "Canada Standard","Canada Standard" +"Cannot identify measure unit for %s","Cannot identify measure unit for %s" +"Cannot identify weight unit for %s","Cannot identify weight unit for %s" "Cannot retrieve shipping rates","Cannot retrieve shipping rates" +"Centimeters","Centimeters" "Commercial","Commercial" "Configuration","Configuration" "Container","Container" +"Content Type","Content Type" "Customer Packaging","Customer Packaging" "Customer Supplied Package","Customer Supplied Package" +"Customer services","Customer services" "DHL","DHL" +"DHL (Deprecated)","DHL (Deprecated)" "Debug","Debug" "Default Package Height","Default Package Height" "Default Package Length","Default Package Length" "Default Package Width","Default Package Width" "Delivered","Delivered" "Delivery Confirmation","Delivery Confirmation" +"Depth","Depth" "Destination Type","Destination Type" "Development","Development" "Direct","Direct" "Displayed Error Message","Displayed Error Message" +"Divide Order Weight","Divide Order Weight" "Documents","Documents" "Domestic Shipment Days","Domestic Shipment Days" +"Domestic economy select","Domestic economy select" +"Domestic express","Domestic express" "Drop Box","Drop Box" "Dropoff","Dropoff" +"Easy shop","Easy shop" +"Economy select","Economy select" "Empty response","Empty response" "Enable Negotiated Rates","Enable Negotiated Rates" "Enabled for Checkout","Enabled for Checkout" "Enables/Disables SSL verification of Magento server by UPS.","Enables/Disables SSL verification of Magento server by UPS." +"Error #%s : %s","Error #%s : %s" "Error #%s : %s (%s)","Error #%s : %s (%s)" "Error #%s: %s","Error #%s: %s" +"Europack","Europack" "Europe First Priority","Europe First Priority" +"Exchange rate %s (Base Currency) -> %s not found. DHL method %s skipped","Exchange rate %s (Base Currency) -> %s not found. DHL method %s skipped" "Express","Express" +"Express 10:30","Express 10:30" "Express 10:30 AM","Express 10:30 AM" +"Express 12:00","Express 12:00" +"Express 9:00","Express 9:00" "Express Mail","Express Mail" "Express Saturday","Express Saturday" "Express Saver","Express Saver" +"Express easy","Express easy" +"Express envelope","Express envelope" +"Express worldwide","Express worldwide" "Failed to parse xml document: %s","Failed to parse xml document: %s" "FedEx","FedEx" "FedEx 10kg Box","FedEx 10kg Box" @@ -74,10 +100,12 @@ "Free Method","Free Method" "Free Shipping with Minimum Order Amount","Free Shipping with Minimum Order Amount" "Freight","Freight" +"Freight worldwide","Freight worldwide" "Gateway URL","Gateway URL" "Gateway XML URL","Gateway XML URL" "Gift","Gift" "Girth","Girth" +"Globalmail business","Globalmail business" "Ground","Ground" "Ground Commercial","Ground Commercial" "Ground Residential","Ground Residential" @@ -85,6 +113,7 @@ "Handling Fee","Handling Fee" "Height","Height" "Home Delivery","Home Delivery" +"Inches","Inches" "Indirect","Indirect" "International Economy","International Economy" "International Express","International Express" @@ -94,6 +123,8 @@ "International Shipment Days","International Shipment Days" "Intl Economy Freight","Intl Economy Freight" "Intl Priority Freight","Intl Priority Freight" +"Jetline","Jetline" +"Jumbo box","Jumbo box" "Key","Key" "Kilograms","Kilograms" "Large","Large" @@ -105,6 +136,7 @@ "Machinable","Machinable" "Maximum Package Weight (Please consult your shipping carrier for maximum supported shipping weight)","Maximum Package Weight (Please consult your shipping carrier for maximum supported shipping weight)" "Media Mail","Media Mail" +"Medical express","Medical express" "Medium Express Box","Medium Express Box" "Merchandise","Merchandise" "Meter Number","Meter Number" @@ -122,11 +154,15 @@ "Next Day Air Saver Letter","Next Day Air Saver Letter" "No","No" "No packages for request","No packages for request" +"Non Documents","Non Documents" +"Non documents","Non documents" "Non-rectangular","Non-rectangular" +"None","None" "Not Required","Not Required" "Order","Order" "Origin of the Shipment","Origin of the Shipment" "Other","Other" +"Others","Others" "PAK","PAK" "Package","Package" "Package Description","Package Description" @@ -152,10 +188,12 @@ "Response is in the wrong format","Response is in the wrong format" "Return","Return" "Round","Round" +"Same day","Same day" "Sample","Sample" "Sandbox Mode","Sandbox Mode" "Second Day Service","Second Day Service" "Secure Gateway URL","Secure Gateway URL" +"Secureline","Secureline" "Sender","Sender" "Service type does not match","Service type does not match" "Ship to Applicable Countries","Ship to Applicable Countries" @@ -175,12 +213,15 @@ "Small Express Box","Small Express Box" "Smart Post","Smart Post" "Sort Order","Sort Order" +"Specific","Specific" +"Sprintline","Sprintline" "Standard Overnight","Standard Overnight" "Station","Station" "Subtotal","Subtotal" "Subtotal With Discount","Subtotal With Discount" "The response is in wrong format.","The response is in wrong format." "There is no available method for selected shipping address.","There is no available method for selected shipping address." +"There is no items in this order","There is no items in this order" "Third Party","Third Party" "Title","Title" "To Lower","To Lower" @@ -223,7 +264,6 @@ "Unknown error","Unknown error" "Used only when ""Additional Protection Value"" is set to ""Configuration"". Can contain only numeric amount.","Used only when ""Additional Protection Value"" is set to ""Configuration"". Can contain only numeric amount." "User ID","User ID" -"UserId","UserId" "Variable","Variable" "Weight Unit","Weight Unit" "Width","Width" @@ -233,5 +273,10 @@ "Worldwide Express Plus","Worldwide Express Plus" "Worldwide Express Plus Letter","Worldwide Express Plus Letter" "Worldwide Express Saver","Worldwide Express Saver" +"Wrong Content Type.","Wrong Content Type." +"Wrong Region.","Wrong Region." "Yes","Yes" "Your Packaging","Your Packaging" +"Zero shipping charge for '%s'","Zero shipping charge for '%s'" +"cm","cm" +"inch","inch" diff --git a/app/locale/en_US/Mage_Widget.csv b/app/locale/en_US/Mage_Widget.csv index edd41fcee7..79dd29c6c9 100644 --- a/app/locale/en_US/Mage_Widget.csv +++ b/app/locale/en_US/Mage_Widget.csv @@ -7,6 +7,7 @@ "All Pages","All Pages" "All Product Types","All Product Types" "Anchor Categories","Anchor Categories" +"Apply","Apply" "Assign to Store Views","Assign to Store Views" "Big Image","Big Image" "Block Reference","Block Reference" @@ -30,6 +31,7 @@ "Next page","Next page" "Non-Anchor Categories","Non-Anchor Categories" "Not Selected","Not Selected" +"Open Chooser","Open Chooser" "Page","Page" "Please Select Block Reference First","Please Select Block Reference First" "Previous page","Previous page" diff --git a/app/locale/en_US/Mage_XmlConnect.csv b/app/locale/en_US/Mage_XmlConnect.csv index 9af43b0eca..0ec93c8cc6 100644 --- a/app/locale/en_US/Mage_XmlConnect.csv +++ b/app/locale/en_US/Mage_XmlConnect.csv @@ -99,6 +99,9 @@ "Back","Back" "Back to App Edit","Back to App Edit" "Background Color","Background Color" +"Balance","Balance" +"Balance Change","Balance Change" +"Balance History","Balance History" "Banner on Home Screen","Banner on Home Screen" "Banner on Home Screen (landscape mode) image missing.","Banner on Home Screen (landscape mode) image missing." "Banner on Home Screen (portrait mode) image missing.","Banner on Home Screen (portrait mode) image missing." @@ -120,7 +123,7 @@ "Can\'t create wishlist.","Can\'t create wishlist." "Can\'t delete ""%s"" theme.","Can\'t delete ""%s"" theme." "Can\'t load XML.","Can\'t load XML." -"Can\'t load application with code ""%s""","Can\'t load application with code ""%s""" +"Can\'t load application with id ""%s""","Can\'t load application with id ""%s""" "Can\'t load cart info.","Can\'t load cart info." "Can\'t load cart.","Can\'t load cart." "Can\'t load customer form.","Can\'t load customer form." @@ -201,6 +204,7 @@ "Customer logout problem.","Customer logout problem." "Customer not logged in.","Customer not logged in." "DD","DD" +"Date","Date" "Date Created","Date Created" "Date Submitted","Date Submitted" "Date Updated","Date Updated" @@ -337,6 +341,7 @@ "Logo in Header","Logo in Header" "Logo in Header image missing.","Logo in Header image missing." "Logout complete.","Logout complete." +"M.I.","M.I." "MEP is PayPal\'s native checkout experience for the iPhone. You can choose to use MEP alongside standard checkout, or use it as your only checkout method for Magento mobile. PayPal MEP requires a PayPal business account","MEP is PayPal\'s native checkout experience for the iPhone. You can choose to use MEP alongside standard checkout, or use it as your only checkout method for Magento mobile. PayPal MEP requires a PayPal business account" "MM","MM" "Mailbox title","Mailbox title" @@ -390,6 +395,7 @@ "Order Date: %s","Order Date: %s" "Order id is not specified.","Order id is not specified." "Order is not available.","Order is not available." +"Ordered","Ordered" "Output format is not specified. Please, specify ""format"" key in constructor, or set it using setFormat().","Output format is not specified. Please, specify ""format"" key in constructor, or set it using setFormat()." "POST data is not valid.","POST data is not valid." "Pages","Pages" @@ -415,6 +421,9 @@ "Please agree to all the terms and conditions before placing the order.","Please agree to all the terms and conditions before placing the order." "Please create and save an application first.","Please create and save an application first." "Please enter ""App Title"".","Please enter ""App Title""." +"Please enter a valid URL. Protocol is required (http://, https:// or ftp://)","Please enter a valid URL. Protocol is required (http://, https:// or ftp://)" +"Please enter a valid date.","Please enter a valid date." +"Please enter a valid email address. For example johndoe@domain.com.","Please enter a valid email address. For example johndoe@domain.com." "Please enter issue number or start date for switch/solo card type.","Please enter issue number or start date for switch/solo card type." "Please enter the Activation Key.","Please enter the Activation Key." "Please enter the Copyright.","Please enter the Copyright." @@ -423,6 +432,7 @@ "Please enter the Resubmission Key.","Please enter the Resubmission Key." "Please enter the Title.","Please enter the Title." "Please make sure your passwords match.","Please make sure your passwords match." +"Please select an option.","Please select an option." "Please select at least one country.","Please select at least one country." "Please upload an image for ""App Background (landscape mode)"" field from Design Tab.","Please upload an image for ""App Background (landscape mode)"" field from Design Tab." "Please upload an image for ""App Background (portrait mode)"" field from Design Tab.","Please upload an image for ""App Background (portrait mode)"" field from Design Tab." @@ -431,6 +441,10 @@ "Please upload an image for ""Banner on Home Screen (portrait mode)"" field from Design Tab.","Please upload an image for ""Banner on Home Screen (portrait mode)"" field from Design Tab." "Please upload an image for ""Banner on Home Screen"" field from Design Tab.","Please upload an image for ""Banner on Home Screen"" field from Design Tab." "Please upload an image for ""Logo in Header"" field from Design Tab.","Please upload an image for ""Logo in Header"" field from Design Tab." +"Please use letters only (a-z or A-Z) in this field.","Please use letters only (a-z or A-Z) in this field." +"Please use numbers only in this field. Please avoid spaces or other characters such as dots or commas.","Please use numbers only in this field. Please avoid spaces or other characters such as dots or commas." +"Please use only letters (a-z or A-Z) or numbers (0-9) only in this field. No spaces or other characters are allowed.","Please use only letters (a-z or A-Z) or numbers (0-9) only in this field. No spaces or other characters are allowed." +"Prefix","Prefix" "Preset Theme","Preset Theme" "Preview","Preview" "Price","Price" @@ -465,8 +479,10 @@ "Recommended size 768px x 294px. Note: Image size affects the performance of your app.","Recommended size 768px x 294px. Note: Image size affects the performance of your app." "Recommended size 768px x 960px. Note: Image size affects the performance of your app.","Recommended size 768px x 960px. Note: Image size affects the performance of your app." "Redemption functionality is disabled.","Redemption functionality is disabled." +"Refunded","Refunded" "Regular and confirmation passwords must be equal","Regular and confirmation passwords must be equal" "Related Product Background Color","Related Product Background Color" +"Request internal error.","Request internal error." "Reset theme to default","Reset theme to default" "Resubmission Key","Resubmission Key" "Resubmit App","Resubmit App" @@ -488,6 +504,7 @@ "Selected product is unavailable.","Selected product is unavailable." "Sender Information","Sender Information" "Settings","Settings" +"Shipped","Shipped" "Shipping Address","Shipping Address" "Shipping Method","Shipping Method" "Shipping address has been set.","Shipping address has been set." @@ -530,6 +547,7 @@ "Submitted","Submitted" "Subtotal (Excl. Tax)","Subtotal (Excl. Tax)" "Subtotal (Incl. Tax)","Subtotal (Incl. Tax)" +"Suffix","Suffix" "Summary of Your Review","Summary of Your Review" "T-Shirts","T-Shirts" "Tabs","Tabs" @@ -544,6 +562,8 @@ "Template does not exist.","Template does not exist." "Template for new AirMail Message does not exist.","Template for new AirMail Message does not exist." "Template has been deleted.","Template has been deleted." +"Text length does not satisfy specified max text range.","Text length does not satisfy specified max text range." +"Text length does not satisfy specified min text range.","Text length does not satisfy specified min text range." "Thank you for registering!","Thank you for registering!" "Thank you for your purchase! ","Thank you for your purchase! " "The Mailbox title will be shown in the More Info tab. To understand more about the title, please click here","The Mailbox title will be shown in the More Info tab. To understand more about the title, please click here" @@ -557,6 +577,7 @@ "Theme name is not set.","Theme name is not set." "There were some problems with the data.","There were some problems with the data." "This email address was not found in our records.","This email address was not found in our records." +"This is a required field.","This is a required field." "Title","Title" "Title bar","Title bar" "To activate PayPal MEP payment method activate Express checkout first. ","To activate PayPal MEP payment method activate Express checkout first. " @@ -637,9 +658,14 @@ "You can watch statistics here.","You can watch statistics here." "You need to enable PayPal Express Checkout first from the Payment configuration before enabling PayPal MECL.","You need to enable PayPal Express Checkout first from the Payment configuration before enabling PayPal MECL." "You will receive an order confirmation email with details of your order and a link to track its progress.","You will receive an order confirmation email with details of your order and a link to track its progress." +"Your current balance is:","Your current balance is:" "Your order # is: %s. ","Your order # is: %s. " "Your review has been accepted for moderation.","Your review has been accepted for moderation." "Zip/Postal Code","Zip/Postal Code" +"\'%s\' exceeds the allowed file size: %d (bytes)","\'%s\' exceeds the allowed file size: %d (bytes)" +"\'%s\' height exceeds allowed value of %d px","\'%s\' height exceeds allowed value of %d px" +"\'%s\' is not a valid file extension. Allowed extensions: %s","\'%s\' is not a valid file extension. Allowed extensions: %s" +"\'%s\' width exceeds allowed value of %d px","\'%s\' width exceeds allowed value of %d px" "and","and" "each","each" "iPad","iPad" diff --git a/downloader/Maged/Connect.php b/downloader/Maged/Connect.php index f1df349d01..f229d5cb73 100644 --- a/downloader/Maged/Connect.php +++ b/downloader/Maged/Connect.php @@ -55,17 +55,17 @@ class Maged_Connect { /** - * Object of config - * - * @var Mage_Connect_Config - */ + * Object of config + * + * @var Mage_Connect_Config + */ protected $_config; /** - * Object of single config - * - * @var Mage_Connect_Singleconfig - */ + * Object of single config + * + * @var Mage_Connect_Singleconfig + */ protected $_sconfig; /** @@ -76,22 +76,29 @@ class Maged_Connect protected $_frontend; /** - * Internal cache for command objects - * - * @var array - */ + * Internal cache for command objects + * + * @var array + */ protected $_cmdCache = array(); /** - * Instance of class - * - * @var Maged_Connect - */ + * Console Started flag + * + * @var boolean + */ + protected $_consoleStarted = false; + + /** + * Instance of class + * + * @var Maged_Connect + */ static protected $_instance; /** - * Constructor - */ + * Constructor loads Config, Cache Config and initializes Frontend + */ public function __construct() { $this->getConfig(); @@ -100,10 +107,20 @@ public function __construct() } /** - * Initialize instance - * - * @return Maged_Connect - */ + * Destructor, sends Console footer if Console started + */ + public function __destruct() + { + if ($this->_consoleStarted) { + $this->_consoleFooter(); + } + } + + /** + * Initialize instance + * + * @return Maged_Connect + */ public static function getInstance() { if (!self::$_instance) { @@ -113,10 +130,10 @@ public static function getInstance() } /** - * Retrieve object of config and set it to Mage_Connect_Command - * - * @return Mage_Connect_Config - */ + * Retrieve object of config and set it to Mage_Connect_Command + * + * @return Mage_Connect_Config + */ public function getConfig() { if (!$this->_config) { @@ -135,15 +152,19 @@ public function getConfig() } /** - * Retrieve object of single config and set it to Mage_Connect_Command - * - * @param bool $reload - * @return Mage_Connect_Singleconfig - */ + * Retrieve object of single config and set it to Mage_Connect_Command + * + * @param bool $reload + * @return Mage_Connect_Singleconfig + */ public function getSingleConfig($reload = false) { if(!$this->_sconfig || $reload) { - $this->_sconfig = new Mage_Connect_Singleconfig($this->getConfig()->magento_root . DIRECTORY_SEPARATOR . $this->getConfig()->downloader_path . DIRECTORY_SEPARATOR . Mage_Connect_Singleconfig::DEFAULT_SCONFIG_FILENAME); + $this->_sconfig = new Mage_Connect_Singleconfig( + $this->getConfig()->magento_root . DIRECTORY_SEPARATOR + . $this->getConfig()->downloader_path . DIRECTORY_SEPARATOR + . Mage_Connect_Singleconfig::DEFAULT_SCONFIG_FILENAME + ); } Mage_Connect_Command::setSconfig($this->_sconfig); return $this->_sconfig; @@ -151,10 +172,10 @@ public function getSingleConfig($reload = false) } /** - * Retrieve object of frontend and set it to Mage_Connect_Command - * - * @return Maged_Connect_Frontend - */ + * Retrieve object of frontend and set it to Mage_Connect_Command + * + * @return Maged_Connect_Frontend + */ public function getFrontend() { if (!$this->_frontend) { @@ -165,30 +186,30 @@ public function getFrontend() } /** - * Retrieve lof from frontend - * - * @return array - */ + * Retrieve lof from frontend + * + * @return array + */ public function getLog() { return $this->getFrontend()->getLog(); } /** - * Retrieve output from frontend - * - * @return array - */ + * Retrieve output from frontend + * + * @return array + */ public function getOutput() { return $this->getFrontend()->getOutput(); } /** - * Clean registry - * - * @return Maged_Connect - */ + * Clean registry + * + * @return Maged_Connect + */ public function cleanSconfig() { $this->getSingleConfig()->clear(); @@ -196,11 +217,11 @@ public function cleanSconfig() } /** - * Delete directory recursively - * - * @param string $path - * @return Maged_Connect - */ + * Delete directory recursively + * + * @param string $path + * @return Maged_Connect + */ public function delTree($path) { if (@is_dir($path)) { $entries = @scandir($path); @@ -217,13 +238,13 @@ public function delTree($path) { } /** - * Run commands from Mage_Connect_Command - * - * @param string $command - * @param array $options - * @param array $params - * @return - */ + * Run commands from Mage_Connect_Command + * + * @param string $command + * @param array $options + * @param array $params + * @return boolean|Mage_Connect_Error + */ public function run($command, $options=array(), $params=array()) { @set_time_limit(0); @@ -257,16 +278,20 @@ public function run($command, $options=array(), $params=array()) } } - public function setRemoteConfig($uri) #$host, $user, $password, $path='', $port=null) + /** + * Set remote Config by URI + * + * @param $uri + * @return Maged_Connect + */ + public function setRemoteConfig($uri) { - #$uri = 'ftp://' . $user . ':' . $password . '@' . $host . (is_numeric($port) ? ':' . $port : '') . '/' . trim($path, '/') . '/'; - //$this->run('config-set', array(), array('remote_config', $uri)); - //$this->run('config-set', array('ftp'=>$uri), array('remote_config', $uri)); $this->getConfig()->remote_config=$uri; return $this; } /** + * Show Errors * * @param array $errors Error messages * @return Maged_Connect @@ -277,7 +302,7 @@ public function showConnectErrors($errors) $run = new Maged_Model_Connect_Request(); if ($callback = $run->get('failure_callback')) { if (is_array($callback)) { - call_user_func_array($callback, array($result)); + call_user_func_array($callback, array($errors)); } else { echo $callback; } @@ -290,8 +315,9 @@ public function showConnectErrors($errors) /** * Run Mage_Connect_Command with html output console style * - * @param array|Maged_Model $runParams command, options, params, - * comment, success_callback, failure_callback + * @throws Maged_Exception + * @param array|string|Maged_Model $runParams command, options, params, comment, success_callback, failure_callback + * @return bool|Mage_Connect_Error */ public function runHtmlConsole($runParams) { @@ -318,6 +344,58 @@ public function runHtmlConsole($runParams) } if (!$run->get('no-header')) { + $this->_consoleHeader(); + } + echo htmlspecialchars($run->get('comment')).'
    '; + + if ($command = $run->get('command')) { + $result = $this->run($command, $run->get('options'), $run->get('params')); + + if ($this->getFrontend()->hasErrors()) { + echo "
    CONNECT ERROR: "; + foreach ($this->getFrontend()->getErrors(false) as $error) { + echo nl2br($error[1]); + echo '
    '; + } + } + echo ''; + } else { + $result = false; + } + if ($this->getFrontend()->getErrors() || !$run->get('no-footer')) { + //$this->_consoleFooter(); + $fe->setLogStream($oldLogStream); + } + return $result; + } + + /** + * Show HTML Console Header + * + * @return void + */ + protected function _consoleHeader() { + if (!$this->_consoleStarted) { ?> '; - }); - - // Wrap noscript elements - h = h.replace(/]+|)>([\s\S]*?)<\/noscript>/g, function(v, attribs, text) { - return ''; - }); - } - - h = h.replace(//g, ''); - - // This function processes the attributes in the HTML string to force boolean - // attributes to the attr="attr" format and convert style, src and href to _mce_ versions - function processTags(html) { - return html.replace(tagRegExp, function(match, elm_name, attrs, end) { - return '<' + elm_name + attrs.replace(attrRegExp, function(match, name, value, val2, val3) { - var mceValue; - - name = name.toLowerCase(); - value = value || val2 || val3 || ""; - - // Treat boolean attributes - if (boolAttrs[name]) { - // false or 0 is treated as a missing attribute - if (value === 'false' || value === '0') - return; - - return name + '="' + name + '"'; - } - - // Is attribute one that needs special treatment - if (mceAttribs[name] && attrs.indexOf('_mce_' + name) == -1) { - mceValue = t.decode(value); - - // Convert URLs to relative/absolute ones - if (s.url_converter && (name == "src" || name == "href")) - mceValue = s.url_converter.call(s.url_converter_scope || t, mceValue, name, elm_name); - - // Process styles lowercases them and compresses them - if (name == 'style') - mceValue = t.serializeStyle(t.parseStyle(mceValue), name); - - return name + '="' + value + '"' + ' _mce_' + name + '="' + t.encode(mceValue) + '"'; - } - - return match; - }) + end + '>'; - }); - }; - - h = processTags(h); - - // Restore script blocks - h = h.replace(/MCE_SCRIPT:([0-9]+)/g, function(val, idx) { - return codeBlocks[idx]; - }); - } - - return h; - }, - /** * Returns the outer HTML of an element. * * @method getOuterHTML - * @param {String/Element} e Element ID or element object to get outer HTML from. + * @param {String/Element} elm Element ID or element object to get outer HTML from. * @return {String} Outer HTML string. + * @example + * tinymce.DOM.getOuterHTML(editorElement); + * tinyMCE.activeEditor.getOuterHTML(tinyMCE.activeEditor.getBody()); */ - getOuterHTML : function(e) { - var d; + getOuterHTML : function(elm) { + var doc, self = this; - e = this.get(e); + elm = self.get(elm); - if (!e) + if (!elm) return null; - if (e.outerHTML !== undefined) - return e.outerHTML; + if (elm.nodeType === 1 && self.hasOuterHTML) + return elm.outerHTML; - d = (e.ownerDocument || this.doc).createElement("body"); - d.appendChild(e.cloneNode(true)); + doc = (elm.ownerDocument || self.doc).createElement("body"); + doc.appendChild(elm.cloneNode(true)); - return d.innerHTML; + return doc.innerHTML; }, /** @@ -1435,6 +1200,12 @@ * @param {Element/String/Array} e DOM element, element id string or array of elements/ids to set outer HTML on. * @param {Object} h HTML code to set as outer value for the element. * @param {Document} d Optional document scope to use in this process defaults to the document of the DOM class. + * @example + * // Sets the outer HTML of all paragraphs in the active editor + * tinyMCE.activeEditor.dom.setOuterHTML(tinyMCE.activeEditor.dom.select('p'), '
    some html
    '); + * + * // Sets the outer HTML of a element by id in the document + * tinyMCE.DOM.setOuterHTML('mydiv', '
    some html
    '); */ setOuterHTML : function(e, h, d) { var t = this; @@ -1485,41 +1256,16 @@ * @param {String} s String to decode entities on. * @return {String} Entity decoded string. */ - decode : function(s) { - var e, n, v; - - // Look for entities to decode - if (/&[\w#]+;/.test(s)) { - // Decode the entities using a div element not super efficient but less code - e = this.doc.createElement("div"); - e.innerHTML = s; - n = e.firstChild; - v = ''; - - if (n) { - do { - v += n.nodeValue; - } while (n = n.nextSibling); - } - - return v || s; - } - - return s; - }, + decode : Entities.decode, /** * Entity encodes a string, encodes the most common entities <>"& into entities. * * @method encode - * @param {String} s String to encode with entities. + * @param {String} text String to encode with entities. * @return {String} Entity encoded string. */ - encode : function(str) { - return ('' + str).replace(encodeCharsRe, function(chr) { - return encodedChars[chr]; - }); - }, + encode : Entities.encodeAllRaw, /** * Inserts a element after the reference element. @@ -1551,16 +1297,17 @@ * Returns true/false if the specified element is a block element or not. * * @method isBlock - * @param {Node} n Element/Node to check. + * @param {Node/String} node Element/Node to check. * @return {Boolean} True/False state if the node is a block element or not. */ - isBlock : function(n) { - if (n.nodeType && n.nodeType !== 1) - return false; + isBlock : function(node) { + var type = node.nodeType; - n = n.nodeName || n; + // If it's a node then check the type and use the nodeName + if (type) + return !!(type === 1 && blockElementsMap[node.nodeName]); - return blockRe.test(n); + return !!blockElementsMap[node]; }, /** @@ -1704,7 +1451,7 @@ // Remove everything but class name ov = v; - v = v.replace(/.*\.([a-z0-9_\-]+).*/i, '$1'); + v = tinymce._replace(/.*\.([a-z0-9_\-]+).*/i, '$1', v); // Filter classes if (f && !(v = f(v, ov))) @@ -1812,6 +1559,62 @@ return n.attributes; }, + /** + * Returns true/false if the specified node is to be considered empty or not. + * + * @example + * tinymce.DOM.isEmpty(node, {img : true}); + * @method isEmpty + * @param {Object} elements Optional name/value object with elements that are automatically treated as non empty elements. + * @return {Boolean} true/false if the node is empty or not. + */ + isEmpty : function(node, elements) { + var self = this, i, attributes, type, walker, name, parentNode; + + node = node.firstChild; + if (node) { + walker = new tinymce.dom.TreeWalker(node); + elements = elements || self.schema ? self.schema.getNonEmptyElements() : null; + + do { + type = node.nodeType; + + if (type === 1) { + // Ignore bogus elements + if (node.getAttribute('data-mce-bogus')) + continue; + + // Keep empty elements like + name = node.nodeName.toLowerCase(); + if (elements && elements[name]) { + // Ignore single BR elements in blocks like


    + parentNode = node.parentNode; + if (name === 'br' && self.isBlock(parentNode) && parentNode.firstChild === node && parentNode.lastChild === node) { + continue; + } + + return false; + } + + // Keep elements with data-bookmark attributes or name attribute like + attributes = self.getAttribs(node); + i = node.attributes.length; + while (i--) { + name = node.attributes[i].nodeName; + if (name === "name" || name === 'data-mce-bookmark') + return false; + } + } + + // Keep non whitespace text nodes + if ((type === 3 && !whiteSpaceRegExp.test(node.nodeValue))) + return false; + } while (node = walker.next()); + } + + return true; + }, + /** * Destroys all internal references to the DOM to solve IE leak issues. * @@ -1836,6 +1639,9 @@ * * @method createRng * @return {DOMRange} DOM Range object. + * @example + * var rng = tinymce.DOM.createRng(); + * alert(rng.startContainer + "," + rng.startOffset); */ createRng : function() { var d = this.doc; @@ -1862,7 +1668,6 @@ if (nodeType == lastNodeType || !node.nodeValue.length) continue; } - idx++; lastNodeType = nodeType; } @@ -1894,23 +1699,24 @@ // this function will then trim of empty edges and produce: //

    text 1

    CHOP

    text 2

    function trim(node) { - var i, children = node.childNodes; + var i, children = node.childNodes, type = node.nodeType; - if (node.nodeType == 1 && node.getAttribute('_mce_type') == 'bookmark') + if (type == 1 && node.getAttribute('data-mce-type') == 'bookmark') return; for (i = children.length - 1; i >= 0; i--) trim(children[i]); - if (node.nodeType != 9) { + if (type != 9) { // Keep non whitespace text nodes - if (node.nodeType == 3 && node.nodeValue.length > 0) - return; - - if (node.nodeType == 1) { + if (type == 3 && node.nodeValue.length > 0) { + // If parent element isn't a block or there isn't any useful contents for example "

    " + if (!t.isBlock(node.parentNode) || tinymce.trim(node.nodeValue).length > 0) + return; + } else if (type == 1) { // If the only child is a bookmark then move it up children = node.childNodes; - if (children.length == 1 && children[0] && children[0].nodeType == 1 && children[0].getAttribute('_mce_type') == 'bookmark') + if (children.length == 1 && children[0] && children[0].nodeType == 1 && children[0].getAttribute('data-mce-type') == 'bookmark') node.parentNode.insertBefore(children[0], node); // Keep non empty elements or img, hr etc @@ -2062,6 +1868,9 @@ * @property DOM * @member tinymce * @type tinymce.dom.DOMUtils + * @example + * // Example of how to add a class to some element by id + * tinymce.DOM.addClass('someid', 'someclass'); */ tinymce.DOM = new tinymce.dom.DOMUtils(document, {process_html : 0}); })(tinymce); diff --git a/js/tiny_mce/classes/dom/Element.js b/js/tiny_mce/classes/dom/Element.js index 1e6d6753fa..a1a8786254 100644 --- a/js/tiny_mce/classes/dom/Element.js +++ b/js/tiny_mce/classes/dom/Element.js @@ -15,6 +15,12 @@ * since it's bound to an element. * * @class tinymce.dom.Element + * @example + * // Creates an basic element for an existing element + * var elm = new tinymce.dom.Element('someid'); + * + * elm.setStyle('background-color', 'red'); + * elm.moveTo(10, 10); */ /** diff --git a/js/tiny_mce/classes/dom/EventUtils.js b/js/tiny_mce/classes/dom/EventUtils.js index 06746e7c24..50851ddcb8 100644 --- a/js/tiny_mce/classes/dom/EventUtils.js +++ b/js/tiny_mce/classes/dom/EventUtils.js @@ -39,6 +39,11 @@ * @param {function} f Function to execute when the event occurs. * @param {Object} s Optional scope to execute the function in. * @return {function} Function callback handler the same as the one passed in. + * @example + * // Adds a click handler to the current document + * tinymce.dom.Event.add(document, 'click', function(e) { + * console.debug(e.target); + * }); */ add : function(o, n, f, s) { var cb, t = this, el = t.events, r; @@ -129,6 +134,14 @@ * @param {String} n Event handler name like for example: "click" * @param {function} f Function to remove. * @return {bool/Array} Bool state if true if the handler was removed or an array with states if multiple elements where passed in. + * @example + * // Adds a click handler to the current document + * var func = tinymce.dom.Event.add(document, 'click', function(e) { + * console.debug(e.target); + * }); + * + * // Removes the click handler from the document + * tinymce.dom.Event.remove(document, 'click', func); */ remove : function(o, n, f) { var t = this, a = t.events, s = false, r; @@ -164,6 +177,11 @@ * * @method clear * @param {Object} o DOM element or object to remove all events from. + * @example + * // Cancels all mousedown events in the active editor + * tinyMCE.activeEditor.onMouseDown.add(function(ed, e) { + * return tinymce.dom.Event.cancel(e); + * }); */ clear : function(o) { var t = this, a = t.events, i, e; @@ -312,7 +330,7 @@ return; try { - // If IE is used, use the trick by Diego Perini + // If IE is used, use the trick by Diego Perini licensed under MIT by request to the author. // http://javascript.nwbox.com/IEContentLoaded/ doc.documentElement.doScroll("left"); } catch (ex) { @@ -335,7 +353,7 @@ }, _stoppers : { - preventDefault : function() { + preventDefault : function() { this.returnValue = false; }, diff --git a/js/tiny_mce/classes/dom/Range.js b/js/tiny_mce/classes/dom/Range.js index 634459d507..54b847221f 100644 --- a/js/tiny_mce/classes/dom/Range.js +++ b/js/tiny_mce/classes/dom/Range.js @@ -106,23 +106,24 @@ }; function compareBoundaryPoints(h, r) { - var sc = t[START_CONTAINER], so = t[START_OFFSET], ec = t[END_CONTAINER], eo = t[END_OFFSET]; + var sc = t[START_CONTAINER], so = t[START_OFFSET], ec = t[END_CONTAINER], eo = t[END_OFFSET], + rsc = r.startContainer, rso = r.startOffset, rec = r.endContainer, reo = r.endOffset; // Check START_TO_START if (h === 0) - return _compareBoundaryPoints(sc, so, sc, so); - + return _compareBoundaryPoints(sc, so, rsc, rso); + // Check START_TO_END if (h === 1) - return _compareBoundaryPoints(sc, so, ec, eo); - + return _compareBoundaryPoints(ec, eo, rsc, rso); + // Check END_TO_END if (h === 2) - return _compareBoundaryPoints(ec, eo, ec, eo); - + return _compareBoundaryPoints(ec, eo, rec, reo); + // Check END_TO_START - if (h === 3) - return _compareBoundaryPoints(ec, eo, sc, so); + if (h === 3) + return _compareBoundaryPoints(sc, so, rec, reo); }; function deleteContents() { @@ -214,7 +215,7 @@ function _compareBoundaryPoints(containerA, offsetA, containerB, offsetB) { var c, offsetC, n, cmnRoot, childA, childB; - + // In the first case the boundary-points have the same container. A is before B // if its offset is less than the offset of B, A is equal to B if its offset is // equal to the offset of B, and A is after B if its offset is greater than the @@ -500,7 +501,7 @@ frag.appendChild(n); startIdx = nodeIndex(startAncestor); - ++startIdx; // Because we already traversed it.... + ++startIdx; // Because we already traversed it cnt = t[END_OFFSET] - startIdx; n = startAncestor.nextSibling; diff --git a/js/tiny_mce/classes/dom/RangeUtils.js b/js/tiny_mce/classes/dom/RangeUtils.js index 6f96886da8..051c395c04 100644 --- a/js/tiny_mce/classes/dom/RangeUtils.js +++ b/js/tiny_mce/classes/dom/RangeUtils.js @@ -37,6 +37,31 @@ return; } + /** + * Excludes start/end text node if they are out side the range + * + * @private + * @param {Array} nodes Nodes to exclude items from. + * @return {Array} Array with nodes excluding the start/end container if needed. + */ + function exclude(nodes) { + var node; + + // First node is excluded + node = nodes[0]; + if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) { + nodes.splice(0, 1); + } + + // Last node is excluded + node = nodes[nodes.length - 1]; + if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) { + nodes.splice(nodes.length - 1, 1); + } + + return nodes; + }; + /** * Collects siblings * @@ -82,7 +107,7 @@ if (!next) siblings.reverse(); - callback(siblings); + callback(exclude(siblings)); } } }; @@ -93,30 +118,30 @@ // If index based end position then resolve it if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) - endContainer = endContainer.childNodes[Math.min(startOffset == endOffset ? endOffset : endOffset - 1, endContainer.childNodes.length - 1)]; - - // Find common ancestor and end points - ancestor = dom.findCommonAncestor(startContainer, endContainer); + endContainer = endContainer.childNodes[Math.min(endOffset - 1, endContainer.childNodes.length - 1)]; // Same container if (startContainer == endContainer) - return callback([startContainer]); + return callback(exclude([startContainer])); + // Find common ancestor and end points + ancestor = dom.findCommonAncestor(startContainer, endContainer); + // Process left side for (node = startContainer; node; node = node.parentNode) { - if (node == endContainer) + if (node === endContainer) return walkBoundary(startContainer, ancestor, true); - if (node == ancestor) + if (node === ancestor) break; } // Process right side for (node = endContainer; node; node = node.parentNode) { - if (node == startContainer) + if (node === startContainer) return walkBoundary(endContainer, ancestor); - if (node == ancestor) + if (node === ancestor) break; } @@ -135,7 +160,7 @@ ); if (siblings.length) - callback(siblings); + callback(exclude(siblings)); // Walk right leaf walkBoundary(endContainer, endPoint); @@ -147,42 +172,40 @@ * @param {Range/RangeObject} rng Range to split. * @return {Object} Range position object. */ -/* this.split = function(rng) { + this.split = function(rng) { var startContainer = rng.startContainer, startOffset = rng.startOffset, endContainer = rng.endContainer, endOffset = rng.endOffset; function splitText(node, offset) { - if (offset == node.nodeValue.length) - node.appendData(INVISIBLE_CHAR); - - node = node.splitText(offset); - - if (node.nodeValue === INVISIBLE_CHAR) - node.nodeValue = ''; - - return node; + return node.splitText(offset); }; // Handle single text node - if (startContainer == endContainer) { - if (startContainer.nodeType == 3) { - if (startOffset != 0) - startContainer = endContainer = splitText(startContainer, startOffset); - - if (endOffset - startOffset != startContainer.nodeValue.length) - splitText(startContainer, endOffset - startOffset); + if (startContainer == endContainer && startContainer.nodeType == 3) { + if (startOffset > 0 && startOffset < startContainer.nodeValue.length) { + endContainer = splitText(startContainer, startOffset); + startContainer = endContainer.previousSibling; + + if (endOffset > startOffset) { + endOffset = endOffset - startOffset; + startContainer = endContainer = splitText(endContainer, endOffset).previousSibling; + endOffset = endContainer.nodeValue.length; + startOffset = 0; + } else { + endOffset = 0; + } } } else { // Split startContainer text node if needed - if (startContainer.nodeType == 3 && startOffset != 0) { + if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) { startContainer = splitText(startContainer, startOffset); startOffset = 0; } // Split endContainer text node if needed - if (endContainer.nodeType == 3 && endOffset != endContainer.nodeValue.length) { + if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) { endContainer = splitText(endContainer, endOffset).previousSibling; endOffset = endContainer.nodeValue.length; } @@ -195,7 +218,7 @@ endOffset : endOffset }; }; -*/ + }; /** diff --git a/js/tiny_mce/classes/dom/Schema.js b/js/tiny_mce/classes/dom/Schema.js deleted file mode 100644 index 4148d7e3c5..0000000000 --- a/js/tiny_mce/classes/dom/Schema.js +++ /dev/null @@ -1,185 +0,0 @@ -/** - * Schema.js - * - * Copyright 2009, Moxiecode Systems AB - * Released under LGPL License. - * - * License: http://tinymce.moxiecode.com/license - * Contributing: http://tinymce.moxiecode.com/contributing - */ - -(function() { - var transitional = {}; - - /** - * Unpacks the specified lookup and string data it will also parse it into an object - * map with sub object for it's children. This will later also include the attributes. - */ - function unpack(lookup, data) { - var key; - - function replace(value) { - return value.replace(/[A-Z]+/g, function(key) { - return replace(lookup[key]); - }); - }; - - // Unpack lookup - for (key in lookup) { - if (lookup.hasOwnProperty(key)) - lookup[key] = replace(lookup[key]); - } - - // Unpack and parse data into object map - replace(data).replace(/#/g, '#text').replace(/(\w+)\[([^\]]+)\]/g, function(str, name, children) { - var i, map = {}; - - children = children.split(/\|/); - - for (i = children.length - 1; i >= 0; i--) - map[children[i]] = 1; - - transitional[name] = map; - }); - }; - - // This is the XHTML 1.0 transitional elements with it's children packed to reduce it's size - // we will later include the attributes here and use it as a default for valid elements but it - // requires us to rewrite the serializer engine - unpack({ - Z : '#|H|K|N|O|P', - Y : '#|X|form|R|Q', - X : 'p|T|div|U|W|isindex|fieldset|table', - W : 'pre|hr|blockquote|address|center|noframes', - U : 'ul|ol|dl|menu|dir', - ZC : '#|p|Y|div|U|W|table|br|span|bdo|object|applet|img|map|K|N|Q', - T : 'h1|h2|h3|h4|h5|h6', - ZB : '#|X|S|Q', - S : 'R|P', - ZA : '#|a|G|J|M|O|P', - R : '#|a|H|K|N|O', - Q : 'noscript|P', - P : 'ins|del|script', - O : 'input|select|textarea|label|button', - N : 'M|L', - M : 'em|strong|dfn|code|q|samp|kbd|var|cite|abbr|acronym', - L : 'sub|sup', - K : 'J|I', - J : 'tt|i|b|u|s|strike', - I : 'big|small|font|basefont', - H : 'G|F', - G : 'br|span|bdo', - F : 'object|applet|img|map|iframe' - }, 'script[]' + - 'style[]' + - 'object[#|param|X|form|a|H|K|N|O|Q]' + - 'param[]' + - 'p[S]' + - 'a[Z]' + - 'br[]' + - 'span[S]' + - 'bdo[S]' + - 'applet[#|param|X|form|a|H|K|N|O|Q]' + - 'h1[S]' + - 'img[]' + - 'map[X|form|Q|area]' + - 'h2[S]' + - 'iframe[#|X|form|a|H|K|N|O|Q]' + - 'h3[S]' + - 'tt[S]' + - 'i[S]' + - 'b[S]' + - 'u[S]' + - 's[S]' + - 'strike[S]' + - 'big[S]' + - 'small[S]' + - 'font[S]' + - 'basefont[]' + - 'em[S]' + - 'strong[S]' + - 'dfn[S]' + - 'code[S]' + - 'q[S]' + - 'samp[S]' + - 'kbd[S]' + - 'var[S]' + - 'cite[S]' + - 'abbr[S]' + - 'acronym[S]' + - 'sub[S]' + - 'sup[S]' + - 'input[]' + - 'select[optgroup|option]' + - 'optgroup[option]' + - 'option[]' + - 'textarea[]' + - 'label[S]' + - 'button[#|p|T|div|U|W|table|G|object|applet|img|map|K|N|Q]' + - 'h4[S]' + - 'ins[#|X|form|a|H|K|N|O|Q]' + - 'h5[S]' + - 'del[#|X|form|a|H|K|N|O|Q]' + - 'h6[S]' + - 'div[#|X|form|a|H|K|N|O|Q]' + - 'ul[li]' + - 'li[#|X|form|a|H|K|N|O|Q]' + - 'ol[li]' + - 'dl[dt|dd]' + - 'dt[S]' + - 'dd[#|X|form|a|H|K|N|O|Q]' + - 'menu[li]' + - 'dir[li]' + - 'pre[ZA]' + - 'hr[]' + - 'blockquote[#|X|form|a|H|K|N|O|Q]' + - 'address[S|p]' + - 'center[#|X|form|a|H|K|N|O|Q]' + - 'noframes[#|X|form|a|H|K|N|O|Q]' + - 'isindex[]' + - 'fieldset[#|legend|X|form|a|H|K|N|O|Q]' + - 'legend[S]' + - 'table[caption|col|colgroup|thead|tfoot|tbody|tr]' + - 'caption[S]' + - 'col[]' + - 'colgroup[col]' + - 'thead[tr]' + - 'tr[th|td]' + - 'th[#|X|form|a|H|K|N|O|Q]' + - 'form[#|X|a|H|K|N|O|Q]' + - 'noscript[#|X|form|a|H|K|N|O|Q]' + - 'td[#|X|form|a|H|K|N|O|Q]' + - 'tfoot[tr]' + - 'tbody[tr]' + - 'area[]' + - 'base[]' + - 'body[#|X|form|a|H|K|N|O|Q]' - ); - - /** - * Schema validator class. - * - * @class tinymce.dom.Schema - * @example - * if (tinymce.activeEditor.schema.isValid('p', 'span')) - * alert('span is valid child of p.'); - */ - tinymce.dom.Schema = function() { - var t = this, elements = transitional; - - /** - * Returns true/false if the specified element and optionally it's child is valid or not - * according to the XHTML transitional DTD. - * - * @method isValid - * @param {String} name Element name to check for. - * @param {String} child_name Element child name to check for. - * @return {boolean} true/false if the element is valid or not. - */ - t.isValid = function(name, child_name) { - var element = elements[name]; - - return !!(element && (!child_name || element[child_name])); - }; - }; -})(); \ No newline at end of file diff --git a/js/tiny_mce/classes/dom/ScriptLoader.js b/js/tiny_mce/classes/dom/ScriptLoader.js index 70bef4a500..b79bab1375 100644 --- a/js/tiny_mce/classes/dom/ScriptLoader.js +++ b/js/tiny_mce/classes/dom/ScriptLoader.js @@ -9,6 +9,30 @@ */ (function(tinymce) { + /** + * This class handles asynchronous/synchronous loading of JavaScript files it will execute callbacks when various items gets loaded. This class is useful to load external JavaScript files. + * + * @class tinymce.dom.ScriptLoader + * @example + * // Load a script from a specific URL using the global script loader + * tinymce.ScriptLoader.load('somescript.js'); + * + * // Load a script using a unique instance of the script loader + * var scriptLoader = new tinymce.dom.ScriptLoader(); + * + * scriptLoader.load('somescript.js'); + * + * // Load multiple scripts + * var scriptLoader = new tinymce.dom.ScriptLoader(); + * + * scriptLoader.add('somescript1.js'); + * scriptLoader.add('somescript2.js'); + * scriptLoader.add('somescript3.js'); + * + * scriptLoader.loadQueue(function() { + * alert('All scripts are now loaded.'); + * }); + */ tinymce.dom.ScriptLoader = function(settings) { var QUEUED = 0, LOADING = 1, @@ -40,6 +64,17 @@ callback(); }; + + function error() { + // Report the error so it's easier for people to spot loading errors + if (typeof(console) !== "undefined" && console.log) + console.log("Failed to load: " + url); + + // We can't mark it as done if there is a load error since + // A) We don't want to produce 404 errors on the server and + // B) the onerror event won't fire on all browsers. + // done(); + }; id = dom.uniqueId(); @@ -49,7 +84,7 @@ // If script is from same domain and we // use IE 6 then use XHR since it's more reliable - if (uri.host == loc.hostname && uri.port == loc.port && (uri.protocol + ':') == loc.protocol) { + if (uri.host == loc.hostname && uri.port == loc.port && (uri.protocol + ':') == loc.protocol && uri.protocol.toLowerCase() != 'file') { tinymce.util.XHR.send({ url : tinymce._addVer(uri.getURI()), success : function(content) { @@ -64,7 +99,9 @@ dom.remove(script); done(); - } + }, + + error : error }); return; @@ -78,17 +115,26 @@ src : tinymce._addVer(url) }); - // Add onload and readystate listeners - elm.onload = done; - elm.onreadystatechange = function() { - var state = elm.readyState; - - // Loaded state is passed on IE 6 however there - // are known issues with this method but we can't use - // XHR in a cross domain loading - if (state == 'complete' || state == 'loaded') - done(); - }; + // Add onload listener for non IE browsers since IE9 + // fires onload event before the script is parsed and executed + if (!tinymce.isIE) + elm.onload = done; + + // Add onerror event will get fired on some browsers but not all of them + elm.onerror = error; + + // Opera 9.60 doesn't seem to fire the onreadystate event at correctly + if (!tinymce.isOpera) { + elm.onreadystatechange = function() { + var state = elm.readyState; + + // Loaded state is passed on IE 6 however there + // are known issues with this method but we can't use + // XHR in a cross domain loading + if (state == 'complete' || state == 'loaded') + done(); + }; + } // Most browsers support this feature so we report errors // for those at least to help users track their missing plugins etc diff --git a/js/tiny_mce/classes/dom/Selection.js b/js/tiny_mce/classes/dom/Selection.js index fd1779c5ec..3041e6672a 100644 --- a/js/tiny_mce/classes/dom/Selection.js +++ b/js/tiny_mce/classes/dom/Selection.js @@ -1,766 +1,1103 @@ -/** - * Selection.js - * - * Copyright 2009, Moxiecode Systems AB - * Released under LGPL License. - * - * License: http://tinymce.moxiecode.com/license - * Contributing: http://tinymce.moxiecode.com/contributing - */ - -(function(tinymce) { - function trimNl(s) { - return s.replace(/[\n\r]+/g, ''); - }; - - // Shorten names - var is = tinymce.is, isIE = tinymce.isIE, each = tinymce.each; - - /** - * This class handles text and control selection it's an crossbrowser utility class. - * Consult the TinyMCE Wiki API for more details and examples on how to use this class. - * @class tinymce.dom.Selection - */ - tinymce.create('tinymce.dom.Selection', { - /** - * Constructs a new selection instance. - * - * @constructor - * @method Selection - * @param {tinymce.dom.DOMUtils} dom DOMUtils object reference. - * @param {Window} win Window to bind the selection object to. - * @param {tinymce.dom.Serializer} serializer DOM serialization class to use for getContent. - */ - Selection : function(dom, win, serializer) { - var t = this; - - t.dom = dom; - t.win = win; - t.serializer = serializer; - - // Add events - each([ - 'onBeforeSetContent', - 'onBeforeGetContent', - 'onSetContent', - 'onGetContent' - ], function(e) { - t[e] = new tinymce.util.Dispatcher(t); - }); - - // No W3C Range support - if (!t.win.getSelection) - t.tridentSel = new tinymce.dom.TridentSelection(t); - - // Prevent leaks - tinymce.addUnload(t.destroy, t); - }, - - /** - * Returns the selected contents using the DOM serializer passed in to this class. - * - * @method getContent - * @param {Object} s Optional settings class with for example output format text or html. - * @return {String} Selected contents in for example HTML format. - */ - getContent : function(s) { - var t = this, r = t.getRng(), e = t.dom.create("body"), se = t.getSel(), wb, wa, n; - - s = s || {}; - wb = wa = ''; - s.get = true; - s.format = s.format || 'html'; - t.onBeforeGetContent.dispatch(t, s); - - if (s.format == 'text') - return t.isCollapsed() ? '' : (r.text || (se.toString ? se.toString() : '')); - - if (r.cloneContents) { - n = r.cloneContents(); - - if (n) - e.appendChild(n); - } else if (is(r.item) || is(r.htmlText)) - e.innerHTML = r.item ? r.item(0).outerHTML : r.htmlText; - else - e.innerHTML = r.toString(); - - // Keep whitespace before and after - if (/^\s/.test(e.innerHTML)) - wb = ' '; - - if (/\s+$/.test(e.innerHTML)) - wa = ' '; - - s.getInner = true; - - s.content = t.isCollapsed() ? '' : wb + t.serializer.serialize(e, s) + wa; - t.onGetContent.dispatch(t, s); - - return s.content; - }, - - /** - * Sets the current selection to the specified content. If any contents is selected it will be replaced - * with the contents passed in to this function. If there is no selection the contents will be inserted - * where the caret is placed in the editor/page. - * - * @method setContent - * @param {String} h HTML contents to set could also be other formats depending on settings. - * @param {Object} s Optional settings object with for example data format. - */ - setContent : function(h, s) { - var t = this, r = t.getRng(), c, d = t.win.document; - - s = s || {format : 'html'}; - s.set = true; - h = s.content = t.dom.processHTML(h); - - // Dispatch before set content event - t.onBeforeSetContent.dispatch(t, s); - h = s.content; - - if (r.insertNode) { - // Make caret marker since insertNode places the caret in the beginning of text after insert - h += '_'; - - // Delete and insert new node - - if (r.startContainer == d && r.endContainer == d) { - // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents - d.body.innerHTML = h; - } else { - r.deleteContents(); - if (d.body.childNodes.length == 0) { - d.body.innerHTML = h; - } else { - r.insertNode(r.createContextualFragment(h)); - } - } - - // Move to caret marker - c = t.dom.get('__caret'); - // Make sure we wrap it compleatly, Opera fails with a simple select call - r = d.createRange(); - r.setStartBefore(c); - r.setEndBefore(c); - t.setRng(r); - - // Remove the caret position - t.dom.remove('__caret'); - } else { - if (r.item) { - // Delete content and get caret text selection - d.execCommand('Delete', false, null); - r = t.getRng(); - } - - r.pasteHTML(h); - } - - // Dispatch set content event - t.onSetContent.dispatch(t, s); - }, - - /** - * Returns the start element of a selection range. If the start is in a text - * node the parent element will be returned. - * - * @method getStart - * @return {Element} Start element of selection range. - */ - getStart : function() { - var rng = this.getRng(), startElement, parentElement, checkRng, node; - - if (rng.duplicate || rng.item) { - // Control selection, return first item - if (rng.item) - return rng.item(0); - - // Get start element - checkRng = rng.duplicate(); - checkRng.collapse(1); - startElement = checkRng.parentElement(); - - // Check if range parent is inside the start element, then return the inner parent element - // This will fix issues when a single element is selected, IE would otherwise return the wrong start element - parentElement = node = rng.parentElement(); - while (node = node.parentNode) { - if (node == startElement) { - startElement = parentElement; - break; - } - } - - // If start element is body element try to move to the first child if it exists - if (startElement && startElement.nodeName == 'BODY') - return startElement.firstChild || startElement; - - return startElement; - } else { - startElement = rng.startContainer; - - if (startElement.nodeType == 1 && startElement.hasChildNodes()) - startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)]; - - if (startElement && startElement.nodeType == 3) - return startElement.parentNode; - - return startElement; - } - }, - - /** - * Returns the end element of a selection range. If the end is in a text - * node the parent element will be returned. - * - * @method getEnd - * @return {Element} End element of selection range. - */ - getEnd : function() { - var t = this, r = t.getRng(), e, eo; - - if (r.duplicate || r.item) { - if (r.item) - return r.item(0); - - r = r.duplicate(); - r.collapse(0); - e = r.parentElement(); - - if (e && e.nodeName == 'BODY') - return e.lastChild || e; - - return e; - } else { - e = r.endContainer; - eo = r.endOffset; - - if (e.nodeType == 1 && e.hasChildNodes()) - e = e.childNodes[eo > 0 ? eo - 1 : eo]; - - if (e && e.nodeType == 3) - return e.parentNode; - - return e; - } - }, - - /** - * Returns a bookmark location for the current selection. This bookmark object - * can then be used to restore the selection after some content modification to the document. - * - * @method getBookmark - * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex. - * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization. - * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection. - */ - getBookmark : function(type, normalized) { - var t = this, dom = t.dom, rng, rng2, id, collapsed, name, element, index, chr = '\uFEFF', styles; - - function findIndex(name, element) { - var index = 0; - - each(dom.select(name), function(node, i) { - if (node == element) - index = i; - }); - - return index; - }; - - if (type == 2) { - function getLocation() { - var rng = t.getRng(true), root = dom.getRoot(), bookmark = {}; - - function getPoint(rng, start) { - var container = rng[start ? 'startContainer' : 'endContainer'], - offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0; - - if (container.nodeType == 3) { - if (normalized) { - for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling) - offset += node.nodeValue.length; - } - - point.push(offset); - } else { - childNodes = container.childNodes; - - if (offset >= childNodes.length && childNodes.length) { - after = 1; - offset = Math.max(0, childNodes.length - 1); - } - - point.push(t.dom.nodeIndex(childNodes[offset], normalized) + after); - } - - for (; container && container != root; container = container.parentNode) - point.push(t.dom.nodeIndex(container, normalized)); - - return point; - }; - - bookmark.start = getPoint(rng, true); - - if (!t.isCollapsed()) - bookmark.end = getPoint(rng); - - return bookmark; - }; - - return getLocation(); - } - - // Handle simple range - if (type) - return {rng : t.getRng()}; - - rng = t.getRng(); - id = dom.uniqueId(); - collapsed = tinyMCE.activeEditor.selection.isCollapsed(); - styles = 'overflow:hidden;line-height:0px'; - - // Explorer method - if (rng.duplicate || rng.item) { - // Text selection - if (!rng.item) { - rng2 = rng.duplicate(); - - // Insert start marker - rng.collapse(); - rng.pasteHTML('' + chr + ''); - - // Insert end marker - if (!collapsed) { - rng2.collapse(false); - rng2.pasteHTML('' + chr + ''); - } - } else { - // Control selection - element = rng.item(0); - name = element.nodeName; - - return {name : name, index : findIndex(name, element)}; - } - } else { - element = t.getNode(); - name = element.nodeName; - if (name == 'IMG') - return {name : name, index : findIndex(name, element)}; - - // W3C method - rng2 = rng.cloneRange(); - - // Insert end marker - if (!collapsed) { - rng2.collapse(false); - rng2.insertNode(dom.create('span', {_mce_type : "bookmark", id : id + '_end', style : styles}, chr)); - } - - rng.collapse(true); - rng.insertNode(dom.create('span', {_mce_type : "bookmark", id : id + '_start', style : styles}, chr)); - } - - t.moveToBookmark({id : id, keep : 1}); - - return {id : id}; - }, - - /** - * Restores the selection to the specified bookmark. - * - * @method moveToBookmark - * @param {Object} bookmark Bookmark to restore selection from. - * @return {Boolean} true/false if it was successful or not. - */ - moveToBookmark : function(bookmark) { - var t = this, dom = t.dom, marker1, marker2, rng, root, startContainer, endContainer, startOffset, endOffset; - - // Clear selection cache - if (t.tridentSel) - t.tridentSel.destroy(); - - if (bookmark) { - if (bookmark.start) { - rng = dom.createRng(); - root = dom.getRoot(); - - function setEndPoint(start) { - var point = bookmark[start ? 'start' : 'end'], i, node, offset, children; - - if (point) { - // Find container node - for (node = root, i = point.length - 1; i >= 1; i--) { - children = node.childNodes; - - if (children.length) - node = children[point[i]]; - } - - // Set offset within container node - if (start) - rng.setStart(node, point[0]); - else - rng.setEnd(node, point[0]); - } - }; - - setEndPoint(true); - setEndPoint(); - - t.setRng(rng); - } else if (bookmark.id) { - function restoreEndPoint(suffix) { - var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep; - - if (marker) { - node = marker.parentNode; - - if (suffix == 'start') { - if (!keep) { - idx = dom.nodeIndex(marker); - } else { - node = marker.firstChild; - idx = 1; - } - - startContainer = endContainer = node; - startOffset = endOffset = idx; - } else { - if (!keep) { - idx = dom.nodeIndex(marker); - } else { - node = marker.firstChild; - idx = 1; - } - - endContainer = node; - endOffset = idx; - } - - if (!keep) { - prev = marker.previousSibling; - next = marker.nextSibling; - - // Remove all marker text nodes - each(tinymce.grep(marker.childNodes), function(node) { - if (node.nodeType == 3) - node.nodeValue = node.nodeValue.replace(/\uFEFF/g, ''); - }); - - // Remove marker but keep children if for example contents where inserted into the marker - // Also remove duplicated instances of the marker for example by a split operation or by WebKit auto split on paste feature - while (marker = dom.get(bookmark.id + '_' + suffix)) - dom.remove(marker, 1); - - // If siblings are text nodes then merge them - if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3) { - idx = prev.nodeValue.length; - prev.appendData(next.nodeValue); - dom.remove(next); - - if (suffix == 'start') { - startContainer = endContainer = prev; - startOffset = endOffset = idx; - } else { - endContainer = prev; - endOffset = idx; - } - } - } - } - }; - - function addBogus(node) { - // Adds a bogus BR element for empty block elements - // on non IE browsers just to have a place to put the caret - if (!isIE && dom.isBlock(node) && !node.innerHTML) - node.innerHTML = '
    '; - - return node; - }; - - // Restore start/end points - restoreEndPoint('start'); - restoreEndPoint('end'); - - rng = dom.createRng(); - rng.setStart(addBogus(startContainer), startOffset); - rng.setEnd(addBogus(endContainer), endOffset); - t.setRng(rng); - } else if (bookmark.name) { - t.select(dom.select(bookmark.name)[bookmark.index]); - } else if (bookmark.rng) - t.setRng(bookmark.rng); - } - }, - - /** - * Selects the specified element. This will place the start and end of the selection range around the element. - * - * @method select - * @param {Element} node HMTL DOM element to select. - * @param {Boolean} content Optional bool state if the contents should be selected or not on non IE browser. - * @return {Element} Selected element the same element as the one that got passed in. - */ - select : function(node, content) { - var t = this, dom = t.dom, rng = dom.createRng(), idx; - - idx = dom.nodeIndex(node); - rng.setStart(node.parentNode, idx); - rng.setEnd(node.parentNode, idx + 1); - - // Find first/last text node or BR element - if (content) { - function setPoint(node, start) { - var walker = new tinymce.dom.TreeWalker(node, node); - - do { - // Text node - if (node.nodeType == 3 && tinymce.trim(node.nodeValue).length != 0) { - if (start) - rng.setStart(node, 0); - else - rng.setEnd(node, node.nodeValue.length); - - return; - } - - // BR element - if (node.nodeName == 'BR') { - if (start) - rng.setStartBefore(node); - else - rng.setEndBefore(node); - - return; - } - } while (node = (start ? walker.next() : walker.prev())); - }; - - setPoint(node, 1); - setPoint(node); - } - - t.setRng(rng); - - return node; - }, - - /** - * Returns true/false if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection. - * - * @method isCollapsed - * @return {Boolean} true/false state if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection. - */ - isCollapsed : function() { - var t = this, r = t.getRng(), s = t.getSel(); - - if (!r || r.item) - return false; - - if (r.compareEndPoints) - return r.compareEndPoints('StartToEnd', r) === 0; - - return !s || r.collapsed; - }, - - /** - * Collapse the selection to start or end of range. - * - * @method collapse - * @param {Boolean} b Optional boolean state if to collapse to end or not. Defaults to start. - */ - collapse : function(b) { - var t = this, r = t.getRng(), n; - - // Control range on IE - if (r.item) { - n = r.item(0); - r = this.win.document.body.createTextRange(); - r.moveToElementText(n); - } - - r.collapse(!!b); - t.setRng(r); - }, - - /** - * Returns the browsers internal selection object. - * - * @method getSel - * @return {Selection} Internal browser selection object. - */ - getSel : function() { - var t = this, w = this.win; - - return w.getSelection ? w.getSelection() : w.document.selection; - }, - - /** - * Returns the browsers internal range object. - * - * @method getRng - * @param {Boolean} w3c Forces a compatible W3C range on IE. - * @return {Range} Internal browser range object. - */ - getRng : function(w3c) { - var t = this, s, r; - - // Found tridentSel object then we need to use that one - if (w3c && t.tridentSel) - return t.tridentSel.getRangeAt(0); - - try { - if (s = t.getSel()) - r = s.rangeCount > 0 ? s.getRangeAt(0) : (s.createRange ? s.createRange() : t.win.document.createRange()); - } catch (ex) { - // IE throws unspecified error here if TinyMCE is placed in a frame/iframe - } - - // No range found then create an empty one - // This can occur when the editor is placed in a hidden container element on Gecko - // Or on IE when there was an exception - if (!r) - r = t.win.document.createRange ? t.win.document.createRange() : t.win.document.body.createTextRange(); - - if (t.selectedRange && t.explicitRange) { - if (r.compareBoundaryPoints(r.START_TO_START, t.selectedRange) === 0 && r.compareBoundaryPoints(r.END_TO_END, t.selectedRange) === 0) { - // Safari, Opera and Chrome only ever select text which causes the range to change. - // This lets us use the originally set range if the selection hasn't been changed by the user. - r = t.explicitRange; - } else { - t.selectedRange = null; - t.explicitRange = null; - } - } - return r; - }, - - /** - * Changes the selection to the specified DOM range. - * - * @method setRng - * @param {Range} r Range to select. - */ - setRng : function(r) { - var s, t = this; - - if (!t.tridentSel) { - s = t.getSel(); - - if (s) { - t.explicitRange = r; - s.removeAllRanges(); - s.addRange(r); - t.selectedRange = s.getRangeAt(0); - } - } else { - // Is W3C Range - if (r.cloneRange) { - t.tridentSel.addRange(r); - return; - } - - // Is IE specific range - try { - r.select(); - } catch (ex) { - // Needed for some odd IE bug #1843306 - } - } - }, - - /** - * Sets the current selection to the specified DOM element. - * - * @method setNode - * @param {Element} n Element to set as the contents of the selection. - * @return {Element} Returns the element that got passed in. - */ - setNode : function(n) { - var t = this; - - t.setContent(t.dom.getOuterHTML(n)); - - return n; - }, - - /** - * Returns the currently selected element or the common ancestor element for both start and end of the selection. - * - * @method getNode - * @return {Element} Currently selected element or common ancestor element. - */ - getNode : function() { - var t = this, rng = t.getRng(), sel = t.getSel(), elm; - - if (rng.setStart) { - // Range maybe lost after the editor is made visible again - if (!rng) - return t.dom.getRoot(); - - elm = rng.commonAncestorContainer; - - // Handle selection a image or other control like element such as anchors - if (!rng.collapsed) { - if (rng.startContainer == rng.endContainer) { - if (rng.startOffset - rng.endOffset < 2) { - if (rng.startContainer.hasChildNodes()) - elm = rng.startContainer.childNodes[rng.startOffset]; - } - } - - // If the anchor node is a element instead of a text node then return this element - if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1) - return sel.anchorNode.childNodes[sel.anchorOffset]; - } - - if (elm && elm.nodeType == 3) - return elm.parentNode; - - return elm; - } - - return rng.item ? rng.item(0) : rng.parentElement(); - }, - - getSelectedBlocks : function(st, en) { - var t = this, dom = t.dom, sb, eb, n, bl = []; - - sb = dom.getParent(st || t.getStart(), dom.isBlock); - eb = dom.getParent(en || t.getEnd(), dom.isBlock); - - if (sb) - bl.push(sb); - - if (sb && eb && sb != eb) { - n = sb; - - while ((n = n.nextSibling) && n != eb) { - if (dom.isBlock(n)) - bl.push(n); - } - } - - if (eb && sb != eb) - bl.push(eb); - - return bl; - }, - - destroy : function(s) { - var t = this; - - t.win = null; - - if (t.tridentSel) - t.tridentSel.destroy(); - - // Manual destroy then remove unload handler - if (!s) - tinymce.removeUnload(t.destroy); - } - }); -})(tinymce); +/** + * Selection.js + * + * Copyright 2009, Moxiecode Systems AB + * Released under LGPL License. + * + * License: http://tinymce.moxiecode.com/license + * Contributing: http://tinymce.moxiecode.com/contributing + */ + +(function(tinymce) { + function trimNl(s) { + return s.replace(/[\n\r]+/g, ''); + }; + + // Shorten names + var is = tinymce.is, isIE = tinymce.isIE, each = tinymce.each; + + /** + * This class handles text and control selection it's an crossbrowser utility class. + * Consult the TinyMCE Wiki API for more details and examples on how to use this class. + * + * @class tinymce.dom.Selection + * @example + * // Getting the currently selected node for the active editor + * alert(tinymce.activeEditor.selection.getNode().nodeName); + */ + tinymce.create('tinymce.dom.Selection', { + /** + * Constructs a new selection instance. + * + * @constructor + * @method Selection + * @param {tinymce.dom.DOMUtils} dom DOMUtils object reference. + * @param {Window} win Window to bind the selection object to. + * @param {tinymce.dom.Serializer} serializer DOM serialization class to use for getContent. + */ + Selection : function(dom, win, serializer) { + var t = this; + + t.dom = dom; + t.win = win; + t.serializer = serializer; + + // Add events + each([ + /** + * This event gets executed before contents is extracted from the selection. + * + * @event onBeforeSetContent + * @param {tinymce.dom.Selection} selection Selection object that fired the event. + * @param {Object} args Contains things like the contents that will be returned. + */ + 'onBeforeSetContent', + + /** + * This event gets executed before contents is inserted into selection. + * + * @event onBeforeGetContent + * @param {tinymce.dom.Selection} selection Selection object that fired the event. + * @param {Object} args Contains things like the contents that will be inserted. + */ + 'onBeforeGetContent', + + /** + * This event gets executed when contents is inserted into selection. + * + * @event onSetContent + * @param {tinymce.dom.Selection} selection Selection object that fired the event. + * @param {Object} args Contains things like the contents that will be inserted. + */ + 'onSetContent', + + /** + * This event gets executed when contents is extracted from the selection. + * + * @event onGetContent + * @param {tinymce.dom.Selection} selection Selection object that fired the event. + * @param {Object} args Contains things like the contents that will be returned. + */ + 'onGetContent' + ], function(e) { + t[e] = new tinymce.util.Dispatcher(t); + }); + + // No W3C Range support + if (!t.win.getSelection) + t.tridentSel = new tinymce.dom.TridentSelection(t); + + if (tinymce.isIE && dom.boxModel) + this._fixIESelection(); + + // Prevent leaks + tinymce.addUnload(t.destroy, t); + }, + + /** + * Move the selection cursor range to the specified node and offset. + * @param node Node to put the cursor in. + * @param offset Offset from the start of the node to put the cursor at. + */ + setCursorLocation: function(node, offset) { + var t = this; var r = t.dom.createRng(); + r.setStart(node, offset); + r.setEnd(node, offset); + t.setRng(r); + t.collapse(false); + }, + /** + * Returns the selected contents using the DOM serializer passed in to this class. + * + * @method getContent + * @param {Object} s Optional settings class with for example output format text or html. + * @return {String} Selected contents in for example HTML format. + * @example + * // Alerts the currently selected contents + * alert(tinyMCE.activeEditor.selection.getContent()); + * + * // Alerts the currently selected contents as plain text + * alert(tinyMCE.activeEditor.selection.getContent({format : 'text'})); + */ + getContent : function(s) { + var t = this, r = t.getRng(), e = t.dom.create("body"), se = t.getSel(), wb, wa, n; + + s = s || {}; + wb = wa = ''; + s.get = true; + s.format = s.format || 'html'; + s.forced_root_block = ''; + t.onBeforeGetContent.dispatch(t, s); + + if (s.format == 'text') + return t.isCollapsed() ? '' : (r.text || (se.toString ? se.toString() : '')); + + if (r.cloneContents) { + n = r.cloneContents(); + + if (n) + e.appendChild(n); + } else if (is(r.item) || is(r.htmlText)) { + // IE will produce invalid markup if elements are present that + // it doesn't understand like custom elements or HTML5 elements. + // Adding a BR in front of the contents and then remoiving it seems to fix it though. + e.innerHTML = '
    ' + (r.item ? r.item(0).outerHTML : r.htmlText); + e.removeChild(e.firstChild); + } else + e.innerHTML = r.toString(); + + // Keep whitespace before and after + if (/^\s/.test(e.innerHTML)) + wb = ' '; + + if (/\s+$/.test(e.innerHTML)) + wa = ' '; + + s.getInner = true; + + s.content = t.isCollapsed() ? '' : wb + t.serializer.serialize(e, s) + wa; + t.onGetContent.dispatch(t, s); + + return s.content; + }, + + /** + * Sets the current selection to the specified content. If any contents is selected it will be replaced + * with the contents passed in to this function. If there is no selection the contents will be inserted + * where the caret is placed in the editor/page. + * + * @method setContent + * @param {String} content HTML contents to set could also be other formats depending on settings. + * @param {Object} args Optional settings object with for example data format. + * @example + * // Inserts some HTML contents at the current selection + * tinyMCE.activeEditor.selection.setContent('Some contents'); + */ + setContent : function(content, args) { + var self = this, rng = self.getRng(), caretNode, doc = self.win.document, frag, temp; + + args = args || {format : 'html'}; + args.set = true; + content = args.content = content; + + // Dispatch before set content event + if (!args.no_events) + self.onBeforeSetContent.dispatch(self, args); + + content = args.content; + + if (rng.insertNode) { + // Make caret marker since insertNode places the caret in the beginning of text after insert + content += '_'; + + // Delete and insert new node + if (rng.startContainer == doc && rng.endContainer == doc) { + // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents + doc.body.innerHTML = content; + } else { + rng.deleteContents(); + + if (doc.body.childNodes.length == 0) { + doc.body.innerHTML = content; + } else { + // createContextualFragment doesn't exists in IE 9 DOMRanges + if (rng.createContextualFragment) { + rng.insertNode(rng.createContextualFragment(content)); + } else { + // Fake createContextualFragment call in IE 9 + frag = doc.createDocumentFragment(); + temp = doc.createElement('div'); + + frag.appendChild(temp); + temp.outerHTML = content; + + rng.insertNode(frag); + } + } + } + + // Move to caret marker + caretNode = self.dom.get('__caret'); + + // Make sure we wrap it compleatly, Opera fails with a simple select call + rng = doc.createRange(); + rng.setStartBefore(caretNode); + rng.setEndBefore(caretNode); + self.setRng(rng); + + // Remove the caret position + self.dom.remove('__caret'); + + try { + self.setRng(rng); + } catch (ex) { + // Might fail on Opera for some odd reason + } + } else { + if (rng.item) { + // Delete content and get caret text selection + doc.execCommand('Delete', false, null); + rng = self.getRng(); + } + + // Explorer removes spaces from the beginning of pasted contents + if (/^\s+/.test(content)) { + rng.pasteHTML('_' + content); + self.dom.remove('__mce_tmp'); + } else + rng.pasteHTML(content); + } + + // Dispatch set content event + if (!args.no_events) + self.onSetContent.dispatch(self, args); + }, + + /** + * Returns the start element of a selection range. If the start is in a text + * node the parent element will be returned. + * + * @method getStart + * @return {Element} Start element of selection range. + */ + getStart : function() { + var rng = this.getRng(), startElement, parentElement, checkRng, node; + + if (rng.duplicate || rng.item) { + // Control selection, return first item + if (rng.item) + return rng.item(0); + + // Get start element + checkRng = rng.duplicate(); + checkRng.collapse(1); + startElement = checkRng.parentElement(); + + // Check if range parent is inside the start element, then return the inner parent element + // This will fix issues when a single element is selected, IE would otherwise return the wrong start element + parentElement = node = rng.parentElement(); + while (node = node.parentNode) { + if (node == startElement) { + startElement = parentElement; + break; + } + } + + return startElement; + } else { + startElement = rng.startContainer; + + if (startElement.nodeType == 1 && startElement.hasChildNodes()) + startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)]; + + if (startElement && startElement.nodeType == 3) + return startElement.parentNode; + + return startElement; + } + }, + + /** + * Returns the end element of a selection range. If the end is in a text + * node the parent element will be returned. + * + * @method getEnd + * @return {Element} End element of selection range. + */ + getEnd : function() { + var t = this, r = t.getRng(), e, eo; + + if (r.duplicate || r.item) { + if (r.item) + return r.item(0); + + r = r.duplicate(); + r.collapse(0); + e = r.parentElement(); + + if (e && e.nodeName == 'BODY') + return e.lastChild || e; + + return e; + } else { + e = r.endContainer; + eo = r.endOffset; + + if (e.nodeType == 1 && e.hasChildNodes()) + e = e.childNodes[eo > 0 ? eo - 1 : eo]; + + if (e && e.nodeType == 3) + return e.parentNode; + + return e; + } + }, + + /** + * Returns a bookmark location for the current selection. This bookmark object + * can then be used to restore the selection after some content modification to the document. + * + * @method getBookmark + * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex. + * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization. + * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection. + * @example + * // Stores a bookmark of the current selection + * var bm = tinyMCE.activeEditor.selection.getBookmark(); + * + * tinyMCE.activeEditor.setContent(tinyMCE.activeEditor.getContent() + 'Some new content'); + * + * // Restore the selection bookmark + * tinyMCE.activeEditor.selection.moveToBookmark(bm); + */ + getBookmark : function(type, normalized) { + var t = this, dom = t.dom, rng, rng2, id, collapsed, name, element, index, chr = '\uFEFF', styles; + + function findIndex(name, element) { + var index = 0; + + each(dom.select(name), function(node, i) { + if (node == element) + index = i; + }); + + return index; + }; + + if (type == 2) { + function getLocation() { + var rng = t.getRng(true), root = dom.getRoot(), bookmark = {}; + + function getPoint(rng, start) { + var container = rng[start ? 'startContainer' : 'endContainer'], + offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0; + + if (container.nodeType == 3) { + if (normalized) { + for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling) + offset += node.nodeValue.length; + } + + point.push(offset); + } else { + childNodes = container.childNodes; + + if (offset >= childNodes.length && childNodes.length) { + after = 1; + offset = Math.max(0, childNodes.length - 1); + } + + point.push(t.dom.nodeIndex(childNodes[offset], normalized) + after); + } + + for (; container && container != root; container = container.parentNode) + point.push(t.dom.nodeIndex(container, normalized)); + + return point; + }; + + bookmark.start = getPoint(rng, true); + + if (!t.isCollapsed()) + bookmark.end = getPoint(rng); + + return bookmark; + }; + + if (t.tridentSel) + return t.tridentSel.getBookmark(type); + + return getLocation(); + } + + // Handle simple range + if (type) + return {rng : t.getRng()}; + + rng = t.getRng(); + id = dom.uniqueId(); + collapsed = tinyMCE.activeEditor.selection.isCollapsed(); + styles = 'overflow:hidden;line-height:0px'; + + // Explorer method + if (rng.duplicate || rng.item) { + // Text selection + if (!rng.item) { + rng2 = rng.duplicate(); + + try { + // Insert start marker + rng.collapse(); + rng.pasteHTML('' + chr + ''); + + // Insert end marker + if (!collapsed) { + rng2.collapse(false); + + // Detect the empty space after block elements in IE and move the end back one character

    ] becomes

    ]

    + rng.moveToElementText(rng2.parentElement()); + if (rng.compareEndPoints('StartToEnd', rng2) == 0) + rng2.move('character', -1); + + rng2.pasteHTML('' + chr + ''); + } + } catch (ex) { + // IE might throw unspecified error so lets ignore it + return null; + } + } else { + // Control selection + element = rng.item(0); + name = element.nodeName; + + return {name : name, index : findIndex(name, element)}; + } + } else { + element = t.getNode(); + name = element.nodeName; + if (name == 'IMG') + return {name : name, index : findIndex(name, element)}; + + // W3C method + rng2 = rng.cloneRange(); + + // Insert end marker + if (!collapsed) { + rng2.collapse(false); + rng2.insertNode(dom.create('span', {'data-mce-type' : "bookmark", id : id + '_end', style : styles}, chr)); + } + + rng.collapse(true); + rng.insertNode(dom.create('span', {'data-mce-type' : "bookmark", id : id + '_start', style : styles}, chr)); + } + + t.moveToBookmark({id : id, keep : 1}); + + return {id : id}; + }, + + /** + * Restores the selection to the specified bookmark. + * + * @method moveToBookmark + * @param {Object} bookmark Bookmark to restore selection from. + * @return {Boolean} true/false if it was successful or not. + * @example + * // Stores a bookmark of the current selection + * var bm = tinyMCE.activeEditor.selection.getBookmark(); + * + * tinyMCE.activeEditor.setContent(tinyMCE.activeEditor.getContent() + 'Some new content'); + * + * // Restore the selection bookmark + * tinyMCE.activeEditor.selection.moveToBookmark(bm); + */ + moveToBookmark : function(bookmark) { + var t = this, dom = t.dom, marker1, marker2, rng, root, startContainer, endContainer, startOffset, endOffset; + + if (bookmark) { + if (bookmark.start) { + rng = dom.createRng(); + root = dom.getRoot(); + + function setEndPoint(start) { + var point = bookmark[start ? 'start' : 'end'], i, node, offset, children; + + if (point) { + offset = point[0]; + + // Find container node + for (node = root, i = point.length - 1; i >= 1; i--) { + children = node.childNodes; + + if (point[i] > children.length - 1) + return; + + node = children[point[i]]; + } + + // Move text offset to best suitable location + if (node.nodeType === 3) + offset = Math.min(point[0], node.nodeValue.length); + + // Move element offset to best suitable location + if (node.nodeType === 1) + offset = Math.min(point[0], node.childNodes.length); + + // Set offset within container node + if (start) + rng.setStart(node, offset); + else + rng.setEnd(node, offset); + } + + return true; + }; + + if (t.tridentSel) + return t.tridentSel.moveToBookmark(bookmark); + + if (setEndPoint(true) && setEndPoint()) { + t.setRng(rng); + } + } else if (bookmark.id) { + function restoreEndPoint(suffix) { + var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep; + + if (marker) { + node = marker.parentNode; + + if (suffix == 'start') { + if (!keep) { + idx = dom.nodeIndex(marker); + } else { + node = marker.firstChild; + idx = 1; + } + + startContainer = endContainer = node; + startOffset = endOffset = idx; + } else { + if (!keep) { + idx = dom.nodeIndex(marker); + } else { + node = marker.firstChild; + idx = 1; + } + + endContainer = node; + endOffset = idx; + } + + if (!keep) { + prev = marker.previousSibling; + next = marker.nextSibling; + + // Remove all marker text nodes + each(tinymce.grep(marker.childNodes), function(node) { + if (node.nodeType == 3) + node.nodeValue = node.nodeValue.replace(/\uFEFF/g, ''); + }); + + // Remove marker but keep children if for example contents where inserted into the marker + // Also remove duplicated instances of the marker for example by a split operation or by WebKit auto split on paste feature + while (marker = dom.get(bookmark.id + '_' + suffix)) + dom.remove(marker, 1); + + // If siblings are text nodes then merge them unless it's Opera since it some how removes the node + // and we are sniffing since adding a lot of detection code for a browser with 3% of the market isn't worth the effort. Sorry, Opera but it's just a fact + if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !tinymce.isOpera) { + idx = prev.nodeValue.length; + prev.appendData(next.nodeValue); + dom.remove(next); + + if (suffix == 'start') { + startContainer = endContainer = prev; + startOffset = endOffset = idx; + } else { + endContainer = prev; + endOffset = idx; + } + } + } + } + }; + + function addBogus(node) { + // Adds a bogus BR element for empty block elements or just a space on IE since it renders BR elements incorrectly + if (dom.isBlock(node) && !node.innerHTML) + node.innerHTML = !isIE ? '
    ' : ' '; + + return node; + }; + + // Restore start/end points + restoreEndPoint('start'); + restoreEndPoint('end'); + + if (startContainer) { + rng = dom.createRng(); + rng.setStart(addBogus(startContainer), startOffset); + rng.setEnd(addBogus(endContainer), endOffset); + t.setRng(rng); + } + } else if (bookmark.name) { + t.select(dom.select(bookmark.name)[bookmark.index]); + } else if (bookmark.rng) + t.setRng(bookmark.rng); + } + }, + + /** + * Selects the specified element. This will place the start and end of the selection range around the element. + * + * @method select + * @param {Element} node HMTL DOM element to select. + * @param {Boolean} content Optional bool state if the contents should be selected or not on non IE browser. + * @return {Element} Selected element the same element as the one that got passed in. + * @example + * // Select the first paragraph in the active editor + * tinyMCE.activeEditor.selection.select(tinyMCE.activeEditor.dom.select('p')[0]); + */ + select : function(node, content) { + var t = this, dom = t.dom, rng = dom.createRng(), idx; + + if (node) { + idx = dom.nodeIndex(node); + rng.setStart(node.parentNode, idx); + rng.setEnd(node.parentNode, idx + 1); + + // Find first/last text node or BR element + if (content) { + function setPoint(node, start) { + var walker = new tinymce.dom.TreeWalker(node, node); + + do { + // Text node + if (node.nodeType == 3 && tinymce.trim(node.nodeValue).length != 0) { + if (start) + rng.setStart(node, 0); + else + rng.setEnd(node, node.nodeValue.length); + + return; + } + + // BR element + if (node.nodeName == 'BR') { + if (start) + rng.setStartBefore(node); + else + rng.setEndBefore(node); + + return; + } + } while (node = (start ? walker.next() : walker.prev())); + }; + + setPoint(node, 1); + setPoint(node); + } + + t.setRng(rng); + } + + return node; + }, + + /** + * Returns true/false if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection. + * + * @method isCollapsed + * @return {Boolean} true/false state if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection. + */ + isCollapsed : function() { + var t = this, r = t.getRng(), s = t.getSel(); + + if (!r || r.item) + return false; + + if (r.compareEndPoints) + return r.compareEndPoints('StartToEnd', r) === 0; + + return !s || r.collapsed; + }, + + /** + * Collapse the selection to start or end of range. + * + * @method collapse + * @param {Boolean} to_start Optional boolean state if to collapse to end or not. Defaults to start. + */ + collapse : function(to_start) { + var self = this, rng = self.getRng(), node; + + // Control range on IE + if (rng.item) { + node = rng.item(0); + rng = self.win.document.body.createTextRange(); + rng.moveToElementText(node); + } + + rng.collapse(!!to_start); + self.setRng(rng); + }, + + /** + * Returns the browsers internal selection object. + * + * @method getSel + * @return {Selection} Internal browser selection object. + */ + getSel : function() { + var t = this, w = this.win; + + return w.getSelection ? w.getSelection() : w.document.selection; + }, + + /** + * Returns the browsers internal range object. + * + * @method getRng + * @param {Boolean} w3c Forces a compatible W3C range on IE. + * @return {Range} Internal browser range object. + * @see http://www.quirksmode.org/dom/range_intro.html + * @see http://www.dotvoid.com/2001/03/using-the-range-object-in-mozilla/ + */ + getRng : function(w3c) { + var t = this, s, r, elm, doc = t.win.document; + + // Found tridentSel object then we need to use that one + if (w3c && t.tridentSel) + return t.tridentSel.getRangeAt(0); + + try { + if (s = t.getSel()) + r = s.rangeCount > 0 ? s.getRangeAt(0) : (s.createRange ? s.createRange() : doc.createRange()); + } catch (ex) { + // IE throws unspecified error here if TinyMCE is placed in a frame/iframe + } + + // We have W3C ranges and it's IE then fake control selection since IE9 doesn't handle that correctly yet + if (tinymce.isIE && r && r.setStart && doc.selection.createRange().item) { + elm = doc.selection.createRange().item(0); + r = doc.createRange(); + r.setStartBefore(elm); + r.setEndAfter(elm); + } + + // No range found then create an empty one + // This can occur when the editor is placed in a hidden container element on Gecko + // Or on IE when there was an exception + if (!r) + r = doc.createRange ? doc.createRange() : doc.body.createTextRange(); + + if (t.selectedRange && t.explicitRange) { + if (r.compareBoundaryPoints(r.START_TO_START, t.selectedRange) === 0 && r.compareBoundaryPoints(r.END_TO_END, t.selectedRange) === 0) { + // Safari, Opera and Chrome only ever select text which causes the range to change. + // This lets us use the originally set range if the selection hasn't been changed by the user. + r = t.explicitRange; + } else { + t.selectedRange = null; + t.explicitRange = null; + } + } + + return r; + }, + + /** + * Changes the selection to the specified DOM range. + * + * @method setRng + * @param {Range} r Range to select. + */ + setRng : function(r) { + var s, t = this; + + if (!t.tridentSel) { + s = t.getSel(); + + if (s) { + t.explicitRange = r; + + try { + s.removeAllRanges(); + } catch (ex) { + // IE9 might throw errors here don't know why + } + + s.addRange(r); + t.selectedRange = s.getRangeAt(0); + } + } else { + // Is W3C Range + if (r.cloneRange) { + t.tridentSel.addRange(r); + return; + } + + // Is IE specific range + try { + r.select(); + } catch (ex) { + // Needed for some odd IE bug #1843306 + } + } + }, + + /** + * Sets the current selection to the specified DOM element. + * + * @method setNode + * @param {Element} n Element to set as the contents of the selection. + * @return {Element} Returns the element that got passed in. + * @example + * // Inserts a DOM node at current selection/caret location + * tinyMCE.activeEditor.selection.setNode(tinyMCE.activeEditor.dom.create('img', {src : 'some.gif', title : 'some title'})); + */ + setNode : function(n) { + var t = this; + + t.setContent(t.dom.getOuterHTML(n)); + + return n; + }, + + /** + * Returns the currently selected element or the common ancestor element for both start and end of the selection. + * + * @method getNode + * @return {Element} Currently selected element or common ancestor element. + * @example + * // Alerts the currently selected elements node name + * alert(tinyMCE.activeEditor.selection.getNode().nodeName); + */ + getNode : function() { + var t = this, rng = t.getRng(), sel = t.getSel(), elm, start = rng.startContainer, end = rng.endContainer; + + // Range maybe lost after the editor is made visible again + if (!rng) + return t.dom.getRoot(); + + if (rng.setStart) { + elm = rng.commonAncestorContainer; + + // Handle selection a image or other control like element such as anchors + if (!rng.collapsed) { + if (rng.startContainer == rng.endContainer) { + if (rng.endOffset - rng.startOffset < 2) { + if (rng.startContainer.hasChildNodes()) + elm = rng.startContainer.childNodes[rng.startOffset]; + } + } + + // If the anchor node is a element instead of a text node then return this element + //if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1) + // return sel.anchorNode.childNodes[sel.anchorOffset]; + + // Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent. + // This happens when you double click an underlined word in FireFox. + if (start.nodeType === 3 && end.nodeType === 3) { + function skipEmptyTextNodes(n, forwards) { + var orig = n; + while (n && n.nodeType === 3 && n.length === 0) { + n = forwards ? n.nextSibling : n.previousSibling; + } + return n || orig; + } + if (start.length === rng.startOffset) { + start = skipEmptyTextNodes(start.nextSibling, true); + } else { + start = start.parentNode; + } + if (rng.endOffset === 0) { + end = skipEmptyTextNodes(end.previousSibling, false); + } else { + end = end.parentNode; + } + + if (start && start === end) + return start; + } + } + + if (elm && elm.nodeType == 3) + return elm.parentNode; + + return elm; + } + + return rng.item ? rng.item(0) : rng.parentElement(); + }, + + getSelectedBlocks : function(st, en) { + var t = this, dom = t.dom, sb, eb, n, bl = []; + + sb = dom.getParent(st || t.getStart(), dom.isBlock); + eb = dom.getParent(en || t.getEnd(), dom.isBlock); + + if (sb) + bl.push(sb); + + if (sb && eb && sb != eb) { + n = sb; + + var walker = new tinymce.dom.TreeWalker(sb, dom.getRoot()); + while ((n = walker.next()) && n != eb) { + if (dom.isBlock(n)) + bl.push(n); + } + } + + if (eb && sb != eb) + bl.push(eb); + + return bl; + }, + + normalize : function() { + var self = this, rng, normalized; + + // Normalize only on non IE browsers for now + if (tinymce.isIE) + return; + + function normalizeEndPoint(start) { + var container, offset, walker, dom = self.dom, body = dom.getRoot(), node; + + container = rng[(start ? 'start' : 'end') + 'Container']; + offset = rng[(start ? 'start' : 'end') + 'Offset']; + + // If the container is a document move it to the body element + if (container.nodeType === 9) { + container = container.body; + offset = 0; + } + + // If the container is body try move it into the closest text node or position + // TODO: Add more logic here to handle element selection cases + if (container === body) { + // Resolve the index + if (container.hasChildNodes()) { + container = container.childNodes[Math.min(!start && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1)]; + offset = 0; + + // Don't walk into elements that doesn't have any child nodes like a IMG + if (container.hasChildNodes()) { + // Walk the DOM to find a text node to place the caret at or a BR + node = container; + walker = new tinymce.dom.TreeWalker(container, body); + do { + // Found a text node use that position + if (node.nodeType === 3) { + offset = start ? 0 : node.nodeValue.length - 1; + container = node; + break; + } + + // Found a BR element that we can place the caret before + if (node.nodeName === 'BR') { + offset = dom.nodeIndex(node); + container = node.parentNode; + break; + } + } while (node = (start ? walker.next() : walker.prev())); + + normalized = true; + } + } + } + + // Set endpoint if it was normalized + if (normalized) + rng['set' + (start ? 'Start' : 'End')](container, offset); + }; + + rng = self.getRng(); + + // Normalize the end points + normalizeEndPoint(true); + + if (rng.collapsed) + normalizeEndPoint(); + + // Set the selection if it was normalized + if (normalized) { + //console.log(self.dom.dumpRng(rng)); + self.setRng(rng); + } + }, + + destroy : function(s) { + var t = this; + + t.win = null; + + // Manual destroy then remove unload handler + if (!s) + tinymce.removeUnload(t.destroy); + }, + + // IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode + _fixIESelection : function() { + var dom = this.dom, doc = dom.doc, body = doc.body, started, startRng, htmlElm; + + // Make HTML element unselectable since we are going to handle selection by hand + doc.documentElement.unselectable = true; + + // Return range from point or null if it failed + function rngFromPoint(x, y) { + var rng = body.createTextRange(); + + try { + rng.moveToPoint(x, y); + } catch (ex) { + // IE sometimes throws and exception, so lets just ignore it + rng = null; + } + + return rng; + }; + + // Fires while the selection is changing + function selectionChange(e) { + var pointRng; + + // Check if the button is down or not + if (e.button) { + // Create range from mouse position + pointRng = rngFromPoint(e.x, e.y); + + if (pointRng) { + // Check if pointRange is before/after selection then change the endPoint + if (pointRng.compareEndPoints('StartToStart', startRng) > 0) + pointRng.setEndPoint('StartToStart', startRng); + else + pointRng.setEndPoint('EndToEnd', startRng); + + pointRng.select(); + } + } else + endSelection(); + } + + // Removes listeners + function endSelection() { + var rng = doc.selection.createRange(); + + // If the range is collapsed then use the last start range + if (startRng && !rng.item && rng.compareEndPoints('StartToEnd', rng) === 0) + startRng.select(); + + dom.unbind(doc, 'mouseup', endSelection); + dom.unbind(doc, 'mousemove', selectionChange); + startRng = started = 0; + }; + + // Detect when user selects outside BODY + dom.bind(doc, ['mousedown', 'contextmenu'], function(e) { + if (e.target.nodeName === 'HTML') { + if (started) + endSelection(); + + // Detect vertical scrollbar, since IE will fire a mousedown on the scrollbar and have target set as HTML + htmlElm = doc.documentElement; + if (htmlElm.scrollHeight > htmlElm.clientHeight) + return; + + started = 1; + // Setup start position + startRng = rngFromPoint(e.x, e.y); + if (startRng) { + // Listen for selection change events + dom.bind(doc, 'mouseup', endSelection); + dom.bind(doc, 'mousemove', selectionChange); + + dom.win.focus(); + startRng.select(); + } + } + }); + } + }); +})(tinymce); diff --git a/js/tiny_mce/classes/dom/Serializer.js b/js/tiny_mce/classes/dom/Serializer.js index 71c0b0bc44..c0530dd915 100644 --- a/js/tiny_mce/classes/dom/Serializer.js +++ b/js/tiny_mce/classes/dom/Serializer.js @@ -9,937 +9,371 @@ */ (function(tinymce) { - // Shorten names - var extend = tinymce.extend, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher, isIE = tinymce.isIE, isGecko = tinymce.isGecko; - - function wildcardToRE(s) { - return s.replace(/([?+*])/g, '.$1'); - }; - /** - * This class is used to serialize DOM trees into a string. - * Consult the TinyMCE Wiki API for more details and examples on how to use this class. + * This class is used to serialize DOM trees into a string. Consult the TinyMCE Wiki API for more details and examples on how to use this class. + * * @class tinymce.dom.Serializer */ - tinymce.create('tinymce.dom.Serializer', { - /** - * Constucts a new DOM serializer class. - * - * @constructor - * @method Serializer - * @param {Object} s Optional name/Value collection of settings for the serializer. - */ - Serializer : function(s) { - var t = this; - - t.key = 0; - t.onPreProcess = new Dispatcher(t); - t.onPostProcess = new Dispatcher(t); - - try { - t.writer = new tinymce.dom.XMLWriter(); - } catch (ex) { - // IE might throw exception if ActiveX is disabled so we then switch to the slightly slower StringWriter - t.writer = new tinymce.dom.StringWriter(); - } - - // Default settings - t.settings = s = extend({ - dom : tinymce.DOM, - valid_nodes : 0, - node_filter : 0, - attr_filter : 0, - invalid_attrs : /^(_mce_|_moz_|sizset|sizcache)/, - closed : /^(br|hr|input|meta|img|link|param|area)$/, - entity_encoding : 'named', - entities : '160,nbsp,161,iexcl,162,cent,163,pound,164,curren,165,yen,166,brvbar,167,sect,168,uml,169,copy,170,ordf,171,laquo,172,not,173,shy,174,reg,175,macr,176,deg,177,plusmn,178,sup2,179,sup3,180,acute,181,micro,182,para,183,middot,184,cedil,185,sup1,186,ordm,187,raquo,188,frac14,189,frac12,190,frac34,191,iquest,192,Agrave,193,Aacute,194,Acirc,195,Atilde,196,Auml,197,Aring,198,AElig,199,Ccedil,200,Egrave,201,Eacute,202,Ecirc,203,Euml,204,Igrave,205,Iacute,206,Icirc,207,Iuml,208,ETH,209,Ntilde,210,Ograve,211,Oacute,212,Ocirc,213,Otilde,214,Ouml,215,times,216,Oslash,217,Ugrave,218,Uacute,219,Ucirc,220,Uuml,221,Yacute,222,THORN,223,szlig,224,agrave,225,aacute,226,acirc,227,atilde,228,auml,229,aring,230,aelig,231,ccedil,232,egrave,233,eacute,234,ecirc,235,euml,236,igrave,237,iacute,238,icirc,239,iuml,240,eth,241,ntilde,242,ograve,243,oacute,244,ocirc,245,otilde,246,ouml,247,divide,248,oslash,249,ugrave,250,uacute,251,ucirc,252,uuml,253,yacute,254,thorn,255,yuml,402,fnof,913,Alpha,914,Beta,915,Gamma,916,Delta,917,Epsilon,918,Zeta,919,Eta,920,Theta,921,Iota,922,Kappa,923,Lambda,924,Mu,925,Nu,926,Xi,927,Omicron,928,Pi,929,Rho,931,Sigma,932,Tau,933,Upsilon,934,Phi,935,Chi,936,Psi,937,Omega,945,alpha,946,beta,947,gamma,948,delta,949,epsilon,950,zeta,951,eta,952,theta,953,iota,954,kappa,955,lambda,956,mu,957,nu,958,xi,959,omicron,960,pi,961,rho,962,sigmaf,963,sigma,964,tau,965,upsilon,966,phi,967,chi,968,psi,969,omega,977,thetasym,978,upsih,982,piv,8226,bull,8230,hellip,8242,prime,8243,Prime,8254,oline,8260,frasl,8472,weierp,8465,image,8476,real,8482,trade,8501,alefsym,8592,larr,8593,uarr,8594,rarr,8595,darr,8596,harr,8629,crarr,8656,lArr,8657,uArr,8658,rArr,8659,dArr,8660,hArr,8704,forall,8706,part,8707,exist,8709,empty,8711,nabla,8712,isin,8713,notin,8715,ni,8719,prod,8721,sum,8722,minus,8727,lowast,8730,radic,8733,prop,8734,infin,8736,ang,8743,and,8744,or,8745,cap,8746,cup,8747,int,8756,there4,8764,sim,8773,cong,8776,asymp,8800,ne,8801,equiv,8804,le,8805,ge,8834,sub,8835,sup,8836,nsub,8838,sube,8839,supe,8853,oplus,8855,otimes,8869,perp,8901,sdot,8968,lceil,8969,rceil,8970,lfloor,8971,rfloor,9001,lang,9002,rang,9674,loz,9824,spades,9827,clubs,9829,hearts,9830,diams,338,OElig,339,oelig,352,Scaron,353,scaron,376,Yuml,710,circ,732,tilde,8194,ensp,8195,emsp,8201,thinsp,8204,zwnj,8205,zwj,8206,lrm,8207,rlm,8211,ndash,8212,mdash,8216,lsquo,8217,rsquo,8218,sbquo,8220,ldquo,8221,rdquo,8222,bdquo,8224,dagger,8225,Dagger,8240,permil,8249,lsaquo,8250,rsaquo,8364,euro', - valid_elements : '*[*]', - extended_valid_elements : 0, - invalid_elements : 0, - fix_table_elements : 1, - fix_list_elements : true, - fix_content_duplication : true, - convert_fonts_to_spans : false, - font_size_classes : 0, - apply_source_formatting : 0, - indent_mode : 'simple', - indent_char : '\t', - indent_levels : 1, - remove_linebreaks : 1, - remove_redundant_brs : 1, - element_format : 'xhtml' - }, s); - - t.dom = s.dom; - t.schema = s.schema; - - // Use raw entities if no entities are defined - if (s.entity_encoding == 'named' && !s.entities) - s.entity_encoding = 'raw'; - - if (s.remove_redundant_brs) { - t.onPostProcess.add(function(se, o) { - // Remove single BR at end of block elements since they get rendered - o.content = o.content.replace(/(
    \s*)+<\/(p|h[1-6]|div|li)>/gi, function(a, b, c) { - // Check if it's a single element - if (/^
    \s*<\//.test(a)) - return ''; - - return a; - }); - }); - } - - // Remove XHTML element endings i.e. produce crap :) XHTML is better - if (s.element_format == 'html') { - t.onPostProcess.add(function(se, o) { - o.content = o.content.replace(/<([^>]+) \/>/g, '<$1>'); - }); - } - - if (s.fix_list_elements) { - t.onPreProcess.add(function(se, o) { - var nl, x, a = ['ol', 'ul'], i, n, p, r = /^(OL|UL)$/, np; - - function prevNode(e, n) { - var a = n.split(','), i; - - while ((e = e.previousSibling) != null) { - for (i=0; i= 1767) { - each(t.dom.select('p table', o.node).reverse(), function(n) { - var parent = t.dom.getParent(n.parentNode, 'table,p'); - - if (parent.nodeName != 'TABLE') { - try { - t.dom.split(parent, n); - } catch (ex) { - // IE can sometimes fire an unknown runtime error so we just ignore it - } - } - }); - } - }); - } - }, - - /** - * Sets a list of entities to use for the named entity encoded. - * - * @method setEntities - * @param {String} s List of entities in the following format: number,name,.... - */ - setEntities : function(s) { - var t = this, a, i, l = {}, v; - - // No need to setup more than once - if (t.entityLookup) - return; - - // Build regex and lookup array - a = s.split(','); - for (i = 0; i < a.length; i += 2) { - v = a[i]; - // Don't add default & " etc. - if (v == 34 || v == 38 || v == 60 || v == 62) - continue; + /** + * Constucts a new DOM serializer class. + * + * @constructor + * @method Serializer + * @param {Object} settings Serializer settings object. + * @param {tinymce.dom.DOMUtils} dom DOMUtils instance reference. + * @param {tinymce.html.Schema} schema Optional schema reference. + */ + tinymce.dom.Serializer = function(settings, dom, schema) { + var onPreProcess, onPostProcess, isIE = tinymce.isIE, each = tinymce.each, htmlParser; - l[String.fromCharCode(a[i])] = a[i + 1]; + // Support the old apply_source_formatting option + if (!settings.apply_source_formatting) + settings.indent = false; - v = parseInt(a[i]).toString(16); - } + settings.remove_trailing_brs = true; - t.entityLookup = l; - }, + // Default DOM and Schema if they are undefined + dom = dom || tinymce.DOM; + schema = schema || new tinymce.html.Schema(settings); + settings.entity_encoding = settings.entity_encoding || 'named'; /** - * Sets the valid elements rules of the serializer this enables you to specify things like what elements should be - * outputted and what attributes specific elements might have. - * Consult the Wiki for more details on this format. + * This event gets executed before a HTML fragment gets serialized into a HTML string. This event enables you to do modifications to the DOM before the serialization occurs. It's important to know that the element that is getting serialized is cloned so it's not inside a document. * - * @method setRules - * @param {String} s Valid elements rules string. + * @event onPreProcess + * @param {tinymce.dom.Serializer} sender object/Serializer instance that is serializing an element. + * @param {Object} args Object containing things like the current node. + * @example + * // Adds an observer to the onPreProcess event + * serializer.onPreProcess.add(function(se, o) { + * // Add a class to each paragraph + * se.dom.addClass(se.dom.select('p', o.node), 'myclass'); + * }); */ - setRules : function(s) { - var t = this; - - t._setup(); - t.rules = {}; - t.wildRules = []; - t.validElements = {}; - - return t.addRules(s); - }, + onPreProcess = new tinymce.util.Dispatcher(self); /** - * Adds valid elements rules to the serializer this enables you to specify things like what elements should be - * outputted and what attributes specific elements might have. - * Consult the Wiki for more details on this format. + * This event gets executed after a HTML fragment has been serialized into a HTML string. This event enables you to do modifications to the HTML string like regexp replaces etc. * - * @method addRules - * @param {String} s Valid elements rules string to add. + * @event onPreProcess + * @param {tinymce.dom.Serializer} sender object/Serializer instance that is serializing an element. + * @param {Object} args Object containing things like the current contents. + * @example + * // Adds an observer to the onPostProcess event + * serializer.onPostProcess.add(function(se, o) { + * // Remove all paragraphs and replace with BR + * o.content = o.content.replace(/]+>|

    /g, ''); + * o.content = o.content.replace(/<\/p>/g, '
    '); + * }); */ - addRules : function(s) { - var t = this, dr; - - if (!s) - return; + onPostProcess = new tinymce.util.Dispatcher(self); - t._setup(); + htmlParser = new tinymce.html.DomParser(settings, schema); - each(s.split(','), function(s) { - var p = s.split(/\[|\]/), tn = p[0].split('/'), ra, at, wat, va = []; + // Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed + htmlParser.addAttributeFilter('src,href,style', function(nodes, name) { + var i = nodes.length, node, value, internalName = 'data-mce-' + name, urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope, undef; - // Extend with default rules - if (dr) - at = tinymce.extend([], dr.attribs); + while (i--) { + node = nodes[i]; - // Parse attributes - if (p.length > 1) { - each(p[1].split('|'), function(s) { - var ar = {}, i; + value = node.attributes.map[internalName]; + if (value !== undef) { + // Set external name to internal value and remove internal + node.attr(name, value.length > 0 ? value : null); + node.attr(internalName, null); + } else { + // No internal attribute found then convert the value we have in the DOM + value = node.attributes.map[name]; - at = at || []; + if (name === "style") + value = dom.serializeStyle(dom.parseStyle(value), node.name); + else if (urlConverter) + value = urlConverter.call(urlConverterScope, value, name, node.name); - // Parse attribute rule - s = s.replace(/::/g, '~'); - s = /^([!\-])?([\w*.?~_\-]+|)([=:<])?(.+)?$/.exec(s); - s[2] = s[2].replace(/~/g, ':'); - - // Add required attributes - if (s[1] == '!') { - ra = ra || []; - ra.push(s[2]); - } - - // Remove inherited attributes - if (s[1] == '-') { - for (i = 0; i 0 ? value : null); } - - // Handle element names - each(tn, function(s, i) { - var pr = s.charAt(0), x = 1, ru = {}; - - // Extend with default rule data - if (dr) { - if (dr.noEmpty) - ru.noEmpty = dr.noEmpty; - - if (dr.fullEnd) - ru.fullEnd = dr.fullEnd; - - if (dr.padd) - ru.padd = dr.padd; - } - - // Handle prefixes - switch (pr) { - case '-': - ru.noEmpty = true; - break; - - case '+': - ru.fullEnd = true; - break; - - case '#': - ru.padd = true; - break; - - default: - x = 0; - } - - tn[i] = s = s.substring(x); - t.validElements[s] = 1; - - // Add element name or element regex - if (/[*.?]/.test(tn[0])) { - ru.nameRE = new RegExp('^' + wildcardToRE(tn[0]) + '$'); - t.wildRules = t.wildRules || {}; - t.wildRules.push(ru); - } else { - ru.name = tn[0]; - - // Store away default rule - if (tn[0] == '@') - dr = ru; - - t.rules[s] = ru; - } - - ru.attribs = at; - - if (ra) - ru.requiredAttribs = ra; - - if (wat) { - // Build valid attributes regexp - s = ''; - each(va, function(v) { - if (s) - s += '|'; - - s += '(' + wildcardToRE(v) + ')'; - }); - ru.validAttribsRE = new RegExp('^' + s.toLowerCase() + '$'); - ru.wildAttribs = wat; - } - }); - }); - - // Build valid elements regexp - s = ''; - each(t.validElements, function(v, k) { - if (s) - s += '|'; - - if (k != '@') - s += k; - }); - t.validElementsRE = new RegExp('^(' + wildcardToRE(s.toLowerCase()) + ')$'); - - //console.debug(t.validElementsRE.toString()); - //console.dir(t.rules); - //console.dir(t.wildRules); - }, - - /** - * Finds a rule object by name. - * - * @method findRule - * @param {String} n Name to look for in rules collection. - * @return {Object} Rule object found or null if it wasn't found. - */ - findRule : function(n) { - var t = this, rl = t.rules, i, r; - - t._setup(); - - // Exact match - r = rl[n]; - if (r) - return r; - - // Try wildcards - rl = t.wildRules; - for (i = 0; i < rl.length; i++) { - if (rl[i].nameRE.test(n)) - return rl[i]; } + }); - return null; - }, - - /** - * Finds an attribute rule object by name. - * - * @method findAttribRule - * @param {Object} ru Rule object to search though. - * @param {String} n Name of the rule to retrive. - * @return {Object} Rule object of the specified attribute. - */ - findAttribRule : function(ru, n) { - var i, wa = ru.wildAttribs; + // Remove internal classes mceItem<..> + htmlParser.addAttributeFilter('class', function(nodes, name) { + var i = nodes.length, node, value; - for (i = 0; i < wa.length; i++) { - if (wa[i].nameRE.test(n)) - return wa[i]; + while (i--) { + node = nodes[i]; + value = node.attr('class').replace(/\s*mce(Item\w+|Selected)\s*/g, ''); + node.attr('class', value.length > 0 ? value : null); } + }); - return null; - }, + // Remove bookmark elements + htmlParser.addAttributeFilter('data-mce-type', function(nodes, name, args) { + var i = nodes.length, node; - /** - * Serializes the specified node into a HTML string. - * - * @method serialize - * @param {Element} n Element/Node to serialize. - * @param {Object} o Object to add serialized contents to, this object will also be passed to the event listeners. - * @return {String} Serialized HTML contents. - */ - serialize : function(n, o) { - var h, t = this, doc, oldDoc, impl, selected; - - t._setup(); - o = o || {}; - o.format = o.format || 'html'; - t.processObj = o; - - // IE looses the selected attribute on option elements so we need to store it - // See: http://support.microsoft.com/kb/829907 - if (isIE) { - selected = []; - each(n.getElementsByTagName('option'), function(n) { - var v = t.dom.getAttrib(n, 'selected'); - - selected.push(v ? v : null); - }); - } - - n = n.cloneNode(true); + while (i--) { + node = nodes[i]; - // IE looses the selected attribute on option elements so we need to restore it - if (isIE) { - each(n.getElementsByTagName('option'), function(n, i) { - t.dom.setAttrib(n, 'selected', selected[i]); - }); + if (node.attributes.map['data-mce-type'] === 'bookmark' && !args.cleanup) + node.remove(); } + }); - // Nodes needs to be attached to something in WebKit/Opera - // Older builds of Opera crashes if you attach the node to an document created dynamically - // and since we can't feature detect a crash we need to sniff the acutal build number - // This fix will make DOM ranges and make Sizzle happy! - impl = n.ownerDocument.implementation; - if (impl.createHTMLDocument && (tinymce.isOpera && opera.buildNumber() >= 1767)) { - // Create an empty HTML document - doc = impl.createHTMLDocument(""); - - // Add the element or it's children if it's a body element to the new document - each(n.nodeName == 'BODY' ? n.childNodes : [n], function(node) { - doc.body.appendChild(doc.importNode(node, true)); - }); - - // Grab first child or body element for serialization - if (n.nodeName != 'BODY') - n = doc.body.firstChild; - else - n = doc.body; - - // set the new document in DOMUtils so createElement etc works - oldDoc = t.dom.doc; - t.dom.doc = doc; - } + // Force script into CDATA sections and remove the mce- prefix also add comments around styles + htmlParser.addNodeFilter('script,style', function(nodes, name) { + var i = nodes.length, node, value; - t.key = '' + (parseInt(t.key) + 1); - - // Pre process - if (!o.no_events) { - o.node = n; - t.onPreProcess.dispatch(t, o); - } - - // Serialize HTML DOM into a string - t.writer.reset(); - t._info = o; - t._serializeNode(n, o.getInner); - - // Post process - o.content = t.writer.getContent(); - - // Restore the old document if it was changed - if (oldDoc) - t.dom.doc = oldDoc; - - if (!o.no_events) - t.onPostProcess.dispatch(t, o); - - t._postProcess(o); - o.node = null; + function trim(value) { + return value.replace(/()/g, '\n') + .replace(/^[\r\n]*|[\r\n]*$/g, '') + .replace(/^\s*(\/\/\s*|\]\]>|-->|\]\]-->)\s*$/g, ''); + }; - return tinymce.trim(o.content); - }, + while (i--) { + node = nodes[i]; + value = node.firstChild ? node.firstChild.value : ''; - // Internal functions + if (name === "script") { + // Remove mce- prefix from script elements + node.attr('type', (node.attr('type') || 'text/javascript').replace(/^mce\-/, '')); - /** - * Indents the specified content object. - * - * @param {Object} o Content object to indent. - */ - _postProcess : function(o) { - var t = this, s = t.settings, h = o.content, sc = [], p; - - if (o.format == 'html') { - // Protect some elements - p = t._protect({ - content : h, - patterns : [ - {pattern : /(]*>)(.*?)(<\/script>)/g}, - {pattern : /(]*>)(.*?)(<\/noscript>)/g}, - {pattern : /(]*>)(.*?)(<\/style>)/g}, - {pattern : /(]*>)(.*?)(<\/pre>)/g, encode : 1}, - {pattern : /()/g} - ] - }); - - h = p.content; - - // Entity encode - if (s.entity_encoding !== 'raw') - h = t._encode(h); - - // Use BR instead of   padded P elements inside editor and use

     

    outside editor -/* if (o.set) - h = h.replace(/

    \s+( | |\u00a0|
    )\s+<\/p>/g, '


    '); - else - h = h.replace(/

    \s+( | |\u00a0|
    )\s+<\/p>/g, '

    $1

    ');*/ - - // Since Gecko and Safari keeps whitespace in the DOM we need to - // remove it inorder to match other browsers. But I think Gecko and Safari is right. - // This process is only done when getting contents out from the editor. - if (!o.set) { - // We need to replace paragraph whitespace with an nbsp before indentation to keep the \u00a0 char - h = h.replace(/

    \s+<\/p>|]+)>\s+<\/p>/g, s.entity_encoding == 'numeric' ? ' 

    ' : ' 

    '); - - if (s.remove_linebreaks) { - h = h.replace(/\r?\n|\r/g, ' '); - h = h.replace(/(<[^>]+>)\s+/g, '$1 '); - h = h.replace(/\s+(<\/[^>]+>)/g, ' $1'); - h = h.replace(/<(p|h[1-6]|blockquote|hr|div|table|tbody|tr|td|body|head|html|title|meta|style|pre|script|link|object) ([^>]+)>\s+/g, '<$1 $2>'); // Trim block start - h = h.replace(/<(p|h[1-6]|blockquote|hr|div|table|tbody|tr|td|body|head|html|title|meta|style|pre|script|link|object)>\s+/g, '<$1>'); // Trim block start - h = h.replace(/\s+<\/(p|h[1-6]|blockquote|hr|div|table|tbody|tr|td|body|head|html|title|meta|style|pre|script|link|object)>/g, ''); // Trim block end - } - - // Simple indentation - if (s.apply_source_formatting && s.indent_mode == 'simple') { - // Add line breaks before and after block elements - h = h.replace(/<(\/?)(ul|hr|table|meta|link|tbody|tr|object|body|head|html|map)(|[^>]+)>\s*/g, '\n<$1$2$3>\n'); - h = h.replace(/\s*<(p|h[1-6]|blockquote|div|title|style|pre|script|td|li|area)(|[^>]+)>/g, '\n<$1$2>'); - h = h.replace(/<\/(p|h[1-6]|blockquote|div|title|style|pre|script|td|li)>\s*/g, '\n'); - h = h.replace(/\n\n/g, '\n'); - } + if (value.length > 0) + node.firstChild.value = '// '; + } else { + if (value.length > 0) + node.firstChild.value = ''; } - - h = t._unprotect(h, p); - - // Restore CDATA sections - h = h.replace(//g, ''); - - // Restore the \u00a0 character if raw mode is enabled - if (s.entity_encoding == 'raw') - h = h.replace(/

     <\/p>|]+)> <\/p>/g, '\u00a0

    '); - - // Restore noscript elements - h = h.replace(/]+|)>([\s\S]*?)<\/noscript>/g, function(v, attribs, text) { - return '' + t.dom.decode(text.replace(//g, '')) + ''; - }); } - - o.content = h; - }, - - _serializeNode : function(n, inner) { - var t = this, s = t.settings, w = t.writer, hc, el, cn, i, l, a, at, no, v, nn, ru, ar, iv, closed, keep, type; - - if (!s.node_filter || s.node_filter(n)) { - switch (n.nodeType) { - case 1: // Element - if (n.hasAttribute ? n.hasAttribute('_mce_bogus') : n.getAttribute('_mce_bogus')) - return; - - iv = keep = false; - hc = n.hasChildNodes(); - nn = n.getAttribute('_mce_name') || n.nodeName.toLowerCase(); - - // Get internal type - type = n.getAttribute('_mce_type'); - if (type) { - if (!t._info.cleanup) { - iv = true; - return; - } else - keep = 1; - } - - // Add correct prefix on IE - if (isIE) { - if (n.scopeName !== 'HTML' && n.scopeName !== 'html') - nn = n.scopeName + ':' + nn; - } - - // Remove mce prefix on IE needed for the abbr element - if (nn.indexOf('mce:') === 0) - nn = nn.substring(4); - - // Check if valid - if (!keep) { - if (!t.validElementsRE || !t.validElementsRE.test(nn) || (t.invalidElementsRE && t.invalidElementsRE.test(nn)) || inner) { - iv = true; - break; - } - } - - if (isIE) { - // Fix IE content duplication (DOM can have multiple copies of the same node) - if (s.fix_content_duplication) { - if (n._mce_serialized == t.key) - return; - - n._mce_serialized = t.key; - } - - // IE sometimes adds a / infront of the node name - if (nn.charAt(0) == '/') - nn = nn.substring(1); - } else if (isGecko) { - // Ignore br elements - if (n.nodeName === 'BR' && n.getAttribute('type') == '_moz') - return; - } - - // Check if valid child - if (s.validate_children) { - if (t.elementName && !t.schema.isValid(t.elementName, nn)) { - iv = true; - break; - } - - t.elementName = nn; - } - - ru = t.findRule(nn); - - // No valid rule for this element could be found then skip it - if (!ru) { - iv = true; - break; - } - - nn = ru.name || nn; - closed = s.closed.test(nn); - - // Skip empty nodes or empty node name in IE - if ((!hc && ru.noEmpty) || (isIE && !nn)) { - iv = true; - break; - } - - // Check required - if (ru.requiredAttribs) { - a = ru.requiredAttribs; - - for (i = a.length - 1; i >= 0; i--) { - if (this.dom.getAttrib(n, a[i]) !== '') - break; - } - - // None of the required was there - if (i == -1) { - iv = true; - break; - } - } - - w.writeStartElement(nn); - - // Add ordered attributes - if (ru.attribs) { - for (i=0, at = ru.attribs, l = at.length; i-1; i--) { - no = at[i]; - - if (no.specified) { - a = no.nodeName.toLowerCase(); - - if (s.invalid_attrs.test(a) || !ru.validAttribsRE.test(a)) - continue; - - ar = t.findAttribRule(ru, a); - v = t._getAttrib(n, ar, a); - - if (v !== null) - w.writeAttribute(a, v); - } - } - } - - // Keep type attribute - if (type && keep) - w.writeAttribute('_mce_type', type); - - // Write text from script - if (nn === 'script' && tinymce.trim(n.innerHTML)) { - w.writeText('// '); // Padd it with a comment so it will parse on older browsers - w.writeCDATA(n.innerHTML.replace(/|<\[CDATA\[|\]\]>/g, '')); // Remove comments and cdata stuctures - hc = false; - break; - } - - // Padd empty nodes with a   - if (ru.padd) { - // If it has only one bogus child, padd it anyway workaround for
    bug - if (hc && (cn = n.firstChild) && cn.nodeType === 1 && n.childNodes.length === 1) { - if (cn.hasAttribute ? cn.hasAttribute('_mce_bogus') : cn.getAttribute('_mce_bogus')) - w.writeText('\u00a0'); - } else if (!hc) - w.writeText('\u00a0'); // No children then padd it - } - - break; - - case 3: // Text - // Check if valid child - if (s.validate_children && t.elementName && !t.schema.isValid(t.elementName, '#text')) - return; - - return w.writeText(n.nodeValue); - - case 4: // CDATA - return w.writeCDATA(n.nodeValue); - - case 8: // Comment - return w.writeComment(n.nodeValue); - } - } else if (n.nodeType == 1) - hc = n.hasChildNodes(); - - if (hc && !closed) { - cn = n.firstChild; - - while (cn) { - t._serializeNode(cn); - t.elementName = nn; - cn = cn.nextSibling; + }); + + // Convert comments to cdata and handle protected comments + htmlParser.addNodeFilter('#comment', function(nodes, name) { + var i = nodes.length, node; + + while (i--) { + node = nodes[i]; + + if (node.value.indexOf('[CDATA[') === 0) { + node.name = '#cdata'; + node.type = 4; + node.value = node.value.replace(/^\[CDATA\[|\]\]$/g, ''); + } else if (node.value.indexOf('mce:protected ') === 0) { + node.name = "#text"; + node.type = 3; + node.raw = true; + node.value = unescape(node.value).substr(14); } } - - // Write element end - if (!iv) { - if (!closed) - w.writeFullEndElement(); - else - w.writeEndElement(); - } - }, - - _protect : function(o) { - var t = this; - - o.items = o.items || []; - - function enc(s) { - return s.replace(/[\r\n\\]/g, function(c) { - if (c === '\n') - return '\\n'; - else if (c === '\\') - return '\\\\'; - - return '\\r'; - }); - }; - - function dec(s) { - return s.replace(/\\[\\rn]/g, function(c) { - if (c === '\\n') - return '\n'; - else if (c === '\\\\') - return '\\'; - - return '\r'; - }); - }; - - each(o.patterns, function(p) { - o.content = dec(enc(o.content).replace(p.pattern, function(x, a, b, c) { - b = dec(b); - - if (p.encode) - b = t._encode(b); - - o.items.push(b); - return a + '' + c; - })); - }); - - return o; - }, - - _unprotect : function(h, o) { - h = h.replace(/\'); - this.count++; - }, - - /** - * String writer specific function. Enables you to write raw contents to the string. - * - * @method writeRaw - * @param {String} v String with raw contents to write. - */ - writeRaw : function(v) { - this.str += v; - }, - - /** - * String writer specific method. This encodes the raw entities of a string. - * - * @method encode - * @param {String} s String to encode. - * @return {String} String with entity encoding of the raw elements like <>&". - */ - encode : function(s) { - return s.replace(/[<>&"]/g, function(v) { - switch (v) { - case '<': - return '<'; - - case '>': - return '>'; - - case '&': - return '&'; - - case '"': - return '"'; - } - - return v; - }); - }, - - /** - * Returns a string representation of the elements/nodes written. - * - * @method getContent - * @return {String} String representation of the written elements/nodes. - */ - getContent : function() { - return this.str; - }, - - _writeAttributesEnd : function(s) { - if (!this.inAttr) - return; - - this.inAttr = false; - - if (s && this.elementCount == this.count) { - this.writeRaw(' />'); - return false; - } - - this.writeRaw('>'); - - return true; - } - }); -})(tinymce); diff --git a/js/tiny_mce/classes/dom/TreeWalker.js b/js/tiny_mce/classes/dom/TreeWalker.js index c7fc06047c..3436aae367 100644 --- a/js/tiny_mce/classes/dom/TreeWalker.js +++ b/js/tiny_mce/classes/dom/TreeWalker.js @@ -59,6 +59,6 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { * @return {Node} Current node where the walker is after moving to the previous node. */ this.prev = function(shallow) { - return (node = findSibling(node, 'lastChild', 'lastSibling', shallow)); + return (node = findSibling(node, 'lastChild', 'previousSibling', shallow)); }; }; diff --git a/js/tiny_mce/classes/dom/TridentSelection.js b/js/tiny_mce/classes/dom/TridentSelection.js index 30d1ae39f7..5f37055313 100644 --- a/js/tiny_mce/classes/dom/TridentSelection.js +++ b/js/tiny_mce/classes/dom/TridentSelection.js @@ -10,366 +10,434 @@ (function() { function Selection(selection) { - var t = this, invisibleChar = '\uFEFF', range, lastIERng, dom = selection.dom, TRUE = true, FALSE = false; + var self = this, dom = selection.dom, TRUE = true, FALSE = false; + + function getPosition(rng, start) { + var checkRng, startIndex = 0, endIndex, inside, + children, child, offset, index, position = -1, parent; + + // Setup test range, collapse it and get the parent + checkRng = rng.duplicate(); + checkRng.collapse(start); + parent = checkRng.parentElement(); + + // Check if the selection is within the right document + if (parent.ownerDocument !== selection.dom.doc) + return; + + // IE will report non editable elements as it's parent so look for an editable one + while (parent.contentEditable === "false") { + parent = parent.parentNode; + } + + // If parent doesn't have any children then return that we are inside the element + if (!parent.hasChildNodes()) { + return {node : parent, inside : 1}; + } + + // Setup node list and endIndex + children = parent.children; + endIndex = children.length - 1; + + // Perform a binary search for the position + while (startIndex <= endIndex) { + index = Math.floor((startIndex + endIndex) / 2); + + // Move selection to node and compare the ranges + child = children[index]; + checkRng.moveToElementText(child); + position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng); + + // Before/after or an exact match + if (position > 0) { + endIndex = index - 1; + } else if (position < 0) { + startIndex = index + 1; + } else { + return {node : child}; + } + } + + // Check if child position is before or we didn't find a position + if (position < 0) { + // No element child was found use the parent element and the offset inside that + if (!child) { + checkRng.moveToElementText(parent); + checkRng.collapse(true); + child = parent; + inside = true; + } else + checkRng.collapse(false); + + checkRng.setEndPoint(start ? 'EndToStart' : 'EndToEnd', rng); + + // Fix for edge case:
    ..
    ab|c
    + if (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) > 0) { + checkRng = rng.duplicate(); + checkRng.collapse(start); + + offset = -1; + while (parent == checkRng.parentElement()) { + if (checkRng.move('character', -1) == 0) + break; + + offset++; + } + } + + offset = offset || checkRng.text.replace('\r\n', ' ').length; + } else { + // Child position is after the selection endpoint + checkRng.collapse(true); + checkRng.setEndPoint(start ? 'StartToStart' : 'StartToEnd', rng); + + // Get the length of the text to find where the endpoint is relative to it's container + offset = checkRng.text.replace('\r\n', ' ').length; + } + + return {node : child, position : position, offset : offset, inside : inside}; + }; // Returns a W3C DOM compatible range object by using the IE Range API function getRange() { - var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed; + var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark, fail; // If selection is outside the current document just return an empty range element = ieRange.item ? ieRange.item(0) : ieRange.parentElement(); if (element.ownerDocument != dom.doc) return domRange; - // Handle control selection or text selection of a image - if (ieRange.item || !element.hasChildNodes()) { + collapsed = selection.isCollapsed(); + + // Handle control selection + if (ieRange.item) { domRange.setStart(element.parentNode, dom.nodeIndex(element)); domRange.setEnd(domRange.startContainer, domRange.startOffset + 1); return domRange; } - collapsed = selection.isCollapsed(); - function findEndPoint(start) { - var marker, container, offset, nodes, startIndex = 0, endIndex, index, parent, checkRng, position; - - // Setup temp range and collapse it - checkRng = ieRange.duplicate(); - checkRng.collapse(start); + var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue; - // Create marker and insert it at the end of the endpoints parent - marker = dom.create('a'); - parent = checkRng.parentElement(); + container = endPoint.node; + offset = endPoint.offset; - // If parent doesn't have any children then set the container to that parent and the index to 0 - if (!parent.hasChildNodes()) { - domRange[start ? 'setStart' : 'setEnd'](parent, 0); + if (endPoint.inside && !container.hasChildNodes()) { + domRange[start ? 'setStart' : 'setEnd'](container, 0); return; } - parent.appendChild(marker); - checkRng.moveToElementText(marker); - position = ieRange.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', checkRng); - if (position > 0) { - // The position is after the end of the parent element. - // This is the case where IE puts the caret to the left edge of a table. - domRange[start ? 'setStartAfter' : 'setEndAfter'](parent); - dom.remove(marker); + if (offset === undef) { + domRange[start ? 'setStartBefore' : 'setEndAfter'](container); return; } - // Setup node list and endIndex - nodes = tinymce.grep(parent.childNodes); - endIndex = nodes.length - 1; - // Perform a binary search for the position - while (startIndex <= endIndex) { - index = Math.floor((startIndex + endIndex) / 2); - - // Insert marker and check it's position relative to the selection - parent.insertBefore(marker, nodes[index]); - checkRng.moveToElementText(marker); - position = ieRange.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', checkRng); - if (position > 0) { - // Marker is to the right - startIndex = index + 1; - } else if (position < 0) { - // Marker is to the left - endIndex = index - 1; - } else { - // Maker is where we are - found = true; - break; + if (endPoint.position < 0) { + sibling = endPoint.inside ? container.firstChild : container.nextSibling; + + if (!sibling) { + domRange[start ? 'setStartAfter' : 'setEndAfter'](container); + return; } - } - // Setup container - container = position > 0 || index == 0 ? marker.nextSibling : marker.previousSibling; + if (!offset) { + if (sibling.nodeType == 3) + domRange[start ? 'setStart' : 'setEnd'](sibling, 0); + else + domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling); - // Handle element selection - if (container.nodeType == 1) { - dom.remove(marker); + return; + } - // Find offset and container - offset = dom.nodeIndex(container); - container = container.parentNode; + // Find the text node and offset + while (sibling) { + nodeValue = sibling.nodeValue; + textNodeOffset += nodeValue.length; + + // We are at or passed the position we where looking for + if (textNodeOffset >= offset) { + container = sibling; + textNodeOffset -= offset; + textNodeOffset = nodeValue.length - textNodeOffset; + break; + } - // Move the offset if we are setting the end or the position is after an element - if (!start || index > 0) - offset++; + sibling = sibling.nextSibling; + } } else { - // Calculate offset within text node - if (position > 0 || index == 0) { - checkRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', ieRange); - offset = checkRng.text.length; - } else { - checkRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', ieRange); - offset = container.nodeValue.length - checkRng.text.length; + // Find the text node and offset + sibling = container.previousSibling; + + if (!sibling) + return domRange[start ? 'setStartBefore' : 'setEndBefore'](container); + + // If there isn't any text to loop then use the first position + if (!offset) { + if (container.nodeType == 3) + domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length); + else + domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling); + + return; } - dom.remove(marker); + while (sibling) { + textNodeOffset += sibling.nodeValue.length; + + // We are at or passed the position we where looking for + if (textNodeOffset >= offset) { + container = sibling; + textNodeOffset -= offset; + break; + } + + sibling = sibling.previousSibling; + } } - domRange[start ? 'setStart' : 'setEnd'](container, offset); + domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset); }; - // Find start point - findEndPoint(true); + try { + // Find start point + findEndPoint(true); - // Find end point if needed - if (!collapsed) - findEndPoint(); + // Find end point if needed + if (!collapsed) + findEndPoint(); + } catch (ex) { + // IE has a nasty bug where text nodes might throw "invalid argument" when you + // access the nodeValue or other properties of text nodes. This seems to happend when + // text nodes are split into two nodes by a delete/backspace call. So lets detect it and try to fix it. + if (ex.number == -2147024809) { + // Get the current selection + bookmark = self.getBookmark(2); + + // Get start element + tmpRange = ieRange.duplicate(); + tmpRange.collapse(true); + element = tmpRange.parentElement(); + + // Get end element + if (!collapsed) { + tmpRange = ieRange.duplicate(); + tmpRange.collapse(false); + element2 = tmpRange.parentElement(); + element2.innerHTML = element2.innerHTML; + } - return domRange; - }; + // Remove the broken elements + element.innerHTML = element.innerHTML; - this.addRange = function(rng) { - var ieRng, ieRng2, doc = selection.dom.doc, body = doc.body, startPos, endPos, sc, so, ec, eo, marker, lastIndex, skipStart, skipEnd; + // Restore the selection + self.moveToBookmark(bookmark); - this.destroy(); + // Since the range has moved we need to re-get it + ieRange = selection.getRng(); - // Setup some shorter versions - sc = rng.startContainer; - so = rng.startOffset; - ec = rng.endContainer; - eo = rng.endOffset; - ieRng = body.createTextRange(); + // Find start point + findEndPoint(true); - // If document selection move caret to first node in document - if (sc == doc || ec == doc) { - ieRng = body.createTextRange(); - ieRng.collapse(); - ieRng.select(); - return; + // Find end point if needed + if (!collapsed) + findEndPoint(); + } else + throw ex; // Throw other errors } - // If child index resolve it - if (sc.nodeType == 1 && sc.hasChildNodes()) { - lastIndex = sc.childNodes.length - 1; + return domRange; + }; + + this.getBookmark = function(type) { + var rng = selection.getRng(), start, end, bookmark = {}; - // Index is higher that the child count then we need to jump over the start container - if (so > lastIndex) { - skipStart = 1; - sc = sc.childNodes[lastIndex]; - } else - sc = sc.childNodes[so]; + function getIndexes(node) { + var node, parent, root, children, i, indexes = []; - // Child was text node then move offset to start of it - if (sc.nodeType == 3) - so = 0; - } + parent = node.parentNode; + root = dom.getRoot().parentNode; - // If child index resolve it - if (ec.nodeType == 1 && ec.hasChildNodes()) { - lastIndex = ec.childNodes.length - 1; + while (parent != root && parent.nodeType !== 9) { + children = parent.children; - if (eo == 0) { - skipEnd = 1; - ec = ec.childNodes[0]; - } else { - ec = ec.childNodes[Math.min(lastIndex, eo - 1)]; + i = children.length; + while (i--) { + if (node === children[i]) { + indexes.push(i); + break; + } + } - // Child was text node then move offset to end of text node - if (ec.nodeType == 3) - eo = ec.nodeValue.length; + node = parent; + parent = parent.parentNode; } + + return indexes; + }; + + function getBookmarkEndPoint(start) { + var position; + + position = getPosition(rng, start); + if (position) { + return { + position : position.position, + offset : position.offset, + indexes : getIndexes(position.node), + inside : position.inside + }; + } + }; + + // Non ubstructive bookmark + if (type === 2) { + // Handle text selection + if (!rng.item) { + bookmark.start = getBookmarkEndPoint(true); + + if (!selection.isCollapsed()) + bookmark.end = getBookmarkEndPoint(); + } else + bookmark.start = {ctrl : true, indexes : getIndexes(rng.item(0))}; } - // Single element selection - if (sc == ec && sc.nodeType == 1) { - // Make control selection for some elements - if (/^(IMG|TABLE)$/.test(sc.nodeName) && so != eo) { - ieRng = body.createControlRange(); - ieRng.addElement(sc); - } else { - ieRng = body.createTextRange(); + return bookmark; + }; - // Padd empty elements with invisible character - if (!sc.hasChildNodes() && sc.canHaveHTML) - sc.innerHTML = invisibleChar; + this.moveToBookmark = function(bookmark) { + var rng, body = dom.doc.body; - // Select element contents - ieRng.moveToElementText(sc); + function resolveIndexes(indexes) { + var node, i, idx, children; - // If it's only containing a padding remove it so the caret remains - if (sc.innerHTML == invisibleChar) { - ieRng.collapse(TRUE); - sc.removeChild(sc.firstChild); + node = dom.getRoot(); + for (i = indexes.length - 1; i >= 0; i--) { + children = node.children; + idx = indexes[i]; + + if (idx <= children.length - 1) { + node = children[idx]; } } - if (so == eo) - ieRng.collapse(eo <= rng.endContainer.childNodes.length - 1); + return node; + }; + + function setBookmarkEndPoint(start) { + var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef; - ieRng.select(); - ieRng.scrollIntoView(); - return; - } + if (endPoint) { + moveLeft = endPoint.position > 0; - // Create range and marker - ieRng = body.createTextRange(); - marker = doc.createElement('span'); - marker.innerHTML = ' '; - - // Set start of range to startContainer/startOffset - if (sc.nodeType == 3) { - // Insert marker after/before startContainer - if (skipStart) - dom.insertAfter(marker, sc); - else - sc.parentNode.insertBefore(marker, sc); - - // Select marker the caret to offset position - ieRng.moveToElementText(marker); - marker.parentNode.removeChild(marker); - - // Move if we need to, moving it 0 characters actually moves it! - if (so > 0) - ieRng.move('character', so); - } else { - ieRng.moveToElementText(sc); + moveRng = body.createTextRange(); + moveRng.moveToElementText(resolveIndexes(endPoint.indexes)); - if (skipStart) - ieRng.collapse(FALSE); - } + offset = endPoint.offset; + if (offset !== undef) { + moveRng.collapse(endPoint.inside || moveLeft); + moveRng.moveStart('character', moveLeft ? -offset : offset); + } else + moveRng.collapse(start); - // If same text container then we can do a more simple move - if (sc == ec && sc.nodeType == 3) { - try { - ieRng.moveEnd('character', eo - so); - ieRng.select(); - ieRng.scrollIntoView(); - } catch (ex) { - // Some times a Runtime error of the 800a025e type gets thrown - // especially when the caret is placed before a table. - // This is a somewhat strange location for the caret. - // TODO: Find a better solution for this would possible require a rewrite of the setRng method - } + rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng); - return; - } + if (start) + rng.collapse(true); + } + }; - // Set end of range to endContainer/endOffset - ieRng2 = body.createTextRange(); - if (ec.nodeType == 3) { - // Insert marker after/before startContainer - ec.parentNode.insertBefore(marker, ec); - - // Move selection to end marker and move caret to end offset - ieRng2.moveToElementText(marker); - marker.parentNode.removeChild(marker); - ieRng2.move('character', eo); - ieRng.setEndPoint('EndToStart', ieRng2); - } else { - ieRng2.moveToElementText(ec); - ieRng2.collapse(!!skipEnd); - ieRng.setEndPoint('EndToEnd', ieRng2); + if (bookmark.start) { + if (bookmark.start.ctrl) { + rng = body.createControlRange(); + rng.addElement(resolveIndexes(bookmark.start.indexes)); + rng.select(); + } else { + rng = body.createTextRange(); + setBookmarkEndPoint(true); + setBookmarkEndPoint(); + rng.select(); + } } - - ieRng.select(); - ieRng.scrollIntoView(); }; - this.getRangeAt = function() { - // Setup new range if the cache is empty - if (!range || !tinymce.dom.RangeUtils.compareRanges(lastIERng, selection.getRng())) { - range = getRange(); + this.addRange = function(rng) { + var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, doc = selection.dom.doc, body = doc.body; - // Store away text range for next call - lastIERng = selection.getRng(); - } + function setEndPoint(start) { + var container, offset, marker, tmpRng, nodes; - // IE will say that the range is equal then produce an invalid argument exception - // if you perform specific operations in a keyup event. For example Ctrl+Del. - // This hack will invalidate the range cache if the exception occurs - try { - range.startContainer.nextSibling; - } catch (ex) { - range = getRange(); - lastIERng = null; - } + marker = dom.create('a'); + container = start ? startContainer : endContainer; + offset = start ? startOffset : endOffset; + tmpRng = ieRng.duplicate(); - // Return cached range - return range; - }; + if (container == doc || container == doc.documentElement) { + container = body; + offset = 0; + } - this.destroy = function() { - // Destroy cached range and last IE range to avoid memory leaks - lastIERng = range = null; - }; + if (container.nodeType == 3) { + container.parentNode.insertBefore(marker, container); + tmpRng.moveToElementText(marker); + tmpRng.moveStart('character', offset); + dom.remove(marker); + ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); + } else { + nodes = container.childNodes; - // IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode - if (selection.dom.boxModel) { - (function() { - var doc = dom.doc, body = doc.body, started, startRng; + if (nodes.length) { + if (offset >= nodes.length) { + dom.insertAfter(marker, nodes[nodes.length - 1]); + } else { + container.insertBefore(marker, nodes[offset]); + } - // Make HTML element unselectable since we are going to handle selection by hand - doc.documentElement.unselectable = TRUE; + tmpRng.moveToElementText(marker); + } else { + // Empty node selection for example
    |
    + marker = doc.createTextNode('\uFEFF'); + container.appendChild(marker); + tmpRng.moveToElementText(marker.parentNode); + tmpRng.collapse(TRUE); + } - // Return range from point or null if it failed - function rngFromPoint(x, y) { - var rng = body.createTextRange(); + ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); + dom.remove(marker); + } + } + + // Setup some shorter versions + startContainer = rng.startContainer; + startOffset = rng.startOffset; + endContainer = rng.endContainer; + endOffset = rng.endOffset; + ieRng = body.createTextRange(); + // If single element selection then try making a control selection out of it + if (startContainer == endContainer && startContainer.nodeType == 1 && startOffset == endOffset - 1) { + if (startOffset == endOffset - 1) { try { - rng.moveToPoint(x, y); + ctrlRng = body.createControlRange(); + ctrlRng.addElement(startContainer.childNodes[startOffset]); + ctrlRng.select(); + return; } catch (ex) { - // IE sometimes throws and exception, so lets just ignore it - rng = null; + // Ignore } + } + } - return rng; - }; - - // Fires while the selection is changing - function selectionChange(e) { - var pointRng; - - // Check if the button is down or not - if (e.button) { - // Create range from mouse position - pointRng = rngFromPoint(e.x, e.y); - - if (pointRng) { - // Check if pointRange is before/after selection then change the endPoint - if (pointRng.compareEndPoints('StartToStart', startRng) > 0) - pointRng.setEndPoint('StartToStart', startRng); - else - pointRng.setEndPoint('EndToEnd', startRng); + // Set start/end point of selection + setEndPoint(true); + setEndPoint(); - pointRng.select(); - } - } else - endSelection(); - } + // Select the new range and scroll it into view + ieRng.select(); + }; - // Removes listeners - function endSelection() { - dom.unbind(doc, 'mouseup', endSelection); - dom.unbind(doc, 'mousemove', selectionChange); - started = 0; - }; - - // Detect when user selects outside BODY - dom.bind(doc, 'mousedown', function(e) { - if (e.target.nodeName === 'HTML') { - if (started) - endSelection(); - - started = 1; - - // Setup start position - startRng = rngFromPoint(e.x, e.y); - if (startRng) { - // Listen for selection change events - dom.bind(doc, 'mouseup', endSelection); - dom.bind(doc, 'mousemove', selectionChange); - - startRng.select(); - } - } - }); - })(); - } + // Expose range method + this.getRangeAt = getRange; }; // Expose the selection object diff --git a/js/tiny_mce/classes/dom/XMLWriter.js b/js/tiny_mce/classes/dom/XMLWriter.js deleted file mode 100644 index fe361a1991..0000000000 --- a/js/tiny_mce/classes/dom/XMLWriter.js +++ /dev/null @@ -1,165 +0,0 @@ -/** - * XMLWriter.js - * - * Copyright 2009, Moxiecode Systems AB - * Released under LGPL License. - * - * License: http://tinymce.moxiecode.com/license - * Contributing: http://tinymce.moxiecode.com/contributing - */ - -(function(tinymce) { - /** - * This class writes nodes into a XML document structure. This structure can then be - * serialized down to a HTML string later on. - * @class tinymce.dom.XMLWriter - */ - tinymce.create('tinymce.dom.XMLWriter', { - node : null, - - /** - * Constructs a new XMLWriter. - * - * @constructor - * @method XMLWriter - * @param {Object} s Optional settings object. - */ - XMLWriter : function(s) { - // Get XML document - function getXML() { - var i = document.implementation; - - if (!i || !i.createDocument) { - // Try IE objects - try {return new ActiveXObject('MSXML2.DOMDocument');} catch (ex) {} - try {return new ActiveXObject('Microsoft.XmlDom');} catch (ex) {} - } else - return i.createDocument('', '', null); - }; - - this.doc = getXML(); - - // Since Opera and WebKit doesn't escape > into > we need to do it our self to normalize the output for all browsers - this.valid = tinymce.isOpera || tinymce.isWebKit; - - this.reset(); - }, - - /** - * Resets the writer so it can be reused the contents of the writer is cleared. - * - * @method reset - */ - reset : function() { - var t = this, d = t.doc; - - if (d.firstChild) - d.removeChild(d.firstChild); - - t.node = d.appendChild(d.createElement("html")); - }, - - /** - * Writes the start of an element like for example: /g, '%MCGT%'); - - this.node.setAttribute(n, v); - }, - - /** - * Write the end of a element. This will add a short end for elements with out children like for example a img element. - * - * @method writeEndElement - */ - writeEndElement : function() { - this.node = this.node.parentNode; - }, - - /** - * Writes the end of a element. This will add a full end to the element even if it didn't have any children. - * - * @method writeFullEndElement - */ - writeFullEndElement : function() { - var t = this, n = t.node; - - n.appendChild(t.doc.createTextNode("")); - t.node = n.parentNode; - }, - - /** - * Writes a text node value. - * - * @method writeText - * @param {String} v Value to append as a text node. - */ - writeText : function(v) { - if (this.valid) - v = v.replace(/>/g, '%MCGT%'); - - this.node.appendChild(this.doc.createTextNode(v)); - }, - - /** - * Writes a CDATA section. - * - * @method writeCDATA - * @param {String} v Value to write in CDATA. - */ - writeCDATA : function(v) { - this.node.appendChild(this.doc.createCDATASection(v)); - }, - - /** - * Writes a comment. - * - * @method writeComment - * @param {String} v Value of the comment. - */ - writeComment : function(v) { - // Fix for bug #2035694 - if (tinymce.isIE) - v = v.replace(/^\-|\-$/g, ' '); - - this.node.appendChild(this.doc.createComment(v.replace(/\-\-/g, ' '))); - }, - - /** - * Returns a string representation of the elements/nodes written. - * - * @method getContent - * @return {String} String representation of the written elements/nodes. - */ - getContent : function() { - var h; - - h = this.doc.xml || new XMLSerializer().serializeToString(this.doc); - h = h.replace(/<\?[^?]+\?>||<\/html>||]+>/g, ''); - h = h.replace(/ ?\/>/g, ' />'); - - if (this.valid) - h = h.replace(/\%MCGT%/g, '>'); - - return h; - } - }); -})(tinymce); diff --git a/js/tiny_mce/classes/firebug/FIREBUG.LICENSE b/js/tiny_mce/classes/firebug/FIREBUG.LICENSE new file mode 100644 index 0000000000..8b9c44ab72 --- /dev/null +++ b/js/tiny_mce/classes/firebug/FIREBUG.LICENSE @@ -0,0 +1,30 @@ +Software License Agreement (BSD License) + +Copyright (c) 2007, Parakey Inc. +All rights reserved. + +Redistribution and use of this software in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + +* Neither the name of Parakey Inc. nor the names of its + contributors may be used to endorse or promote products + derived from this software without specific prior + written permission of Parakey Inc. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/js/tiny_mce/classes/html/DomParser.js b/js/tiny_mce/classes/html/DomParser.js new file mode 100644 index 0000000000..dadbf0891e --- /dev/null +++ b/js/tiny_mce/classes/html/DomParser.js @@ -0,0 +1,577 @@ +/** + * DomParser.js + * + * Copyright 2010, Moxiecode Systems AB + * Released under LGPL License. + * + * License: http://tinymce.moxiecode.com/license + * Contributing: http://tinymce.moxiecode.com/contributing + */ + +(function(tinymce) { + var Node = tinymce.html.Node; + + /** + * This class parses HTML code into a DOM like structure of nodes it will remove redundant whitespace and make + * sure that the node tree is valid according to the specified schema. So for example:

    a

    b

    c

    will become

    a

    b

    c

    + * + * @example + * var parser = new tinymce.html.DomParser({validate: true}, schema); + * var rootNode = parser.parse('

    content

    '); + * + * @class tinymce.html.DomParser + * @version 3.4 + */ + + /** + * Constructs a new DomParser instance. + * + * @constructor + * @method DomParser + * @param {Object} settings Name/value collection of settings. comment, cdata, text, start and end are callbacks. + * @param {tinymce.html.Schema} schema HTML Schema class to use when parsing. + */ + tinymce.html.DomParser = function(settings, schema) { + var self = this, nodeFilters = {}, attributeFilters = [], matchedNodes = {}, matchedAttributes = {}; + + settings = settings || {}; + settings.validate = "validate" in settings ? settings.validate : true; + settings.root_name = settings.root_name || 'body'; + self.schema = schema = schema || new tinymce.html.Schema(); + + function fixInvalidChildren(nodes) { + var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i, + childClone, nonEmptyElements, nonSplitableElements, sibling, nextNode; + + nonSplitableElements = tinymce.makeMap('tr,td,th,tbody,thead,tfoot,table'); + nonEmptyElements = schema.getNonEmptyElements(); + + for (ni = 0; ni < nodes.length; ni++) { + node = nodes[ni]; + + // Already removed + if (!node.parent) + continue; + + // Get list of all parent nodes until we find a valid parent to stick the child into + parents = [node]; + for (parent = node.parent; parent && !schema.isValidChild(parent.name, node.name) && !nonSplitableElements[parent.name]; parent = parent.parent) + parents.push(parent); + + // Found a suitable parent + if (parent && parents.length > 1) { + // Reverse the array since it makes looping easier + parents.reverse(); + + // Clone the related parent and insert that after the moved node + newParent = currentNode = self.filterNode(parents[0].clone()); + + // Start cloning and moving children on the left side of the target node + for (i = 0; i < parents.length - 1; i++) { + if (schema.isValidChild(currentNode.name, parents[i].name)) { + tempNode = self.filterNode(parents[i].clone()); + currentNode.append(tempNode); + } else + tempNode = currentNode; + + for (childNode = parents[i].firstChild; childNode && childNode != parents[i + 1]; ) { + nextNode = childNode.next; + tempNode.append(childNode); + childNode = nextNode; + } + + currentNode = tempNode; + } + + if (!newParent.isEmpty(nonEmptyElements)) { + parent.insert(newParent, parents[0], true); + parent.insert(node, newParent); + } else { + parent.insert(node, parents[0], true); + } + + // Check if the element is empty by looking through it's contents and special treatment for


    + parent = parents[0]; + if (parent.isEmpty(nonEmptyElements) || parent.firstChild === parent.lastChild && parent.firstChild.name === 'br') { + parent.empty().remove(); + } + } else if (node.parent) { + // If it's an LI try to find a UL/OL for it or wrap it + if (node.name === 'li') { + sibling = node.prev; + if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) { + sibling.append(node); + continue; + } + + sibling = node.next; + if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) { + sibling.insert(node, sibling.firstChild, true); + continue; + } + + node.wrap(self.filterNode(new Node('ul', 1))); + continue; + } + + // Try wrapping the element in a DIV + if (schema.isValidChild(node.parent.name, 'div') && schema.isValidChild('div', node.name)) { + node.wrap(self.filterNode(new Node('div', 1))); + } else { + // We failed wrapping it, then remove or unwrap it + if (node.name === 'style' || node.name === 'script') + node.empty().remove(); + else + node.unwrap(); + } + } + } + }; + + /** + * Runs the specified node though the element and attributes filters. + * + * @param {tinymce.html.Node} Node the node to run filters on. + * @return {tinymce.html.Node} The passed in node. + */ + self.filterNode = function(node) { + var i, name, list; + + // Run element filters + if (name in nodeFilters) { + list = matchedNodes[name]; + + if (list) + list.push(node); + else + matchedNodes[name] = [node]; + } + + // Run attribute filters + i = attributeFilters.length; + while (i--) { + name = attributeFilters[i].name; + + if (name in node.attributes.map) { + list = matchedAttributes[name]; + + if (list) + list.push(node); + else + matchedAttributes[name] = [node]; + } + } + + return node; + }; + + /** + * Adds a node filter function to the parser, the parser will collect the specified nodes by name + * and then execute the callback ones it has finished parsing the document. + * + * @example + * parser.addNodeFilter('p,h1', function(nodes, name) { + * for (var i = 0; i < nodes.length; i++) { + * console.log(nodes[i].name); + * } + * }); + * @method addNodeFilter + * @method {String} name Comma separated list of nodes to collect. + * @param {function} callback Callback function to execute once it has collected nodes. + */ + self.addNodeFilter = function(name, callback) { + tinymce.each(tinymce.explode(name), function(name) { + var list = nodeFilters[name]; + + if (!list) + nodeFilters[name] = list = []; + + list.push(callback); + }); + }; + + /** + * Adds a attribute filter function to the parser, the parser will collect nodes that has the specified attributes + * and then execute the callback ones it has finished parsing the document. + * + * @example + * parser.addAttributeFilter('src,href', function(nodes, name) { + * for (var i = 0; i < nodes.length; i++) { + * console.log(nodes[i].name); + * } + * }); + * @method addAttributeFilter + * @method {String} name Comma separated list of nodes to collect. + * @param {function} callback Callback function to execute once it has collected nodes. + */ + self.addAttributeFilter = function(name, callback) { + tinymce.each(tinymce.explode(name), function(name) { + var i; + + for (i = 0; i < attributeFilters.length; i++) { + if (attributeFilters[i].name === name) { + attributeFilters[i].callbacks.push(callback); + return; + } + } + + attributeFilters.push({name: name, callbacks: [callback]}); + }); + }; + + /** + * Parses the specified HTML string into a DOM like node tree and returns the result. + * + * @example + * var rootNode = new DomParser({...}).parse('text'); + * @method parse + * @param {String} html Html string to sax parse. + * @param {Object} args Optional args object that gets passed to all filter functions. + * @return {tinymce.html.Node} Root node containing the tree. + */ + self.parse = function(html, args) { + var parser, rootNode, node, nodes, i, l, fi, fl, list, name, validate, + blockElements, startWhiteSpaceRegExp, invalidChildren = [], + endWhiteSpaceRegExp, allWhiteSpaceRegExp, whiteSpaceElements, children, nonEmptyElements, rootBlockName; + + args = args || {}; + matchedNodes = {}; + matchedAttributes = {}; + blockElements = tinymce.extend(tinymce.makeMap('script,style,head,html,body,title,meta,param'), schema.getBlockElements()); + nonEmptyElements = schema.getNonEmptyElements(); + children = schema.children; + validate = settings.validate; + rootBlockName = "forced_root_block" in args ? args.forced_root_block : settings.forced_root_block; + + whiteSpaceElements = schema.getWhiteSpaceElements(); + startWhiteSpaceRegExp = /^[ \t\r\n]+/; + endWhiteSpaceRegExp = /[ \t\r\n]+$/; + allWhiteSpaceRegExp = /[ \t\r\n]+/g; + + function addRootBlocks() { + var node = rootNode.firstChild, next, rootBlockNode; + + while (node) { + next = node.next; + + if (node.type == 3 || (node.type == 1 && node.name !== 'p' && !blockElements[node.name] && !node.attr('data-mce-type'))) { + if (!rootBlockNode) { + // Create a new root block element + rootBlockNode = createNode(rootBlockName, 1); + rootNode.insert(rootBlockNode, node); + rootBlockNode.append(node); + } else + rootBlockNode.append(node); + } else { + rootBlockNode = null; + } + + node = next; + }; + }; + + function createNode(name, type) { + var node = new Node(name, type), list; + + if (name in nodeFilters) { + list = matchedNodes[name]; + + if (list) + list.push(node); + else + matchedNodes[name] = [node]; + } + + return node; + }; + + function removeWhitespaceBefore(node) { + var textNode, textVal, sibling; + + for (textNode = node.prev; textNode && textNode.type === 3; ) { + textVal = textNode.value.replace(endWhiteSpaceRegExp, ''); + + if (textVal.length > 0) { + textNode.value = textVal; + textNode = textNode.prev; + } else { + sibling = textNode.prev; + textNode.remove(); + textNode = sibling; + } + } + }; + + parser = new tinymce.html.SaxParser({ + validate : validate, + fix_self_closing : !validate, // Let the DOM parser handle
  • in
  • or

    in

    for better results + + cdata: function(text) { + node.append(createNode('#cdata', 4)).value = text; + }, + + text: function(text, raw) { + var textNode; + + // Trim all redundant whitespace on non white space elements + if (!whiteSpaceElements[node.name]) { + text = text.replace(allWhiteSpaceRegExp, ' '); + + if (node.lastChild && blockElements[node.lastChild.name]) + text = text.replace(startWhiteSpaceRegExp, ''); + } + + // Do we need to create the node + if (text.length !== 0) { + textNode = createNode('#text', 3); + textNode.raw = !!raw; + node.append(textNode).value = text; + } + }, + + comment: function(text) { + node.append(createNode('#comment', 8)).value = text; + }, + + pi: function(name, text) { + node.append(createNode(name, 7)).value = text; + removeWhitespaceBefore(node); + }, + + doctype: function(text) { + var newNode; + + newNode = node.append(createNode('#doctype', 10)); + newNode.value = text; + removeWhitespaceBefore(node); + }, + + start: function(name, attrs, empty) { + var newNode, attrFiltersLen, elementRule, textNode, attrName, text, sibling, parent; + + elementRule = validate ? schema.getElementRule(name) : {}; + if (elementRule) { + newNode = createNode(elementRule.outputName || name, 1); + newNode.attributes = attrs; + newNode.shortEnded = empty; + + node.append(newNode); + + // Check if node is valid child of the parent node is the child is + // unknown we don't collect it since it's probably a custom element + parent = children[node.name]; + if (parent && children[newNode.name] && !parent[newNode.name]) + invalidChildren.push(newNode); + + attrFiltersLen = attributeFilters.length; + while (attrFiltersLen--) { + attrName = attributeFilters[attrFiltersLen].name; + + if (attrName in attrs.map) { + list = matchedAttributes[attrName]; + + if (list) + list.push(newNode); + else + matchedAttributes[attrName] = [newNode]; + } + } + + // Trim whitespace before block + if (blockElements[name]) + removeWhitespaceBefore(newNode); + + // Change current node if the element wasn't empty i.e not
    or + if (!empty) + node = newNode; + } + }, + + end: function(name) { + var textNode, elementRule, text, sibling, tempNode; + + elementRule = validate ? schema.getElementRule(name) : {}; + if (elementRule) { + if (blockElements[name]) { + if (!whiteSpaceElements[node.name]) { + // Trim whitespace at beginning of block + for (textNode = node.firstChild; textNode && textNode.type === 3; ) { + text = textNode.value.replace(startWhiteSpaceRegExp, ''); + + if (text.length > 0) { + textNode.value = text; + textNode = textNode.next; + } else { + sibling = textNode.next; + textNode.remove(); + textNode = sibling; + } + } + + // Trim whitespace at end of block + for (textNode = node.lastChild; textNode && textNode.type === 3; ) { + text = textNode.value.replace(endWhiteSpaceRegExp, ''); + + if (text.length > 0) { + textNode.value = text; + textNode = textNode.prev; + } else { + sibling = textNode.prev; + textNode.remove(); + textNode = sibling; + } + } + } + + // Trim start white space + textNode = node.prev; + if (textNode && textNode.type === 3) { + text = textNode.value.replace(startWhiteSpaceRegExp, ''); + + if (text.length > 0) + textNode.value = text; + else + textNode.remove(); + } + } + + // Handle empty nodes + if (elementRule.removeEmpty || elementRule.paddEmpty) { + if (node.isEmpty(nonEmptyElements)) { + if (elementRule.paddEmpty) + node.empty().append(new Node('#text', '3')).value = '\u00a0'; + else { + // Leave nodes that have a name like + if (!node.attributes.map.name) { + tempNode = node.parent; + node.empty().remove(); + node = tempNode; + return; + } + } + } + } + + node = node.parent; + } + } + }, schema); + + rootNode = node = new Node(args.context || settings.root_name, 11); + + parser.parse(html); + + // Fix invalid children or report invalid children in a contextual parsing + if (validate && invalidChildren.length) { + if (!args.context) + fixInvalidChildren(invalidChildren); + else + args.invalid = true; + } + + // Wrap nodes in the root into block elements if the root is body + if (rootBlockName && rootNode.name == 'body') + addRootBlocks(); + + // Run filters only when the contents is valid + if (!args.invalid) { + // Run node filters + for (name in matchedNodes) { + list = nodeFilters[name]; + nodes = matchedNodes[name]; + + // Remove already removed children + fi = nodes.length; + while (fi--) { + if (!nodes[fi].parent) + nodes.splice(fi, 1); + } + + for (i = 0, l = list.length; i < l; i++) + list[i](nodes, name, args); + } + + // Run attribute filters + for (i = 0, l = attributeFilters.length; i < l; i++) { + list = attributeFilters[i]; + + if (list.name in matchedAttributes) { + nodes = matchedAttributes[list.name]; + + // Remove already removed children + fi = nodes.length; + while (fi--) { + if (!nodes[fi].parent) + nodes.splice(fi, 1); + } + + for (fi = 0, fl = list.callbacks.length; fi < fl; fi++) + list.callbacks[fi](nodes, list.name, args); + } + } + } + + return rootNode; + }; + + // Remove
    at end of block elements Gecko and WebKit injects BR elements to + // make it possible to place the caret inside empty blocks. This logic tries to remove + // these elements and keep br elements that where intended to be there intact + if (settings.remove_trailing_brs) { + self.addNodeFilter('br', function(nodes, name) { + var i, l = nodes.length, node, blockElements = schema.getBlockElements(), + nonEmptyElements = schema.getNonEmptyElements(), parent, prev, prevName; + + // Remove brs from body element as well + blockElements.body = 1; + + // Must loop forwards since it will otherwise remove all brs in

    a


    + for (i = 0; i < l; i++) { + node = nodes[i]; + parent = node.parent; + + if (blockElements[node.parent.name] && node === parent.lastChild) { + // Loop all nodes to the right of the current node and check for other BR elements + // excluding bookmarks since they are invisible + prev = node.prev; + while (prev) { + prevName = prev.name; + + // Ignore bookmarks + if (prevName !== "span" || prev.attr('data-mce-type') !== 'bookmark') { + // Found a non BR element + if (prevName !== "br") + break; + + // Found another br it's a

    structure then don't remove anything + if (prevName === 'br') { + node = null; + break; + } + } + + prev = prev.prev; + } + + if (node) { + node.remove(); + + // Is the parent to be considered empty after we removed the BR + if (parent.isEmpty(nonEmptyElements)) { + elementRule = schema.getElementRule(parent.name); + + // Remove or padd the element depending on schema rule + if (elementRule) { + if (elementRule.removeEmpty) + parent.remove(); + else if (elementRule.paddEmpty) + parent.empty().append(new tinymce.html.Node('#text', 3)).value = '\u00a0'; + } + } + } + } + } + }); + } + } +})(tinymce); diff --git a/js/tiny_mce/classes/html/Entities.js b/js/tiny_mce/classes/html/Entities.js new file mode 100644 index 0000000000..5965f55a94 --- /dev/null +++ b/js/tiny_mce/classes/html/Entities.js @@ -0,0 +1,253 @@ +/** + * Entities.js + * + * Copyright 2010, Moxiecode Systems AB + * Released under LGPL License. + * + * License: http://tinymce.moxiecode.com/license + * Contributing: http://tinymce.moxiecode.com/contributing + */ + +(function(tinymce) { + var namedEntities, baseEntities, reverseEntities, + attrsCharsRegExp = /[&<>\"\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, + textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, + rawCharsRegExp = /[<>&\"\']/g, + entityRegExp = /&(#x|#)?([\w]+);/g, + asciiMap = { + 128 : "\u20AC", 130 : "\u201A", 131 : "\u0192", 132 : "\u201E", 133 : "\u2026", 134 : "\u2020", + 135 : "\u2021", 136 : "\u02C6", 137 : "\u2030", 138 : "\u0160", 139 : "\u2039", 140 : "\u0152", + 142 : "\u017D", 145 : "\u2018", 146 : "\u2019", 147 : "\u201C", 148 : "\u201D", 149 : "\u2022", + 150 : "\u2013", 151 : "\u2014", 152 : "\u02DC", 153 : "\u2122", 154 : "\u0161", 155 : "\u203A", + 156 : "\u0153", 158 : "\u017E", 159 : "\u0178" + }; + + // Raw entities + baseEntities = { + '\"' : '"', // Needs to be escaped since the YUI compressor would otherwise break the code + "'" : ''', + '<' : '<', + '>' : '>', + '&' : '&' + }; + + // Reverse lookup table for raw entities + reverseEntities = { + '<' : '<', + '>' : '>', + '&' : '&', + '"' : '"', + ''' : "'" + }; + + // Decodes text by using the browser + function nativeDecode(text) { + var elm; + + elm = document.createElement("div"); + elm.innerHTML = text; + + return elm.textContent || elm.innerText || text; + }; + + // Build a two way lookup table for the entities + function buildEntitiesLookup(items, radix) { + var i, chr, entity, lookup = {}; + + if (items) { + items = items.split(','); + radix = radix || 10; + + // Build entities lookup table + for (i = 0; i < items.length; i += 2) { + chr = String.fromCharCode(parseInt(items[i], radix)); + + // Only add non base entities + if (!baseEntities[chr]) { + entity = '&' + items[i + 1] + ';'; + lookup[chr] = entity; + lookup[entity] = chr; + } + } + + return lookup; + } + }; + + // Unpack entities lookup where the numbers are in radix 32 to reduce the size + namedEntities = buildEntitiesLookup( + '50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' + + '5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' + + '5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' + + '5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' + + '68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' + + '6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' + + '6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' + + '75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' + + '7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' + + '7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' + + 'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' + + 'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' + + 't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' + + 'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' + + 'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' + + '81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' + + '8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' + + '8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' + + '8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' + + '8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' + + 'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' + + 'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' + + 'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' + + '80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' + + '811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro' + , 32); + + tinymce.html = tinymce.html || {}; + + /** + * Entity encoder class. + * + * @class tinymce.html.SaxParser + * @static + * @version 3.4 + */ + tinymce.html.Entities = { + /** + * Encodes the specified string using raw entities. This means only the required XML base entities will be endoded. + * + * @method encodeRaw + * @param {String} text Text to encode. + * @param {Boolean} attr Optional flag to specify if the text is attribute contents. + * @return {String} Entity encoded text. + */ + encodeRaw : function(text, attr) { + return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { + return baseEntities[chr] || chr; + }); + }, + + /** + * Encoded the specified text with both the attributes and text entities. This function will produce larger text contents + * since it doesn't know if the context is within a attribute or text node. This was added for compatibility + * and is exposed as the DOMUtils.encode function. + * + * @method encodeAllRaw + * @param {String} text Text to encode. + * @return {String} Entity encoded text. + */ + encodeAllRaw : function(text) { + return ('' + text).replace(rawCharsRegExp, function(chr) { + return baseEntities[chr] || chr; + }); + }, + + /** + * Encodes the specified string using numeric entities. The core entities will be encoded as named ones but all non lower ascii characters + * will be encoded into numeric entities. + * + * @method encodeNumeric + * @param {String} text Text to encode. + * @param {Boolean} attr Optional flag to specify if the text is attribute contents. + * @return {String} Entity encoded text. + */ + encodeNumeric : function(text, attr) { + return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { + // Multi byte sequence convert it to a single entity + if (chr.length > 1) + return '&#' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';'; + + return baseEntities[chr] || '&#' + chr.charCodeAt(0) + ';'; + }); + }, + + /** + * Encodes the specified string using named entities. The core entities will be encoded as named ones but all non lower ascii characters + * will be encoded into named entities. + * + * @method encodeNamed + * @param {String} text Text to encode. + * @param {Boolean} attr Optional flag to specify if the text is attribute contents. + * @param {Object} entities Optional parameter with entities to use. + * @return {String} Entity encoded text. + */ + encodeNamed : function(text, attr, entities) { + entities = entities || namedEntities; + + return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { + return baseEntities[chr] || entities[chr] || chr; + }); + }, + + /** + * Returns an encode function based on the name(s) and it's optional entities. + * + * @method getEncodeFunc + * @param {String} name Comma separated list of encoders for example named,numeric. + * @param {String} entities Optional parameter with entities to use instead of the built in set. + * @return {function} Encode function to be used. + */ + getEncodeFunc : function(name, entities) { + var Entities = tinymce.html.Entities; + + entities = buildEntitiesLookup(entities) || namedEntities; + + function encodeNamedAndNumeric(text, attr) { + return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { + return baseEntities[chr] || entities[chr] || '&#' + chr.charCodeAt(0) + ';' || chr; + }); + }; + + function encodeCustomNamed(text, attr) { + return Entities.encodeNamed(text, attr, entities); + }; + + // Replace + with , to be compatible with previous TinyMCE versions + name = tinymce.makeMap(name.replace(/\+/g, ',')); + + // Named and numeric encoder + if (name.named && name.numeric) + return encodeNamedAndNumeric; + + // Named encoder + if (name.named) { + // Custom names + if (entities) + return encodeCustomNamed; + + return Entities.encodeNamed; + } + + // Numeric + if (name.numeric) + return Entities.encodeNumeric; + + // Raw encoder + return Entities.encodeRaw; + }, + + /** + * Decodes the specified string, this will replace entities with raw UTF characters. + * + * @param {String} text Text to entity decode. + * @return {String} Entity decoded string. + */ + decode : function(text) { + return text.replace(entityRegExp, function(all, numeric, value) { + if (numeric) { + value = parseInt(value, numeric.length === 2 ? 16 : 10); + + // Support upper UTF + if (value > 0xFFFF) { + value -= 0x10000; + + return String.fromCharCode(0xD800 + (value >> 10), 0xDC00 + (value & 0x3FF)); + } else + return asciiMap[value] || String.fromCharCode(value); + } + + return reverseEntities[all] || namedEntities[all] || nativeDecode(all); + }); + } + }; +})(tinymce); diff --git a/js/tiny_mce/classes/html/Node.js b/js/tiny_mce/classes/html/Node.js new file mode 100644 index 0000000000..774adbc737 --- /dev/null +++ b/js/tiny_mce/classes/html/Node.js @@ -0,0 +1,474 @@ +/** + * Node.js + * + * Copyright 2010, Moxiecode Systems AB + * Released under LGPL License. + * + * License: http://tinymce.moxiecode.com/license + * Contributing: http://tinymce.moxiecode.com/contributing + */ + +(function(tinymce) { + var whiteSpaceRegExp = /^[ \t\r\n]*$/, typeLookup = { + '#text' : 3, + '#comment' : 8, + '#cdata' : 4, + '#pi' : 7, + '#doctype' : 10, + '#document-fragment' : 11 + }; + + // Walks the tree left/right + function walk(node, root_node, prev) { + var sibling, parent, startName = prev ? 'lastChild' : 'firstChild', siblingName = prev ? 'prev' : 'next'; + + // Walk into nodes if it has a start + if (node[startName]) + return node[startName]; + + // Return the sibling if it has one + if (node !== root_node) { + sibling = node[siblingName]; + + if (sibling) + return sibling; + + // Walk up the parents to look for siblings + for (parent = node.parent; parent && parent !== root_node; parent = parent.parent) { + sibling = parent[siblingName]; + + if (sibling) + return sibling; + } + } + }; + + /** + * This class is a minimalistic implementation of a DOM like node used by the DomParser class. + * + * @example + * var node = new tinymce.html.Node('strong', 1); + * someRoot.append(node); + * + * @class tinymce.html.Node + * @version 3.4 + */ + + /** + * Constructs a new Node instance. + * + * @constructor + * @method Node + * @param {String} name Name of the node type. + * @param {Number} type Numeric type representing the node. + */ + function Node(name, type) { + this.name = name; + this.type = type; + + if (type === 1) { + this.attributes = []; + this.attributes.map = {}; + } + } + + tinymce.extend(Node.prototype, { + /** + * Replaces the current node with the specified one. + * + * @example + * someNode.replace(someNewNode); + * + * @method replace + * @param {tinymce.html.Node} node Node to replace the current node with. + * @return {tinymce.html.Node} The old node that got replaced. + */ + replace : function(node) { + var self = this; + + if (node.parent) + node.remove(); + + self.insert(node, self); + self.remove(); + + return self; + }, + + /** + * Gets/sets or removes an attribute by name. + * + * @example + * someNode.attr("name", "value"); // Sets an attribute + * console.log(someNode.attr("name")); // Gets an attribute + * someNode.attr("name", null); // Removes an attribute + * + * @method attr + * @param {String} name Attribute name to set or get. + * @param {String} value Optional value to set. + * @return {String/tinymce.html.Node} String or undefined on a get operation or the current node on a set operation. + */ + attr : function(name, value) { + var self = this, attrs, i, undef; + + if (typeof name !== "string") { + for (i in name) + self.attr(i, name[i]); + + return self; + } + + if (attrs = self.attributes) { + if (value !== undef) { + // Remove attribute + if (value === null) { + if (name in attrs.map) { + delete attrs.map[name]; + + i = attrs.length; + while (i--) { + if (attrs[i].name === name) { + attrs = attrs.splice(i, 1); + return self; + } + } + } + + return self; + } + + // Set attribute + if (name in attrs.map) { + // Set attribute + i = attrs.length; + while (i--) { + if (attrs[i].name === name) { + attrs[i].value = value; + break; + } + } + } else + attrs.push({name: name, value: value}); + + attrs.map[name] = value; + + return self; + } else { + return attrs.map[name]; + } + } + }, + + /** + * Does a shallow clones the node into a new node. It will also exclude id attributes since + * there should only be one id per document. + * + * @example + * var clonedNode = node.clone(); + * + * @method clone + * @return {tinymce.html.Node} New copy of the original node. + */ + clone : function() { + var self = this, clone = new Node(self.name, self.type), i, l, selfAttrs, selfAttr, cloneAttrs; + + // Clone element attributes + if (selfAttrs = self.attributes) { + cloneAttrs = []; + cloneAttrs.map = {}; + + for (i = 0, l = selfAttrs.length; i < l; i++) { + selfAttr = selfAttrs[i]; + + // Clone everything except id + if (selfAttr.name !== 'id') { + cloneAttrs[cloneAttrs.length] = {name: selfAttr.name, value: selfAttr.value}; + cloneAttrs.map[selfAttr.name] = selfAttr.value; + } + } + + clone.attributes = cloneAttrs; + } + + clone.value = self.value; + clone.shortEnded = self.shortEnded; + + return clone; + }, + + /** + * Wraps the node in in another node. + * + * @example + * node.wrap(wrapperNode); + * + * @method wrap + */ + wrap : function(wrapper) { + var self = this; + + self.parent.insert(wrapper, self); + wrapper.append(self); + + return self; + }, + + /** + * Unwraps the node in other words it removes the node but keeps the children. + * + * @example + * node.unwrap(); + * + * @method unwrap + */ + unwrap : function() { + var self = this, node, next; + + for (node = self.firstChild; node; ) { + next = node.next; + self.insert(node, self, true); + node = next; + } + + self.remove(); + }, + + /** + * Removes the node from it's parent. + * + * @example + * node.remove(); + * + * @method remove + * @return {tinymce.html.Node} Current node that got removed. + */ + remove : function() { + var self = this, parent = self.parent, next = self.next, prev = self.prev; + + if (parent) { + if (parent.firstChild === self) { + parent.firstChild = next; + + if (next) + next.prev = null; + } else { + prev.next = next; + } + + if (parent.lastChild === self) { + parent.lastChild = prev; + + if (prev) + prev.next = null; + } else { + next.prev = prev; + } + + self.parent = self.next = self.prev = null; + } + + return self; + }, + + /** + * Appends a new node as a child of the current node. + * + * @example + * node.append(someNode); + * + * @method append + * @param {tinymce.html.Node} node Node to append as a child of the current one. + * @return {tinymce.html.Node} The node that got appended. + */ + append : function(node) { + var self = this, last; + + if (node.parent) + node.remove(); + + last = self.lastChild; + if (last) { + last.next = node; + node.prev = last; + self.lastChild = node; + } else + self.lastChild = self.firstChild = node; + + node.parent = self; + + return node; + }, + + /** + * Inserts a node at a specific position as a child of the current node. + * + * @example + * parentNode.insert(newChildNode, oldChildNode); + * + * @method insert + * @param {tinymce.html.Node} node Node to insert as a child of the current node. + * @param {tinymce.html.Node} ref_node Reference node to set node before/after. + * @param {Boolean} before Optional state to insert the node before the reference node. + * @return {tinymce.html.Node} The node that got inserted. + */ + insert : function(node, ref_node, before) { + var parent; + + if (node.parent) + node.remove(); + + parent = ref_node.parent || this; + + if (before) { + if (ref_node === parent.firstChild) + parent.firstChild = node; + else + ref_node.prev.next = node; + + node.prev = ref_node.prev; + node.next = ref_node; + ref_node.prev = node; + } else { + if (ref_node === parent.lastChild) + parent.lastChild = node; + else + ref_node.next.prev = node; + + node.next = ref_node.next; + node.prev = ref_node; + ref_node.next = node; + } + + node.parent = parent; + + return node; + }, + + /** + * Get all children by name. + * + * @method getAll + * @param {String} name Name of the child nodes to collect. + * @return {Array} Array with child nodes matchin the specified name. + */ + getAll : function(name) { + var self = this, node, collection = []; + + for (node = self.firstChild; node; node = walk(node, self)) { + if (node.name === name) + collection.push(node); + } + + return collection; + }, + + /** + * Removes all children of the current node. + * + * @method empty + * @return {tinymce.html.Node} The current node that got cleared. + */ + empty : function() { + var self = this, nodes, i, node; + + // Remove all children + if (self.firstChild) { + nodes = []; + + // Collect the children + for (node = self.firstChild; node; node = walk(node, self)) + nodes.push(node); + + // Remove the children + i = nodes.length; + while (i--) { + node = nodes[i]; + node.parent = node.firstChild = node.lastChild = node.next = node.prev = null; + } + } + + self.firstChild = self.lastChild = null; + + return self; + }, + + /** + * Returns true/false if the node is to be considered empty or not. + * + * @example + * node.isEmpty({img : true}); + * @method isEmpty + * @param {Object} elements Name/value object with elements that are automatically treated as non empty elements. + * @return {Boolean} true/false if the node is empty or not. + */ + isEmpty : function(elements) { + var self = this, node = self.firstChild, i, name; + + if (node) { + do { + if (node.type === 1) { + // Ignore bogus elements + if (node.attributes.map['data-mce-bogus']) + continue; + + // Keep empty elements like + if (elements[node.name]) + return false; + + // Keep elements with data attributes or name attribute like
    + i = node.attributes.length; + while (i--) { + name = node.attributes[i].name; + if (name === "name" || name.indexOf('data-') === 0) + return false; + } + } + + // Keep non whitespace text nodes + if ((node.type === 3 && !whiteSpaceRegExp.test(node.value))) + return false; + } while (node = walk(node, self)); + } + + return true; + }, + + /** + * Walks to the next or previous node and returns that node or null if it wasn't found. + * + * @method walk + * @param {Boolean} prev Optional previous node state defaults to false. + * @return {tinymce.html.Node} Node that is next to or previous of the current node. + */ + walk : function(prev) { + return walk(this, null, prev); + } + }); + + tinymce.extend(Node, { + /** + * Creates a node of a specific type. + * + * @static + * @method create + * @param {String} name Name of the node type to create for example "b" or "#text". + * @param {Object} attrs Name/value collection of attributes that will be applied to elements. + */ + create : function(name, attrs) { + var node, attrName; + + // Create node + node = new Node(name, typeLookup[name] || 1); + + // Add attributes if needed + if (attrs) { + for (attrName in attrs) + node.attr(attrName, attrs[attrName]); + } + + return node; + } + }); + + tinymce.html.Node = Node; +})(tinymce); diff --git a/js/tiny_mce/classes/html/SaxParser.js b/js/tiny_mce/classes/html/SaxParser.js new file mode 100644 index 0000000000..73a71c0f30 --- /dev/null +++ b/js/tiny_mce/classes/html/SaxParser.js @@ -0,0 +1,355 @@ +/** + * SaxParser.js + * + * Copyright 2010, Moxiecode Systems AB + * Released under LGPL License. + * + * License: http://tinymce.moxiecode.com/license + * Contributing: http://tinymce.moxiecode.com/contributing + */ + +(function(tinymce) { + /** + * This class parses HTML code using pure JavaScript and executes various events for each item it finds. It will + * always execute the events in the right order for tag soup code like

    . It will also remove elements + * and attributes that doesn't fit the schema if the validate setting is enabled. + * + * @example + * var parser = new tinymce.html.SaxParser({ + * validate: true, + * + * comment: function(text) { + * console.log('Comment:', text); + * }, + * + * cdata: function(text) { + * console.log('CDATA:', text); + * }, + * + * text: function(text, raw) { + * console.log('Text:', text, 'Raw:', raw); + * }, + * + * start: function(name, attrs, empty) { + * console.log('Start:', name, attrs, empty); + * }, + * + * end: function(name) { + * console.log('End:', name); + * }, + * + * pi: function(name, text) { + * console.log('PI:', name, text); + * }, + * + * doctype: function(text) { + * console.log('DocType:', text); + * } + * }, schema); + * @class tinymce.html.SaxParser + * @version 3.4 + */ + + /** + * Constructs a new SaxParser instance. + * + * @constructor + * @method SaxParser + * @param {Object} settings Name/value collection of settings. comment, cdata, text, start and end are callbacks. + * @param {tinymce.html.Schema} schema HTML Schema class to use when parsing. + */ + tinymce.html.SaxParser = function(settings, schema) { + var self = this, noop = function() {}; + + settings = settings || {}; + self.schema = schema = schema || new tinymce.html.Schema(); + + if (settings.fix_self_closing !== false) + settings.fix_self_closing = true; + + // Add handler functions from settings and setup default handlers + tinymce.each('comment cdata text start end pi doctype'.split(' '), function(name) { + if (name) + self[name] = settings[name] || noop; + }); + + /** + * Parses the specified HTML string and executes the callbacks for each item it finds. + * + * @example + * new SaxParser({...}).parse('text'); + * @method parse + * @param {String} html Html string to sax parse. + */ + self.parse = function(html) { + var self = this, matches, index = 0, value, endRegExp, stack = [], attrList, i, text, name, isInternalElement, removeInternalElements, + shortEndedElements, fillAttrsMap, isShortEnded, validate, elementRule, isValidElement, attr, attribsValue, invalidPrefixRegExp, + validAttributesMap, validAttributePatterns, attributesRequired, attributesDefault, attributesForced, selfClosing, + tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0, decode = tinymce.html.Entities.decode, fixSelfClosing, isIE; + + function processEndTag(name) { + var pos, i; + + // Find position of parent of the same type + pos = stack.length; + while (pos--) { + if (stack[pos].name === name) + break; + } + + // Found parent + if (pos >= 0) { + // Close all the open elements + for (i = stack.length - 1; i >= pos; i--) { + name = stack[i]; + + if (name.valid) + self.end(name.name); + } + + // Remove the open elements from the stack + stack.length = pos; + } + }; + + // Precompile RegExps and map objects + tokenRegExp = new RegExp('<(?:' + + '(?:!--([\\w\\W]*?)-->)|' + // Comment + '(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|' + // CDATA + '(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE + '(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI + '(?:\\/([^>]+)>)|' + // End element + '(?:([^\\s\\/<>]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/)>)' + // Start element + ')', 'g'); + + attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:\\.|[^\"])*)\")|(?:\'((?:\\.|[^\'])*)\')|([^>\s]+)))?/g; + specialElements = { + 'script' : /<\/script[^>]*>/gi, + 'style' : /<\/style[^>]*>/gi, + 'noscript' : /<\/noscript[^>]*>/gi + }; + + // Setup lookup tables for empty elements and boolean attributes + shortEndedElements = schema.getShortEndedElements(); + selfClosing = schema.getSelfClosingElements(); + fillAttrsMap = schema.getBoolAttrs(); + validate = settings.validate; + removeInternalElements = settings.remove_internals; + fixSelfClosing = settings.fix_self_closing; + isIE = tinymce.isIE; + invalidPrefixRegExp = /^:/; + + while (matches = tokenRegExp.exec(html)) { + // Text + if (index < matches.index) + self.text(decode(html.substr(index, matches.index - index))); + + if (value = matches[6]) { // End element + value = value.toLowerCase(); + + // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements + if (isIE && invalidPrefixRegExp.test(value)) + value = value.substr(1); + + processEndTag(value); + } else if (value = matches[7]) { // Start element + value = value.toLowerCase(); + + // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements + if (isIE && invalidPrefixRegExp.test(value)) + value = value.substr(1); + + isShortEnded = value in shortEndedElements; + + // Is self closing tag for example an
  • after an open
  • + if (fixSelfClosing && selfClosing[value] && stack.length > 0 && stack[stack.length - 1].name === value) + processEndTag(value); + + // Validate element + if (!validate || (elementRule = schema.getElementRule(value))) { + isValidElement = true; + + // Grab attributes map and patters when validation is enabled + if (validate) { + validAttributesMap = elementRule.attributes; + validAttributePatterns = elementRule.attributePatterns; + } + + // Parse attributes + if (attribsValue = matches[8]) { + isInternalElement = attribsValue.indexOf('data-mce-type') !== -1; // Check if the element is an internal element + + // If the element has internal attributes then remove it if we are told to do so + if (isInternalElement && removeInternalElements) + isValidElement = false; + + attrList = []; + attrList.map = {}; + + attribsValue.replace(attrRegExp, function(match, name, value, val2, val3) { + var attrRule, i; + + name = name.toLowerCase(); + value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute + + // Validate name and value + if (validate && !isInternalElement && name.indexOf('data-') !== 0) { + attrRule = validAttributesMap[name]; + + // Find rule by pattern matching + if (!attrRule && validAttributePatterns) { + i = validAttributePatterns.length; + while (i--) { + attrRule = validAttributePatterns[i]; + if (attrRule.pattern.test(name)) + break; + } + + // No rule matched + if (i === -1) + attrRule = null; + } + + // No attribute rule found + if (!attrRule) + return; + + // Validate value + if (attrRule.validValues && !(value in attrRule.validValues)) + return; + } + + // Add attribute to list and map + attrList.map[name] = value; + attrList.push({ + name: name, + value: value + }); + }); + } else { + attrList = []; + attrList.map = {}; + } + + // Process attributes if validation is enabled + if (validate && !isInternalElement) { + attributesRequired = elementRule.attributesRequired; + attributesDefault = elementRule.attributesDefault; + attributesForced = elementRule.attributesForced; + + // Handle forced attributes + if (attributesForced) { + i = attributesForced.length; + while (i--) { + attr = attributesForced[i]; + name = attr.name; + attrValue = attr.value; + + if (attrValue === '{$uid}') + attrValue = 'mce_' + idCount++; + + attrList.map[name] = attrValue; + attrList.push({name: name, value: attrValue}); + } + } + + // Handle default attributes + if (attributesDefault) { + i = attributesDefault.length; + while (i--) { + attr = attributesDefault[i]; + name = attr.name; + + if (!(name in attrList.map)) { + attrValue = attr.value; + + if (attrValue === '{$uid}') + attrValue = 'mce_' + idCount++; + + attrList.map[name] = attrValue; + attrList.push({name: name, value: attrValue}); + } + } + } + + // Handle required attributes + if (attributesRequired) { + i = attributesRequired.length; + while (i--) { + if (attributesRequired[i] in attrList.map) + break; + } + + // None of the required attributes where found + if (i === -1) + isValidElement = false; + } + + // Invalidate element if it's marked as bogus + if (attrList.map['data-mce-bogus']) + isValidElement = false; + } + + if (isValidElement) + self.start(value, attrList, isShortEnded); + } else + isValidElement = false; + + // Treat script, noscript and style a bit different since they may include code that looks like elements + if (endRegExp = specialElements[value]) { + endRegExp.lastIndex = index = matches.index + matches[0].length; + + if (matches = endRegExp.exec(html)) { + if (isValidElement) + text = html.substr(index, matches.index - index); + + index = matches.index + matches[0].length; + } else { + text = html.substr(index); + index = html.length; + } + + if (isValidElement && text.length > 0) + self.text(text, true); + + if (isValidElement) + self.end(value); + + tokenRegExp.lastIndex = index; + continue; + } + + // Push value on to stack + if (!isShortEnded) { + if (!attribsValue || attribsValue.indexOf('/') != attribsValue.length - 1) + stack.push({name: value, valid: isValidElement}); + else if (isValidElement) + self.end(value); + } + } else if (value = matches[1]) { // Comment + self.comment(value); + } else if (value = matches[2]) { // CDATA + self.cdata(value); + } else if (value = matches[3]) { // DOCTYPE + self.doctype(value); + } else if (value = matches[4]) { // PI + self.pi(value, matches[5]); + } + + index = matches.index + matches[0].length; + } + + // Text + if (index < html.length) + self.text(decode(html.substr(index))); + + // Close any open elements + for (i = stack.length - 1; i >= 0; i--) { + value = stack[i]; + + if (value.valid) + self.end(value.name); + } + }; + } +})(tinymce); diff --git a/js/tiny_mce/classes/html/Schema.js b/js/tiny_mce/classes/html/Schema.js new file mode 100644 index 0000000000..ff7c173b49 --- /dev/null +++ b/js/tiny_mce/classes/html/Schema.js @@ -0,0 +1,663 @@ +/** + * Schema.js + * + * Copyright 2010, Moxiecode Systems AB + * Released under LGPL License. + * + * License: http://tinymce.moxiecode.com/license + * Contributing: http://tinymce.moxiecode.com/contributing + */ + +(function(tinymce) { + var transitional = {}, boolAttrMap, blockElementsMap, shortEndedElementsMap, nonEmptyElementsMap, customElementsMap = {}, + defaultWhiteSpaceElementsMap, selfClosingElementsMap, makeMap = tinymce.makeMap, each = tinymce.each; + + function split(str, delim) { + return str.split(delim || ','); + }; + + /** + * Unpacks the specified lookup and string data it will also parse it into an object + * map with sub object for it's children. This will later also include the attributes. + */ + function unpack(lookup, data) { + var key, elements = {}; + + function replace(value) { + return value.replace(/[A-Z]+/g, function(key) { + return replace(lookup[key]); + }); + }; + + // Unpack lookup + for (key in lookup) { + if (lookup.hasOwnProperty(key)) + lookup[key] = replace(lookup[key]); + } + + // Unpack and parse data into object map + replace(data).replace(/#/g, '#text').replace(/(\w+)\[([^\]]+)\]\[([^\]]*)\]/g, function(str, name, attributes, children) { + attributes = split(attributes, '|'); + + elements[name] = { + attributes : makeMap(attributes), + attributesOrder : attributes, + children : makeMap(children, '|', {'#comment' : {}}) + } + }); + + return elements; + }; + + // Build a lookup table for block elements both lowercase and uppercase + blockElementsMap = 'h1,h2,h3,h4,h5,h6,hr,p,div,address,pre,form,table,tbody,thead,tfoot,' + + 'th,tr,td,li,ol,ul,caption,blockquote,center,dl,dt,dd,dir,fieldset,' + + 'noscript,menu,isindex,samp,header,footer,article,section,hgroup'; + blockElementsMap = makeMap(blockElementsMap, ',', makeMap(blockElementsMap.toUpperCase())); + + // This is the XHTML 1.0 transitional elements with it's attributes and children packed to reduce it's size + transitional = unpack({ + Z : 'H|K|N|O|P', + Y : 'X|form|R|Q', + ZG : 'E|span|width|align|char|charoff|valign', + X : 'p|T|div|U|W|isindex|fieldset|table', + ZF : 'E|align|char|charoff|valign', + W : 'pre|hr|blockquote|address|center|noframes', + ZE : 'abbr|axis|headers|scope|rowspan|colspan|align|char|charoff|valign|nowrap|bgcolor|width|height', + ZD : '[E][S]', + U : 'ul|ol|dl|menu|dir', + ZC : 'p|Y|div|U|W|table|br|span|bdo|object|applet|img|map|K|N|Q', + T : 'h1|h2|h3|h4|h5|h6', + ZB : 'X|S|Q', + S : 'R|P', + ZA : 'a|G|J|M|O|P', + R : 'a|H|K|N|O', + Q : 'noscript|P', + P : 'ins|del|script', + O : 'input|select|textarea|label|button', + N : 'M|L', + M : 'em|strong|dfn|code|q|samp|kbd|var|cite|abbr|acronym', + L : 'sub|sup', + K : 'J|I', + J : 'tt|i|b|u|s|strike', + I : 'big|small|font|basefont', + H : 'G|F', + G : 'br|span|bdo', + F : 'object|applet|img|map|iframe', + E : 'A|B|C', + D : 'accesskey|tabindex|onfocus|onblur', + C : 'onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup', + B : 'lang|xml:lang|dir', + A : 'id|class|style|title' + }, 'script[id|charset|type|language|src|defer|xml:space][]' + + 'style[B|id|type|media|title|xml:space][]' + + 'object[E|declare|classid|codebase|data|type|codetype|archive|standby|width|height|usemap|name|tabindex|align|border|hspace|vspace][#|param|Y]' + + 'param[id|name|value|valuetype|type][]' + + 'p[E|align][#|S]' + + 'a[E|D|charset|type|name|href|hreflang|rel|rev|shape|coords|target][#|Z]' + + 'br[A|clear][]' + + 'span[E][#|S]' + + 'bdo[A|C|B][#|S]' + + 'applet[A|codebase|archive|code|object|alt|name|width|height|align|hspace|vspace][#|param|Y]' + + 'h1[E|align][#|S]' + + 'img[E|src|alt|name|longdesc|width|height|usemap|ismap|align|border|hspace|vspace][]' + + 'map[B|C|A|name][X|form|Q|area]' + + 'h2[E|align][#|S]' + + 'iframe[A|longdesc|name|src|frameborder|marginwidth|marginheight|scrolling|align|width|height][#|Y]' + + 'h3[E|align][#|S]' + + 'tt[E][#|S]' + + 'i[E][#|S]' + + 'b[E][#|S]' + + 'u[E][#|S]' + + 's[E][#|S]' + + 'strike[E][#|S]' + + 'big[E][#|S]' + + 'small[E][#|S]' + + 'font[A|B|size|color|face][#|S]' + + 'basefont[id|size|color|face][]' + + 'em[E][#|S]' + + 'strong[E][#|S]' + + 'dfn[E][#|S]' + + 'code[E][#|S]' + + 'q[E|cite][#|S]' + + 'samp[E][#|S]' + + 'kbd[E][#|S]' + + 'var[E][#|S]' + + 'cite[E][#|S]' + + 'abbr[E][#|S]' + + 'acronym[E][#|S]' + + 'sub[E][#|S]' + + 'sup[E][#|S]' + + 'input[E|D|type|name|value|checked|disabled|readonly|size|maxlength|src|alt|usemap|onselect|onchange|accept|align][]' + + 'select[E|name|size|multiple|disabled|tabindex|onfocus|onblur|onchange][optgroup|option]' + + 'optgroup[E|disabled|label][option]' + + 'option[E|selected|disabled|label|value][]' + + 'textarea[E|D|name|rows|cols|disabled|readonly|onselect|onchange][]' + + 'label[E|for|accesskey|onfocus|onblur][#|S]' + + 'button[E|D|name|value|type|disabled][#|p|T|div|U|W|table|G|object|applet|img|map|K|N|Q]' + + 'h4[E|align][#|S]' + + 'ins[E|cite|datetime][#|Y]' + + 'h5[E|align][#|S]' + + 'del[E|cite|datetime][#|Y]' + + 'h6[E|align][#|S]' + + 'div[E|align][#|Y]' + + 'ul[E|type|compact][li]' + + 'li[E|type|value][#|Y]' + + 'ol[E|type|compact|start][li]' + + 'dl[E|compact][dt|dd]' + + 'dt[E][#|S]' + + 'dd[E][#|Y]' + + 'menu[E|compact][li]' + + 'dir[E|compact][li]' + + 'pre[E|width|xml:space][#|ZA]' + + 'hr[E|align|noshade|size|width][]' + + 'blockquote[E|cite][#|Y]' + + 'address[E][#|S|p]' + + 'center[E][#|Y]' + + 'noframes[E][#|Y]' + + 'isindex[A|B|prompt][]' + + 'fieldset[E][#|legend|Y]' + + 'legend[E|accesskey|align][#|S]' + + 'table[E|summary|width|border|frame|rules|cellspacing|cellpadding|align|bgcolor][caption|col|colgroup|thead|tfoot|tbody|tr]' + + 'caption[E|align][#|S]' + + 'col[ZG][]' + + 'colgroup[ZG][col]' + + 'thead[ZF][tr]' + + 'tr[ZF|bgcolor][th|td]' + + 'th[E|ZE][#|Y]' + + 'form[E|action|method|name|enctype|onsubmit|onreset|accept|accept-charset|target][#|X|R|Q]' + + 'noscript[E][#|Y]' + + 'td[E|ZE][#|Y]' + + 'tfoot[ZF][tr]' + + 'tbody[ZF][tr]' + + 'area[E|D|shape|coords|href|nohref|alt|target][]' + + 'base[id|href|target][]' + + 'body[E|onload|onunload|background|bgcolor|text|link|vlink|alink][#|Y]' + ); + + boolAttrMap = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected,autoplay,loop,controls'); + shortEndedElementsMap = makeMap('area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed,source'); + nonEmptyElementsMap = tinymce.extend(makeMap('td,th,iframe,video,audio,object'), shortEndedElementsMap); + defaultWhiteSpaceElementsMap = makeMap('pre,script,style,textarea'); + selfClosingElementsMap = makeMap('colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr'); + + /** + * Schema validator class. + * + * @class tinymce.html.Schema + * @example + * if (tinymce.activeEditor.schema.isValidChild('p', 'span')) + * alert('span is valid child of p.'); + * + * if (tinymce.activeEditor.schema.getElementRule('p')) + * alert('P is a valid element.'); + * + * @class tinymce.html.Schema + * @version 3.4 + */ + + /** + * Constructs a new Schema instance. + * + * @constructor + * @method Schema + * @param {Object} settings Name/value settings object. + */ + tinymce.html.Schema = function(settings) { + var self = this, elements = {}, children = {}, patternElements = [], validStyles, whiteSpaceElementsMap; + + settings = settings || {}; + + // Allow all elements and attributes if verify_html is set to false + if (settings.verify_html === false) + settings.valid_elements = '*[*]'; + + // Build styles list + if (settings.valid_styles) { + validStyles = {}; + + // Convert styles into a rule list + each(settings.valid_styles, function(value, key) { + validStyles[key] = tinymce.explode(value); + }); + } + + whiteSpaceElementsMap = settings.whitespace_elements ? makeMap(settings.whitespace_elements) : defaultWhiteSpaceElementsMap; + + // Converts a wildcard expression string to a regexp for example *a will become /.*a/. + function patternToRegExp(str) { + return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$'); + }; + + // Parses the specified valid_elements string and adds to the current rules + // This function is a bit hard to read since it's heavily optimized for speed + function addValidElements(valid_elements) { + var ei, el, ai, al, yl, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder, + prefix, outputName, globalAttributes, globalAttributesOrder, transElement, key, childKey, value, + elementRuleRegExp = /^([#+-])?([^\[\/]+)(?:\/([^\[]+))?(?:\[([^\]]+)\])?$/, + attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/, + hasPatternsRegExp = /[*?+]/; + + if (valid_elements) { + // Split valid elements into an array with rules + valid_elements = split(valid_elements); + + if (elements['@']) { + globalAttributes = elements['@'].attributes; + globalAttributesOrder = elements['@'].attributesOrder; + } + + // Loop all rules + for (ei = 0, el = valid_elements.length; ei < el; ei++) { + // Parse element rule + matches = elementRuleRegExp.exec(valid_elements[ei]); + if (matches) { + // Setup local names for matches + prefix = matches[1]; + elementName = matches[2]; + outputName = matches[3]; + attrData = matches[4]; + + // Create new attributes and attributesOrder + attributes = {}; + attributesOrder = []; + + // Create the new element + element = { + attributes : attributes, + attributesOrder : attributesOrder + }; + + // Padd empty elements prefix + if (prefix === '#') + element.paddEmpty = true; + + // Remove empty elements prefix + if (prefix === '-') + element.removeEmpty = true; + + // Copy attributes from global rule into current rule + if (globalAttributes) { + for (key in globalAttributes) + attributes[key] = globalAttributes[key]; + + attributesOrder.push.apply(attributesOrder, globalAttributesOrder); + } + + // Attributes defined + if (attrData) { + attrData = split(attrData, '|'); + for (ai = 0, al = attrData.length; ai < al; ai++) { + matches = attrRuleRegExp.exec(attrData[ai]); + if (matches) { + attr = {}; + attrType = matches[1]; + attrName = matches[2].replace(/::/g, ':'); + prefix = matches[3]; + value = matches[4]; + + // Required + if (attrType === '!') { + element.attributesRequired = element.attributesRequired || []; + element.attributesRequired.push(attrName); + attr.required = true; + } + + // Denied from global + if (attrType === '-') { + delete attributes[attrName]; + attributesOrder.splice(tinymce.inArray(attributesOrder, attrName), 1); + continue; + } + + // Default value + if (prefix) { + // Default value + if (prefix === '=') { + element.attributesDefault = element.attributesDefault || []; + element.attributesDefault.push({name: attrName, value: value}); + attr.defaultValue = value; + } + + // Forced value + if (prefix === ':') { + element.attributesForced = element.attributesForced || []; + element.attributesForced.push({name: attrName, value: value}); + attr.forcedValue = value; + } + + // Required values + if (prefix === '<') + attr.validValues = makeMap(value, '?'); + } + + // Check for attribute patterns + if (hasPatternsRegExp.test(attrName)) { + element.attributePatterns = element.attributePatterns || []; + attr.pattern = patternToRegExp(attrName); + element.attributePatterns.push(attr); + } else { + // Add attribute to order list if it doesn't already exist + if (!attributes[attrName]) + attributesOrder.push(attrName); + + attributes[attrName] = attr; + } + } + } + } + + // Global rule, store away these for later usage + if (!globalAttributes && elementName == '@') { + globalAttributes = attributes; + globalAttributesOrder = attributesOrder; + } + + // Handle substitute elements such as b/strong + if (outputName) { + element.outputName = elementName; + elements[outputName] = element; + } + + // Add pattern or exact element + if (hasPatternsRegExp.test(elementName)) { + element.pattern = patternToRegExp(elementName); + patternElements.push(element); + } else + elements[elementName] = element; + } + } + } + }; + + function setValidElements(valid_elements) { + elements = {}; + patternElements = []; + + addValidElements(valid_elements); + + each(transitional, function(element, name) { + children[name] = element.children; + }); + }; + + // Adds custom non HTML elements to the schema + function addCustomElements(custom_elements) { + var customElementRegExp = /^(~)?(.+)$/; + + if (custom_elements) { + each(split(custom_elements), function(rule) { + var matches = customElementRegExp.exec(rule), + inline = matches[1] === '~', + cloneName = inline ? 'span' : 'div', + name = matches[2]; + + children[name] = children[cloneName]; + customElementsMap[name] = cloneName; + + // If it's not marked as inline then add it to valid block elements + if (!inline) + blockElementsMap[name] = {}; + + // Add custom elements at span/div positions + each(children, function(element, child) { + if (element[cloneName]) + element[name] = element[cloneName]; + }); + }); + } + }; + + // Adds valid children to the schema object + function addValidChildren(valid_children) { + var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/; + + if (valid_children) { + each(split(valid_children), function(rule) { + var matches = childRuleRegExp.exec(rule), parent, prefix; + + if (matches) { + prefix = matches[1]; + + // Add/remove items from default + if (prefix) + parent = children[matches[2]]; + else + parent = children[matches[2]] = {'#comment' : {}}; + + parent = children[matches[2]]; + + each(split(matches[3], '|'), function(child) { + if (prefix === '-') + delete parent[child]; + else + parent[child] = {}; + }); + } + }); + } + }; + + function getElementRule(name) { + var element = elements[name], i; + + // Exact match found + if (element) + return element; + + // No exact match then try the patterns + i = patternElements.length; + while (i--) { + element = patternElements[i]; + + if (element.pattern.test(name)) + return element; + } + }; + + if (!settings.valid_elements) { + // No valid elements defined then clone the elements from the transitional spec + each(transitional, function(element, name) { + elements[name] = { + attributes : element.attributes, + attributesOrder : element.attributesOrder + }; + + children[name] = element.children; + }); + + // Switch these + each(split('strong/b,em/i'), function(item) { + item = split(item, '/'); + elements[item[1]].outputName = item[0]; + }); + + // Add default alt attribute for images + elements.img.attributesDefault = [{name: 'alt', value: ''}]; + + // Remove these if they are empty by default + each(split('ol,ul,sub,sup,blockquote,span,font,a,table,tbody,tr'), function(name) { + elements[name].removeEmpty = true; + }); + + // Padd these by default + each(split('p,h1,h2,h3,h4,h5,h6,th,td,pre,div,address,caption'), function(name) { + elements[name].paddEmpty = true; + }); + } else + setValidElements(settings.valid_elements); + + addCustomElements(settings.custom_elements); + addValidChildren(settings.valid_children); + addValidElements(settings.extended_valid_elements); + + // Todo: Remove this when we fix list handling to be valid + addValidChildren('+ol[ul|ol],+ul[ul|ol]'); + + // If the user didn't allow span only allow internal spans + if (!getElementRule('span')) + addValidElements('span[!data-mce-type|*]'); + + // Delete invalid elements + if (settings.invalid_elements) { + tinymce.each(tinymce.explode(settings.invalid_elements), function(item) { + if (elements[item]) + delete elements[item]; + }); + } + + /** + * Name/value map object with valid parents and children to those parents. + * + * @example + * children = { + * div:{p:{}, h1:{}} + * }; + * @field children + * @type {Object} + */ + self.children = children; + + /** + * Name/value map object with valid styles for each element. + * + * @field styles + * @type {Object} + */ + self.styles = validStyles; + + /** + * Returns a map with boolean attributes. + * + * @method getBoolAttrs + * @return {Object} Name/value lookup map for boolean attributes. + */ + self.getBoolAttrs = function() { + return boolAttrMap; + }; + + /** + * Returns a map with block elements. + * + * @method getBoolAttrs + * @return {Object} Name/value lookup map for block elements. + */ + self.getBlockElements = function() { + return blockElementsMap; + }; + + /** + * Returns a map with short ended elements such as BR or IMG. + * + * @method getShortEndedElements + * @return {Object} Name/value lookup map for short ended elements. + */ + self.getShortEndedElements = function() { + return shortEndedElementsMap; + }; + + /** + * Returns a map with self closing tags such as
  • . + * + * @method getSelfClosingElements + * @return {Object} Name/value lookup map for self closing tags elements. + */ + self.getSelfClosingElements = function() { + return selfClosingElementsMap; + }; + + /** + * Returns a map with elements that should be treated as contents regardless if it has text + * content in them or not such as TD, VIDEO or IMG. + * + * @method getNonEmptyElements + * @return {Object} Name/value lookup map for non empty elements. + */ + self.getNonEmptyElements = function() { + return nonEmptyElementsMap; + }; + + /** + * Returns a map with elements where white space is to be preserved like PRE or SCRIPT. + * + * @method getWhiteSpaceElements + * @return {Object} Name/value lookup map for white space elements. + */ + self.getWhiteSpaceElements = function() { + return whiteSpaceElementsMap; + }; + + /** + * Returns true/false if the specified element and it's child is valid or not + * according to the schema. + * + * @method isValidChild + * @param {String} name Element name to check for. + * @param {String} child Element child to verify. + * @return {Boolean} True/false if the element is a valid child of the specified parent. + */ + self.isValidChild = function(name, child) { + var parent = children[name]; + + return !!(parent && parent[child]); + }; + + /** + * Returns true/false if the specified element is valid or not + * according to the schema. + * + * @method getElementRule + * @param {String} name Element name to check for. + * @return {Object} Element object or undefined if the element isn't valid. + */ + self.getElementRule = getElementRule; + + /** + * Returns an map object of all custom elements. + * + * @method getCustomElements + * @return {Object} Name/value map object of all custom elements. + */ + self.getCustomElements = function() { + return customElementsMap; + }; + + /** + * Parses a valid elements string and adds it to the schema. The valid elements format is for example "element[attr=default|otherattr]". + * Existing rules will be replaced with the ones specified, so this extends the schema. + * + * @method addValidElements + * @param {String} valid_elements String in the valid elements format to be parsed. + */ + self.addValidElements = addValidElements; + + /** + * Parses a valid elements string and sets it to the schema. The valid elements format is for example "element[attr=default|otherattr]". + * Existing rules will be replaced with the ones specified, so this extends the schema. + * + * @method setValidElements + * @param {String} valid_elements String in the valid elements format to be parsed. + */ + self.setValidElements = setValidElements; + + /** + * Adds custom non HTML elements to the schema. + * + * @method addCustomElements + * @param {String} custom_elements Comma separated list of custom elements to add. + */ + self.addCustomElements = addCustomElements; + + /** + * Parses a valid children string and adds them to the schema structure. The valid children format is for example: "element[child1|child2]". + * + * @method addValidChildren + * @param {String} valid_children Valid children elements string to parse + */ + self.addValidChildren = addValidChildren; + }; + + // Expose boolMap and blockElementMap as static properties for usage in DOMUtils + tinymce.html.Schema.boolAttrMap = boolAttrMap; + tinymce.html.Schema.blockElementsMap = blockElementsMap; +})(tinymce); diff --git a/js/tiny_mce/classes/html/Serializer.js b/js/tiny_mce/classes/html/Serializer.js new file mode 100644 index 0000000000..b74e223d94 --- /dev/null +++ b/js/tiny_mce/classes/html/Serializer.js @@ -0,0 +1,152 @@ +/** + * Serializer.js + * + * Copyright 2010, Moxiecode Systems AB + * Released under LGPL License. + * + * License: http://tinymce.moxiecode.com/license + * Contributing: http://tinymce.moxiecode.com/contributing + */ + +(function(tinymce) { + /** + * This class is used to serialize down the DOM tree into a string using a Writer instance. + * + * + * @example + * new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('

    text

    ')); + * @class tinymce.html.Serializer + * @version 3.4 + */ + + /** + * Constructs a new Serializer instance. + * + * @constructor + * @method Serializer + * @param {Object} settings Name/value settings object. + * @param {tinymce.html.Schema} schema Schema instance to use. + */ + tinymce.html.Serializer = function(settings, schema) { + var self = this, writer = new tinymce.html.Writer(settings); + + settings = settings || {}; + settings.validate = "validate" in settings ? settings.validate : true; + + self.schema = schema = schema || new tinymce.html.Schema(); + self.writer = writer; + + /** + * Serializes the specified node into a string. + * + * @example + * new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('

    text

    ')); + * @method serialize + * @param {tinymce.html.Node} node Node instance to serialize. + * @return {String} String with HTML based on DOM tree. + */ + self.serialize = function(node) { + var handlers, validate; + + validate = settings.validate; + + handlers = { + // #text + 3: function(node, raw) { + writer.text(node.value, node.raw); + }, + + // #comment + 8: function(node) { + writer.comment(node.value); + }, + + // Processing instruction + 7: function(node) { + writer.pi(node.name, node.value); + }, + + // Doctype + 10: function(node) { + writer.doctype(node.value); + }, + + // CDATA + 4: function(node) { + writer.cdata(node.value); + }, + + // Document fragment + 11: function(node) { + if ((node = node.firstChild)) { + do { + walk(node); + } while (node = node.next); + } + } + }; + + writer.reset(); + + function walk(node) { + var handler = handlers[node.type], name, isEmpty, attrs, attrName, attrValue, sortedAttrs, i, l, elementRule; + + if (!handler) { + name = node.name; + isEmpty = node.shortEnded; + attrs = node.attributes; + + // Sort attributes + if (validate && attrs && attrs.length > 1) { + sortedAttrs = []; + sortedAttrs.map = {}; + + elementRule = schema.getElementRule(node.name); + for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) { + attrName = elementRule.attributesOrder[i]; + + if (attrName in attrs.map) { + attrValue = attrs.map[attrName]; + sortedAttrs.map[attrName] = attrValue; + sortedAttrs.push({name: attrName, value: attrValue}); + } + } + + for (i = 0, l = attrs.length; i < l; i++) { + attrName = attrs[i].name; + + if (!(attrName in sortedAttrs.map)) { + attrValue = attrs.map[attrName]; + sortedAttrs.map[attrName] = attrValue; + sortedAttrs.push({name: attrName, value: attrValue}); + } + } + + attrs = sortedAttrs; + } + + writer.start(node.name, attrs, isEmpty); + + if (!isEmpty) { + if ((node = node.firstChild)) { + do { + walk(node); + } while (node = node.next); + } + + writer.end(name); + } + } else + handler(node); + } + + // Serialize element and treat all non elements as fragments + if (node.type == 1 && !settings.inner) + walk(node); + else + handlers[11](node); + + return writer.getContent(); + }; + } +})(tinymce); diff --git a/js/tiny_mce/classes/html/Styles.js b/js/tiny_mce/classes/html/Styles.js new file mode 100644 index 0000000000..1324cfba3e --- /dev/null +++ b/js/tiny_mce/classes/html/Styles.js @@ -0,0 +1,279 @@ +/** + * Styles.js + * + * Copyright 2010, Moxiecode Systems AB + * Released under LGPL License. + * + * License: http://tinymce.moxiecode.com/license + * Contributing: http://tinymce.moxiecode.com/contributing + */ + +/** + * This class is used to parse CSS styles it also compresses styles to reduce the output size. + * + * @example + * var Styles = new tinymce.html.Styles({ + * url_converter: function(url) { + * return url; + * } + * }); + * + * styles = Styles.parse('border: 1px solid red'); + * styles.color = 'red'; + * + * console.log(new tinymce.html.StyleSerializer().serialize(styles)); + * + * @class tinymce.html.Styles + * @version 3.4 + */ +tinymce.html.Styles = function(settings, schema) { + var rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi, + urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi, + styleRegExp = /\s*([^:]+):\s*([^;]+);?/g, + trimRightRegExp = /\s+$/, + urlColorRegExp = /rgb/, + undef, i, encodingLookup = {}, encodingItems; + + settings = settings || {}; + + encodingItems = '\\" \\\' \\; \\: ; : \uFEFF'.split(' '); + for (i = 0; i < encodingItems.length; i++) { + encodingLookup[encodingItems[i]] = '\uFEFF' + i; + encodingLookup['\uFEFF' + i] = encodingItems[i]; + } + + function toHex(match, r, g, b) { + function hex(val) { + val = parseInt(val).toString(16); + + return val.length > 1 ? val : '0' + val; // 0 -> 00 + }; + + return '#' + hex(r) + hex(g) + hex(b); + }; + + return { + /** + * Parses the specified RGB color value and returns a hex version of that color. + * + * @method toHex + * @param {String} color RGB string value like rgb(1,2,3) + * @return {String} Hex version of that RGB value like #FF00FF. + */ + toHex : function(color) { + return color.replace(rgbRegExp, toHex); + }, + + /** + * Parses the specified style value into an object collection. This parser will also + * merge and remove any redundant items that browsers might have added. It will also convert non hex + * colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings. + * + * @method parse + * @param {String} css Style value to parse for example: border:1px solid red;. + * @return {Object} Object representation of that style like {border : '1px solid red'} + */ + parse : function(css) { + var styles = {}, matches, name, value, isEncoded, urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope || this; + + function compress(prefix, suffix) { + var top, right, bottom, left; + + // Get values and check it it needs compressing + top = styles[prefix + '-top' + suffix]; + if (!top) + return; + + right = styles[prefix + '-right' + suffix]; + if (top != right) + return; + + bottom = styles[prefix + '-bottom' + suffix]; + if (right != bottom) + return; + + left = styles[prefix + '-left' + suffix]; + if (bottom != left) + return; + + // Compress + styles[prefix + suffix] = left; + delete styles[prefix + '-top' + suffix]; + delete styles[prefix + '-right' + suffix]; + delete styles[prefix + '-bottom' + suffix]; + delete styles[prefix + '-left' + suffix]; + }; + + /** + * Checks if the specific style can be compressed in other words if all border-width are equal. + */ + function canCompress(key) { + var value = styles[key], i; + + if (!value || value.indexOf(' ') < 0) + return; + + value = value.split(' '); + i = value.length; + while (i--) { + if (value[i] !== value[0]) + return false; + } + + styles[key] = value[0]; + + return true; + }; + + /** + * Compresses multiple styles into one style. + */ + function compress2(target, a, b, c) { + if (!canCompress(a)) + return; + + if (!canCompress(b)) + return; + + if (!canCompress(c)) + return; + + // Compress + styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c]; + delete styles[a]; + delete styles[b]; + delete styles[c]; + }; + + // Encodes the specified string by replacing all \" \' ; : with _ + function encode(str) { + isEncoded = true; + + return encodingLookup[str]; + }; + + // Decodes the specified string by replacing all _ with it's original value \" \' etc + // It will also decode the \" \' if keep_slashes is set to fale or omitted + function decode(str, keep_slashes) { + if (isEncoded) { + str = str.replace(/\uFEFF[0-9]/g, function(str) { + return encodingLookup[str]; + }); + } + + if (!keep_slashes) + str = str.replace(/\\([\'\";:])/g, "$1"); + + return str; + } + + if (css) { + // Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing + css = css.replace(/\\[\"\';:\uFEFF]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, function(str) { + return str.replace(/[;:]/g, encode); + }); + + // Parse styles + while (matches = styleRegExp.exec(css)) { + name = matches[1].replace(trimRightRegExp, '').toLowerCase(); + value = matches[2].replace(trimRightRegExp, ''); + + if (name && value.length > 0) { + // Opera will produce 700 instead of bold in their style values + if (name === 'font-weight' && value === '700') + value = 'bold'; + else if (name === 'color' || name === 'background-color') // Lowercase colors like RED + value = value.toLowerCase(); + + // Convert RGB colors to HEX + value = value.replace(rgbRegExp, toHex); + + // Convert URLs and force them into url('value') format + value = value.replace(urlOrStrRegExp, function(match, url, url2, url3, str, str2) { + str = str || str2; + + if (str) { + str = decode(str); + + // Force strings into single quote format + return "'" + str.replace(/\'/g, "\\'") + "'"; + } + + url = decode(url || url2 || url3); + + // Convert the URL to relative/absolute depending on config + if (urlConverter) + url = urlConverter.call(urlConverterScope, url, 'style'); + + // Output new URL format + return "url('" + url.replace(/\'/g, "\\'") + "')"; + }); + + styles[name] = isEncoded ? decode(value, true) : value; + } + + styleRegExp.lastIndex = matches.index + matches[0].length; + } + + // Compress the styles to reduce it's size for example IE will expand styles + compress("border", ""); + compress("border", "-width"); + compress("border", "-color"); + compress("border", "-style"); + compress("padding", ""); + compress("margin", ""); + compress2('border', 'border-width', 'border-style', 'border-color'); + + // Remove pointless border, IE produces these + if (styles.border === 'medium none') + delete styles.border; + } + + return styles; + }, + + /** + * Serializes the specified style object into a string. + * + * @method serialize + * @param {Object} styles Object to serialize as string for example: {border : '1px solid red'} + * @param {String} element_name Optional element name, if specified only the styles that matches the schema will be serialized. + * @return {String} String representation of the style object for example: border: 1px solid red. + */ + serialize : function(styles, element_name) { + var css = '', name, value; + + function serializeStyles(name) { + var styleList, i, l, value; + + styleList = schema.styles[name]; + if (styleList) { + for (i = 0, l = styleList.length; i < l; i++) { + name = styleList[i]; + value = styles[name]; + + if (value !== undef && value.length > 0) + css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; + } + } + }; + + // Serialize styles according to schema + if (element_name && schema && schema.styles) { + // Serialize global styles and element specific styles + serializeStyles('*'); + serializeStyles(element_name); + } else { + // Output the styles in the order they are inside the object + for (name in styles) { + value = styles[name]; + + if (value !== undef && value.length > 0) + css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; + } + } + + return css; + } + }; +}; diff --git a/js/tiny_mce/classes/html/Writer.js b/js/tiny_mce/classes/html/Writer.js new file mode 100644 index 0000000000..8010a0b8f3 --- /dev/null +++ b/js/tiny_mce/classes/html/Writer.js @@ -0,0 +1,186 @@ +/** + * Writer.js + * + * Copyright 2010, Moxiecode Systems AB + * Released under LGPL License. + * + * License: http://tinymce.moxiecode.com/license + * Contributing: http://tinymce.moxiecode.com/contributing + */ + +/** + * This class is used to write HTML tags out it can be used with the Serializer or the SaxParser. + * + * @class tinymce.html.Writer + * @example + * var writer = new tinymce.html.Writer({indent : true}); + * var parser = new tinymce.html.SaxParser(writer).parse('


    '); + * console.log(writer.getContent()); + * + * @class tinymce.html.Writer + * @version 3.4 + */ + +/** + * Constructs a new Writer instance. + * + * @constructor + * @method Writer + * @param {Object} settings Name/value settings object. + */ +tinymce.html.Writer = function(settings) { + var html = [], indent, indentBefore, indentAfter, encode, htmlOutput; + + settings = settings || {}; + indent = settings.indent; + indentBefore = tinymce.makeMap(settings.indent_before || ''); + indentAfter = tinymce.makeMap(settings.indent_after || ''); + encode = tinymce.html.Entities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities); + htmlOutput = settings.element_format == "html"; + + return { + /** + * Writes the a start element such as

    . + * + * @method start + * @param {String} name Name of the element. + * @param {Array} attrs Optional attribute array or undefined if it hasn't any. + * @param {Boolean} empty Optional empty state if the tag should end like
    . + */ + start: function(name, attrs, empty) { + var i, l, attr, value; + + if (indent && indentBefore[name] && html.length > 0) { + value = html[html.length - 1]; + + if (value.length > 0 && value !== '\n') + html.push('\n'); + } + + html.push('<', name); + + if (attrs) { + for (i = 0, l = attrs.length; i < l; i++) { + attr = attrs[i]; + html.push(' ', attr.name, '="', encode(attr.value, true), '"'); + } + } + + if (!empty || htmlOutput) + html[html.length] = '>'; + else + html[html.length] = ' />'; + + if (empty && indent && indentAfter[name] && html.length > 0) { + value = html[html.length - 1]; + + if (value.length > 0 && value !== '\n') + html.push('\n'); + } + }, + + /** + * Writes the a end element such as

    . + * + * @method end + * @param {String} name Name of the element. + */ + end: function(name) { + var value; + + /*if (indent && indentBefore[name] && html.length > 0) { + value = html[html.length - 1]; + + if (value.length > 0 && value !== '\n') + html.push('\n'); + }*/ + + html.push(''); + + if (indent && indentAfter[name] && html.length > 0) { + value = html[html.length - 1]; + + if (value.length > 0 && value !== '\n') + html.push('\n'); + } + }, + + /** + * Writes a text node. + * + * @method text + * @param {String} text String to write out. + * @param {Boolean} raw Optional raw state if true the contents wont get encoded. + */ + text: function(text, raw) { + if (text.length > 0) + html[html.length] = raw ? text : encode(text); + }, + + /** + * Writes a cdata node such as . + * + * @method cdata + * @param {String} text String to write out inside the cdata. + */ + cdata: function(text) { + html.push(''); + }, + + /** + * Writes a comment node such as . + * + * @method cdata + * @param {String} text String to write out inside the comment. + */ + comment: function(text) { + html.push(''); + }, + + /** + * Writes a PI node such as . + * + * @method pi + * @param {String} name Name of the pi. + * @param {String} text String to write out inside the pi. + */ + pi: function(name, text) { + if (text) + html.push(''); + else + html.push(''); + + if (indent) + html.push('\n'); + }, + + /** + * Writes a doctype node such as . + * + * @method doctype + * @param {String} text String to write out inside the doctype. + */ + doctype: function(text) { + html.push('', indent ? '\n' : ''); + }, + + /** + * Resets the internal buffer if one wants to reuse the writer. + * + * @method reset + */ + reset: function() { + html.length = 0; + }, + + /** + * Returns the contents that got serialized. + * + * @method getContent + * @return {String} HTML contents that got written down. + */ + getContent: function() { + return html.join('').replace(/\n$/, ''); + } + }; +}; diff --git a/js/tiny_mce/classes/tinymce.js b/js/tiny_mce/classes/tinymce.js index ebd3664ec2..6afea2fe2d 100644 --- a/js/tiny_mce/classes/tinymce.js +++ b/js/tiny_mce/classes/tinymce.js @@ -1,664 +1,869 @@ -/** - * tinymce.js - * - * Copyright 2009, Moxiecode Systems AB - * Released under LGPL License. - * - * License: http://tinymce.moxiecode.com/license - * Contributing: http://tinymce.moxiecode.com/contributing - */ - -(function(win) { - var whiteSpaceRe = /^\s*|\s*$/g, - undefined; - - /** - * Core namespace with core functionality for the TinyMCE API all sub classes will be added to this namespace/object. - * - * @static - * @class tinymce - * @example - * // Using each method - * tinymce.each([1, 2, 3], function(v, i) { - * console.log(i + '=' + v); - * }); - * - * // Checking for a specific browser - * if (tinymce.isIE) - * console.log("IE"); - */ - var tinymce = { - /** - * Major version of TinyMCE build. - * - * @property majorVersion - * @type String - */ - majorVersion : '@@tinymce_major_version@@', - - /** - * Major version of TinyMCE build. - * - * @property minorVersion - * @type String - */ - minorVersion : '@@tinymce_minor_version@@', - - /** - * Release date of TinyMCE build. - * - * @property minorVersion - * @type String - */ - releaseDate : '@@tinymce_release_date@@', - - /** - * Initializes the TinyMCE global namespace this will setup browser detection and figure out where TinyMCE is running from. - */ - _init : function() { - var t = this, d = document, na = navigator, ua = na.userAgent, i, nl, n, base, p, v; - - /** - * Constant that is true if the browser is Opera. - * - * @property isOpera - * @type Boolean - * @final - */ - t.isOpera = win.opera && opera.buildNumber; - - /** - * Constant that is true if the browser is WebKit (Safari/Chrome). - * - * @property isWebKit - * @type Boolean - * @final - */ - t.isWebKit = /WebKit/.test(ua); - - /** - * Constant that is true if the browser is IE. - * - * @property isIE - * @type Boolean - * @final - */ - t.isIE = !t.isWebKit && !t.isOpera && (/MSIE/gi).test(ua) && (/Explorer/gi).test(na.appName); - - /** - * Constant that is true if the browser is IE 6 or older. - * - * @property isIE6 - * @type Boolean - * @final - */ - t.isIE6 = t.isIE && /MSIE [56]/.test(ua); - - /** - * Constant that is true if the browser is Gecko. - * - * @property isGecko - * @type Boolean - * @final - */ - t.isGecko = !t.isWebKit && /Gecko/.test(ua); - - /** - * Constant that is true if the os is Mac OS. - * - * @property isMac - * @type Boolean - * @final - */ - t.isMac = ua.indexOf('Mac') != -1; - - /** - * Constant that is true if the runtime is Adobe Air. - * - * @property isAir - * @type Boolean - * @final - */ - t.isAir = /adobeair/i.test(ua); - - /** - * Constant that tells if the current browser is an iPhone or iPad. - * - * @property isIDevice - * @type Boolean - * @final - */ - t.isIDevice = /(iPad|iPhone)/.test(ua); - - // TinyMCE .NET webcontrol might be setting the values for TinyMCE - if (win.tinyMCEPreInit) { - t.suffix = tinyMCEPreInit.suffix; - t.baseURL = tinyMCEPreInit.base; - t.query = tinyMCEPreInit.query; - return; - } - - // Get suffix and base - t.suffix = ''; - - // If base element found, add that infront of baseURL - nl = d.getElementsByTagName('base'); - for (i=0; i : - s = /^((static) )?([\w.]+)(:([\w.]+))?/.exec(s); - cn = s[3].match(/(^|\.)(\w+)$/i)[2]; // Class name - - // Create namespace for new class - ns = t.createNS(s[3].replace(/\.\w+$/, '')); - - // Class already exists - if (ns[cn]) - return; - - // Make pure static class - if (s[2] == 'static') { - ns[cn] = p; - - if (this.onCreate) - this.onCreate(s[2], s[3], ns[cn]); - - return; - } - - // Create default constructor - if (!p[cn]) { - p[cn] = function() {}; - de = 1; - } - - // Add constructor and methods - ns[cn] = p[cn]; - t.extend(ns[cn].prototype, p); - - // Extend - if (s[5]) { - sp = t.resolve(s[5]).prototype; - scn = s[5].match(/\.(\w+)$/i)[1]; // Class name - - // Extend constructor - c = ns[cn]; - if (de) { - // Add passthrough constructor - ns[cn] = function() { - return sp[scn].apply(this, arguments); - }; - } else { - // Add inherit constructor - ns[cn] = function() { - this.parent = sp[scn]; - return c.apply(this, arguments); - }; - } - ns[cn].prototype[cn] = ns[cn]; - - // Add super methods - t.each(sp, function(f, n) { - ns[cn].prototype[n] = sp[n]; - }); - - // Add overridden methods - t.each(p, function(f, n) { - // Extend methods if needed - if (sp[n]) { - ns[cn].prototype[n] = function() { - this.parent = sp[n]; - return f.apply(this, arguments); - }; - } else { - if (n != cn) - ns[cn].prototype[n] = f; - } - }); - } - - // Add static methods - t.each(p['static'], function(f, n) { - ns[cn][n] = f; - }); - - if (this.onCreate) - this.onCreate(s[2], s[3], ns[cn].prototype); - }, - - /** - * Executed the specified function for each item in a object tree. - * - * @method walk - * @param {Object} o Object tree to walk though. - * @param {function} f Function to call for each item. - * @param {String} n Optional name of collection inside the objects to walk for example childNodes. - * @param {String} s Optional scope to execute the function in. - */ - walk : function(o, f, n, s) { - s = s || this; - - if (o) { - if (n) - o = o[n]; - - tinymce.each(o, function(o, i) { - if (f.call(s, o, i, n) === false) - return false; - - tinymce.walk(o, f, n, s); - }); - } - }, - - /** - * Creates a namespace on a specific object. - * - * @method createNS - * @param {String} n Namespace to create for example a.b.c.d. - * @param {Object} o Optional object to add namespace to, defaults to window. - * @return {Object} New namespace object the last item in path. - */ - createNS : function(n, o) { - var i, v; - - o = o || win; - - n = n.split('.'); - for (i=0; i=534; + + // TinyMCE .NET webcontrol might be setting the values for TinyMCE + if (win.tinyMCEPreInit) { + t.suffix = tinyMCEPreInit.suffix; + t.baseURL = tinyMCEPreInit.base; + t.query = tinyMCEPreInit.query; + return; + } + + // Get suffix and base + t.suffix = ''; + + // If base element found, add that infront of baseURL + nl = d.getElementsByTagName('base'); + for (i=0; i 3;}); + */ + grep : function(a, f) { + var o = []; + + tinymce.each(a, function(v) { + if (!f || f(v)) + o.push(v); + }); + + return o; + }, + + /** + * Returns the index of a value in an array, this method will return -1 if the item wasn't found. + * + * @method inArray + * @param {Array} a Array/Object to search for value in. + * @param {Object} v Value to check for inside the array. + * @return {Number/String} Index of item inside the array inside an object. Or -1 if it wasn't found. + * @example + * // Get index of value in array this will alert 1 since 2 is at that index + * alert(tinymce.inArray([1,2,3], 2)); + */ + inArray : function(a, v) { + var i, l; + + if (a) { + for (i = 0, l = a.length; i < l; i++) { + if (a[i] === v) + return i; + } + } + + return -1; + }, + + /** + * Extends an object with the specified other object(s). + * + * @method extend + * @param {Object} o Object to extend with new items. + * @param {Object} e..n Object(s) to extend the specified object with. + * @return {Object} o New extended object, same reference as the input object. + * @example + * // Extends obj1 with two new fields + * var obj = tinymce.extend(obj1, { + * somefield1 : 'a', + * somefield2 : 'a' + * }); + * + * // Extends obj with obj2 and obj3 + * tinymce.extend(obj, obj2, obj3); + */ + extend : function(o, e) { + var i, l, a = arguments; + + for (i = 1, l = a.length; i < l; i++) { + e = a[i]; + + tinymce.each(e, function(v, n) { + if (v !== undefined) + o[n] = v; + }); + } + + return o; + }, + + // #endif + + /** + * Removes whitespace from the beginning and end of a string. + * + * @method trim + * @param {String} s String to remove whitespace from. + * @return {String} New string with removed whitespace. + */ + trim : function(s) { + return (s ? '' + s : '').replace(whiteSpaceRe, ''); + }, + + /** + * Creates a class, subclass or static singleton. + * More details on this method can be found in the Wiki. + * + * @method create + * @param {String} s Class name, inheritage and prefix. + * @param {Object} p Collection of methods to add to the class. + * @param {Object} root Optional root object defaults to the global window object. + * @example + * // Creates a basic class + * tinymce.create('tinymce.somepackage.SomeClass', { + * SomeClass : function() { + * // Class constructor + * }, + * + * method : function() { + * // Some method + * } + * }); + * + * // Creates a basic subclass class + * tinymce.create('tinymce.somepackage.SomeSubClass:tinymce.somepackage.SomeClass', { + * SomeSubClass: function() { + * // Class constructor + * this.parent(); // Call parent constructor + * }, + * + * method : function() { + * // Some method + * this.parent(); // Call parent method + * }, + * + * 'static' : { + * staticMethod : function() { + * // Static method + * } + * } + * }); + * + * // Creates a singleton/static class + * tinymce.create('static tinymce.somepackage.SomeSingletonClass', { + * method : function() { + * // Some method + * } + * }); + */ + create : function(s, p, root) { + var t = this, sp, ns, cn, scn, c, de = 0; + + // Parse : : + s = /^((static) )?([\w.]+)(:([\w.]+))?/.exec(s); + cn = s[3].match(/(^|\.)(\w+)$/i)[2]; // Class name + + // Create namespace for new class + ns = t.createNS(s[3].replace(/\.\w+$/, ''), root); + + // Class already exists + if (ns[cn]) + return; + + // Make pure static class + if (s[2] == 'static') { + ns[cn] = p; + + if (this.onCreate) + this.onCreate(s[2], s[3], ns[cn]); + + return; + } + + // Create default constructor + if (!p[cn]) { + p[cn] = function() {}; + de = 1; + } + + // Add constructor and methods + ns[cn] = p[cn]; + t.extend(ns[cn].prototype, p); + + // Extend + if (s[5]) { + sp = t.resolve(s[5]).prototype; + scn = s[5].match(/\.(\w+)$/i)[1]; // Class name + + // Extend constructor + c = ns[cn]; + if (de) { + // Add passthrough constructor + ns[cn] = function() { + return sp[scn].apply(this, arguments); + }; + } else { + // Add inherit constructor + ns[cn] = function() { + this.parent = sp[scn]; + return c.apply(this, arguments); + }; + } + ns[cn].prototype[cn] = ns[cn]; + + // Add super methods + t.each(sp, function(f, n) { + ns[cn].prototype[n] = sp[n]; + }); + + // Add overridden methods + t.each(p, function(f, n) { + // Extend methods if needed + if (sp[n]) { + ns[cn].prototype[n] = function() { + this.parent = sp[n]; + return f.apply(this, arguments); + }; + } else { + if (n != cn) + ns[cn].prototype[n] = f; + } + }); + } + + // Add static methods + t.each(p['static'], function(f, n) { + ns[cn][n] = f; + }); + + if (this.onCreate) + this.onCreate(s[2], s[3], ns[cn].prototype); + }, + + /** + * Executed the specified function for each item in a object tree. + * + * @method walk + * @param {Object} o Object tree to walk though. + * @param {function} f Function to call for each item. + * @param {String} n Optional name of collection inside the objects to walk for example childNodes. + * @param {String} s Optional scope to execute the function in. + */ + walk : function(o, f, n, s) { + s = s || this; + + if (o) { + if (n) + o = o[n]; + + tinymce.each(o, function(o, i) { + if (f.call(s, o, i, n) === false) + return false; + + tinymce.walk(o, f, n, s); + }); + } + }, + + /** + * Creates a namespace on a specific object. + * + * @method createNS + * @param {String} n Namespace to create for example a.b.c.d. + * @param {Object} o Optional object to add namespace to, defaults to window. + * @return {Object} New namespace object the last item in path. + * @example + * // Create some namespace + * tinymce.createNS('tinymce.somepackage.subpackage'); + * + * // Add a singleton + * var tinymce.somepackage.subpackage.SomeSingleton = { + * method : function() { + * // Some method + * } + * }; + */ + createNS : function(n, o) { + var i, v; + + o = o || win; + + n = n.split('.'); + for (i=0; i'; - - if (s.image) - h += '' + l + ''; + h = ''; + if (s.image && !(this.editor &&this.editor.forcedHighContrastMode) ) + h += '' + DOM.encode(s.title) + '' + l; else - h += '' + (l ? '' + l + '' : '') + ''; + h += '' + (l ? '' + l + '' : ''); + h += ''; + h += ''; return h; }, diff --git a/js/tiny_mce/classes/ui/ColorSplitButton.js b/js/tiny_mce/classes/ui/ColorSplitButton.js index 9391dc8862..d389ecaa11 100644 --- a/js/tiny_mce/classes/ui/ColorSplitButton.js +++ b/js/tiny_mce/classes/ui/ColorSplitButton.js @@ -26,11 +26,12 @@ * @method ColorSplitButton * @param {String} id Control id for the color split button. * @param {Object} s Optional name/value settings object. + * @param {Editor} ed The editor instance this button is for. */ - ColorSplitButton : function(id, s) { + ColorSplitButton : function(id, s, ed) { var t = this; - t.parent(id, s); + t.parent(id, s, ed); /** * Settings object. @@ -123,20 +124,21 @@ hideMenu : function(e) { var t = this; - // Prevent double toogles by canceling the mouse click event to the button - if (e && e.type == "mousedown" && DOM.getParent(e.target, function(e) {return e.id === t.id + '_open';})) - return; - - if (!e || !DOM.getParent(e.target, '.mceSplitButtonMenu')) { - DOM.removeClass(t.id, 'mceSplitButtonSelected'); - Event.remove(DOM.doc, 'mousedown', t.hideMenu, t); - Event.remove(t.id + '_menu', 'keydown', t._keyHandler); - DOM.hide(t.id + '_menu'); - } + if (t.isMenuVisible) { + // Prevent double toogles by canceling the mouse click event to the button + if (e && e.type == "mousedown" && DOM.getParent(e.target, function(e) {return e.id === t.id + '_open';})) + return; - t.onHideMenu.dispatch(t); + if (!e || !DOM.getParent(e.target, '.mceSplitButtonMenu')) { + DOM.removeClass(t.id, 'mceSplitButtonSelected'); + Event.remove(DOM.doc, 'mousedown', t.hideMenu, t); + Event.remove(t.id + '_menu', 'keydown', t._keyHandler); + DOM.hide(t.id + '_menu'); + } - t.isMenuVisible = 0; + t.isMenuVisible = 0; + t.onHideMenu.dispatch(); + } }, /** @@ -145,13 +147,13 @@ * @method renderMenu */ renderMenu : function() { - var t = this, m, i = 0, s = t.settings, n, tb, tr, w; + var t = this, m, i = 0, s = t.settings, n, tb, tr, w, context; - w = DOM.add(s.menu_container, 'div', {id : t.id + '_menu', 'class' : s['menu_class'] + ' ' + s['class'], style : 'position:absolute;left:0;top:-1000px;'}); + w = DOM.add(s.menu_container, 'div', {role: 'listbox', id : t.id + '_menu', 'class' : s['menu_class'] + ' ' + s['class'], style : 'position:absolute;left:0;top:-1000px;'}); m = DOM.add(w, 'div', {'class' : s['class'] + ' mceSplitButtonMenu'}); DOM.add(m, 'span', {'class' : 'mceMenuLine'}); - n = DOM.add(m, 'table', {'class' : 'mceColorSplitMenu'}); + n = DOM.add(m, 'table', {role: 'presentation', 'class' : 'mceColorSplitMenu'}); tb = DOM.add(n, 'tbody'); // Generate color grid @@ -165,20 +167,32 @@ } n = DOM.add(tr, 'td'); - n = DOM.add(n, 'a', { + role : 'option', href : 'javascript:;', style : { backgroundColor : '#' + c }, - _mce_color : '#' + c + 'title': t.editor.getLang('colors.' + c, c), + 'data-mce-color' : '#' + c }); + + if (t.editor.forcedHighContrastMode) { + n = DOM.add(n, 'canvas', { width: 16, height: 16, 'aria-hidden': 'true' }); + if (n.getContext && (context = n.getContext("2d"))) { + context.fillStyle = '#' + c; + context.fillRect(0, 0, 16, 16); + } else { + // No point leaving a canvas element around if it's not supported for drawing on anyway. + DOM.remove(n); + } + } }); if (s.more_colors_func) { n = DOM.add(tb, 'tr'); n = DOM.add(n, 'td', {colspan : s.grid_width, 'class' : 'mceMoreColors'}); - n = DOM.add(n, 'a', {id : t.id + '_more', href : 'javascript:;', onclick : 'return false;', 'class' : 'mceMoreColors'}, s.more_colors_title); + n = DOM.add(n, 'a', {role: 'option', id : t.id + '_more', href : 'javascript:;', onclick : 'return false;', 'class' : 'mceMoreColors'}, s.more_colors_title); Event.add(n, 'click', function(e) { s.more_colors_func.call(s.more_colors_scope || this); @@ -187,13 +201,25 @@ } DOM.addClass(m, 'mceColorSplitMenu'); + + new tinymce.ui.KeyboardNavigation({ + root: t.id + '_menu', + items: DOM.select('a', t.id + '_menu'), + onCancel: function() { + t.hideMenu(); + t.focus(); + } + }); + + // Prevent IE from scrolling and hindering click to occur #4019 + Event.add(t.id + '_menu', 'mousedown', function(e) {return Event.cancel(e);}); Event.add(t.id + '_menu', 'click', function(e) { var c; - e = e.target; + e = DOM.getParent(e.target, 'a', tb); - if (e.nodeName == 'A' && (c = e.getAttribute('_mce_color'))) + if (e && e.nodeName.toLowerCase() == 'a' && (c = e.getAttribute('data-mce-color'))) t.setColor(c); return Event.cancel(e); // Prevent IE auto save warning @@ -209,13 +235,23 @@ * @param {String} c Color code value in hex for example: #FF00FF */ setColor : function(c) { + this.displayColor(c); + this.hideMenu(); + this.settings.onselect(c); + }, + + /** + * Change the currently selected color for the control. + * + * @method displayColor + * @param {String} c Color code value in hex for example: #FF00FF + */ + displayColor : function(c) { var t = this; DOM.setStyle(t.id + '_preview', 'backgroundColor', c); t.value = c; - t.hideMenu(); - t.settings.onselect(c); }, /** diff --git a/js/tiny_mce/classes/ui/Container.js b/js/tiny_mce/classes/ui/Container.js index 1a6a6aa34e..47d5b0e8f7 100644 --- a/js/tiny_mce/classes/ui/Container.js +++ b/js/tiny_mce/classes/ui/Container.js @@ -24,8 +24,8 @@ tinymce.create('tinymce.ui.Container:tinymce.ui.Control', { * @param {String} id Control id to use for the container. * @param {Object} s Optional name/value settings object. */ - Container : function(id, s) { - this.parent(id, s); + Container : function(id, s, editor) { + this.parent(id, s, editor); /** * Array of controls added to the container. diff --git a/js/tiny_mce/classes/ui/Control.js b/js/tiny_mce/classes/ui/Control.js index d5635ffe92..1564c827bd 100644 --- a/js/tiny_mce/classes/ui/Control.js +++ b/js/tiny_mce/classes/ui/Control.js @@ -27,7 +27,7 @@ * @param {String} id Control id. * @param {Object} s Optional name/value settings object. */ - Control : function(id, s) { + Control : function(id, s, editor) { this.id = id; this.settings = s = s || {}; this.rendered = false; @@ -36,6 +36,18 @@ this.scope = s.scope || this; this.disabled = 0; this.active = 0; + this.editor = editor; + }, + + setAriaProperty : function(property, value) { + var element = DOM.get(this.id + '_aria') || DOM.get(this.id); + if (element) { + DOM.setAttrib(element, 'aria-' + property, !!value); + } + }, + + focus : function() { + DOM.get(this.id).focus(); }, /** @@ -46,19 +58,8 @@ * @param {Boolean} s Boolean state if the control should be disabled or not. */ setDisabled : function(s) { - var e; - if (s != this.disabled) { - e = DOM.get(this.id); - - // Add accessibility title for unavailable actions - if (e && this.settings.unavailable_prefix) { - if (s) { - this.prevTitle = e.title; - e.title = this.settings.unavailable_prefix + ": " + e.title; - } else - e.title = this.prevTitle; - } + this.setAriaProperty('disabled', s); this.setState('Disabled', s); this.setState('Enabled', !s); @@ -88,6 +89,7 @@ if (s != this.active) { this.setState('Active', s); this.active = s; + this.setAriaProperty('pressed', s); } }, diff --git a/js/tiny_mce/classes/ui/DropMenu.js b/js/tiny_mce/classes/ui/DropMenu.js index cc4390c3f7..84ef334788 100644 --- a/js/tiny_mce/classes/ui/DropMenu.js +++ b/js/tiny_mce/classes/ui/DropMenu.js @@ -17,6 +17,50 @@ * * @class tinymce.ui.DropMenu * @extends tinymce.ui.Menu + * @example + * // Adds a menu to the currently active editor instance + * var dm = tinyMCE.activeEditor.controlManager.createDropMenu('somemenu'); + * + * // Add some menu items + * dm.add({title : 'Menu 1', onclick : function() { + * alert('Item 1 was clicked.'); + * }}); + * + * dm.add({title : 'Menu 2', onclick : function() { + * alert('Item 2 was clicked.'); + * }}); + * + * // Adds a submenu + * var sub1 = dm.addMenu({title : 'Menu 3'}); + * sub1.add({title : 'Menu 1.1', onclick : function() { + * alert('Item 1.1 was clicked.'); + * }}); + * + * // Adds a horizontal separator + * sub1.addSeparator(); + * + * sub1.add({title : 'Menu 1.2', onclick : function() { + * alert('Item 1.2 was clicked.'); + * }}); + * + * // Adds a submenu to the submenu + * var sub2 = sub1.addMenu({title : 'Menu 1.3'}); + * + * // Adds items to the sub sub menu + * sub2.add({title : 'Menu 1.3.1', onclick : function() { + * alert('Item 1.3.1 was clicked.'); + * }}); + * + * sub2.add({title : 'Menu 1.3.2', onclick : function() { + * alert('Item 1.3.2 was clicked.'); + * }}); + * + * dm.add({title : 'Menu 4', onclick : function() { + * alert('Item 3 was clicked.'); + * }}); + * + * // Display the menu at position 100, 100 + * dm.showMenu(100, 100); */ tinymce.create('tinymce.ui.DropMenu:tinymce.ui.Menu', { /** @@ -60,12 +104,20 @@ s['class'] = s['class'] || cs['class']; s.vp_offset_x = s.vp_offset_x || cs.vp_offset_x; s.vp_offset_y = s.vp_offset_y || cs.vp_offset_y; + s.keyboard_focus = cs.keyboard_focus; m = new tinymce.ui.DropMenu(s.id || DOM.uniqueId(), s); m.onAddItem.add(t.onAddItem.dispatch, t.onAddItem); return m; }, + + focus : function() { + var t = this; + if (t.keyboardNav) { + t.keyboardNav.focus(); + } + }, /** * Repaints the menu after new items have been added dynamically. @@ -202,13 +254,13 @@ } }); } + + Event.add(co, 'keydown', t._keyHandler, t); t.onShowMenu.dispatch(t); - if (s.keyboard_focus) { - Event.add(co, 'keydown', t._keyHandler, t); - DOM.select('a', 'menu_' + t.id)[0].focus(); // Select first link - t._focusIdx = 0; + if (s.keyboard_focus) { + t._setupKeyboardNav(); } }, @@ -223,6 +275,7 @@ if (!t.isMenuVisible) return; + if (t.keyboardNav) t.keyboardNav.destroy(); Event.remove(co, 'mouseover', t.mouseOverFunc); Event.remove(co, 'click', t.mouseClickFunc); Event.remove(co, 'keydown', t._keyHandler); @@ -292,8 +345,11 @@ destroy : function() { var t = this, co = DOM.get('menu_' + t.id); + if (t.keyboardNav) t.keyboardNav.destroy(); Event.remove(co, 'mouseover', t.mouseOverFunc); + Event.remove(DOM.select('a', co), 'focus', t.mouseOverFunc); Event.remove(co, 'click', t.mouseClickFunc); + Event.remove(co, 'keydown', t._keyHandler); if (t.element) t.element.remove(); @@ -310,15 +366,18 @@ renderNode : function() { var t = this, s = t.settings, n, tb, co, w; - w = DOM.create('div', {id : 'menu_' + t.id, 'class' : s['class'], 'style' : 'position:absolute;left:0;top:0;z-index:200000'}); - co = DOM.add(w, 'div', {id : 'menu_' + t.id + '_co', 'class' : t.classPrefix + (s['class'] ? ' ' + s['class'] : '')}); + w = DOM.create('div', {role: 'listbox', id : 'menu_' + t.id, 'class' : s['class'], 'style' : 'position:absolute;left:0;top:0;z-index:200000;outline:0'}); + if (t.settings.parent) { + DOM.setAttrib(w, 'aria-parent', 'menu_' + t.settings.parent.id); + } + co = DOM.add(w, 'div', {role: 'presentation', id : 'menu_' + t.id + '_co', 'class' : t.classPrefix + (s['class'] ? ' ' + s['class'] : '')}); t.element = new Element('menu_' + t.id, {blocker : 1, container : s.container}); if (s.menu_line) DOM.add(co, 'span', {'class' : t.classPrefix + 'Line'}); // n = DOM.add(co, 'div', {id : 'menu_' + t.id + '_co', 'class' : 'mceMenuContainer'}); - n = DOM.add(co, 'table', {id : 'menu_' + t.id + '_tbl', border : 0, cellPadding : 0, cellSpacing : 0}); + n = DOM.add(co, 'table', {role: 'presentation', id : 'menu_' + t.id + '_tbl', border : 0, cellPadding : 0, cellSpacing : 0}); tb = DOM.add(n, 'tbody'); each(t.items, function(o) { @@ -331,33 +390,36 @@ }, // Internal functions + _setupKeyboardNav : function(){ + var contextMenu, menuItems, t=this; + contextMenu = DOM.select('#menu_' + t.id)[0]; + menuItems = DOM.select('a[role=option]', 'menu_' + t.id); + menuItems.splice(0,0,contextMenu); + t.keyboardNav = new tinymce.ui.KeyboardNavigation({ + root: 'menu_' + t.id, + items: menuItems, + onCancel: function() { + t.hideMenu(); + }, + enableUpDown: true + }); + contextMenu.focus(); + }, - _keyHandler : function(e) { - var t = this, kc = e.keyCode; - - function focus(d) { - var i = t._focusIdx + d, e = DOM.select('a', 'menu_' + t.id)[i]; - - if (e) { - t._focusIdx = i; - e.focus(); - } - }; - - switch (kc) { - case 38: - focus(-1); // Select first link - return; - - case 40: - focus(1); - return; - - case 13: - return; - - case 27: - return this.hideMenu(); + _keyHandler : function(evt) { + var t = this, e; + switch (evt.keyCode) { + case 37: // Left + if (t.settings.parent) { + t.hideMenu(); + t.settings.parent.focus(); + Event.cancel(evt); + } + break; + case 39: // Right + if (t.mouseOverFunc) + t.mouseOverFunc(evt); + break; } }, @@ -375,8 +437,13 @@ } n = ro = DOM.add(tb, 'tr', {id : o.id, 'class' : cp + 'Item ' + cp + 'ItemEnabled'}); - n = it = DOM.add(n, 'td'); - n = a = DOM.add(n, 'a', {href : 'javascript:;', onclick : "return false;", onmousedown : 'return false;'}); + n = it = DOM.add(n, s.titleItem ? 'th' : 'td'); + n = a = DOM.add(n, 'a', {id: o.id + '_aria', role: s.titleItem ? 'presentation' : 'option', href : 'javascript:;', onclick : "return false;", onmousedown : 'return false;'}); + + if (s.parent) { + DOM.setAttrib(a, 'aria-haspopup', 'true'); + DOM.setAttrib(a, 'aria-owns', 'menu_' + o.id); + } DOM.addClass(it, s['class']); // n = DOM.add(n, 'span', {'class' : 'item'}); diff --git a/js/tiny_mce/classes/ui/KeyboardNavigation.js b/js/tiny_mce/classes/ui/KeyboardNavigation.js new file mode 100644 index 0000000000..b083704083 --- /dev/null +++ b/js/tiny_mce/classes/ui/KeyboardNavigation.js @@ -0,0 +1,183 @@ +/** + * KeyboardNavigation.js + * + * Copyright 2011, Moxiecode Systems AB + * Released under LGPL License. + * + * License: http://tinymce.moxiecode.com/license + * Contributing: http://tinymce.moxiecode.com/contributing + */ + +(function(tinymce) { + var Event = tinymce.dom.Event, each = tinymce.each; + + /** + * This class provides basic keyboard navigation using the arrow keys to children of a component. + * For example, this class handles moving between the buttons on the toolbars. + * + * @class tinymce.ui.KeyboardNavigation + */ + tinymce.create('tinymce.ui.KeyboardNavigation', { + /** + * Create a new KeyboardNavigation instance to handle the focus for a specific element. + * + * @constructor + * @method KeyboardNavigation + * @param {Object} settings the settings object to define how keyboard navigation works. + * @param {DOMUtils} dom the DOMUtils instance to use. + * + * @setting {Element/String} root the root element or ID of the root element for the control. + * @setting {Array} items an array containing the items to move focus between. Every object in this array must have an id attribute which maps to the actual DOM element. If the actual elements are passed without an ID then one is automatically assigned. + * @setting {Function} onCancel the callback for when the user presses escape or otherwise indicates cancelling. + * @setting {Function} onAction (optional) the action handler to call when the user activates an item. + * @setting {Boolean} enableLeftRight (optional, default) when true, the up/down arrows move through items. + * @setting {Boolean} enableUpDown (optional) when true, the up/down arrows move through items. + * Note for both up/down and left/right explicitly set both enableLeftRight and enableUpDown to true. + */ + KeyboardNavigation: function(settings, dom) { + var t = this, root = settings.root, items = settings.items, + enableUpDown = settings.enableUpDown, enableLeftRight = settings.enableLeftRight || !settings.enableUpDown, + excludeFromTabOrder = settings.excludeFromTabOrder, + itemFocussed, itemBlurred, rootKeydown, rootFocussed, focussedId; + + dom = dom || tinymce.DOM; + + itemFocussed = function(evt) { + focussedId = evt.target.id; + }; + + itemBlurred = function(evt) { + dom.setAttrib(evt.target.id, 'tabindex', '-1'); + }; + + rootFocussed = function(evt) { + var item = dom.get(focussedId); + dom.setAttrib(item, 'tabindex', '0'); + item.focus(); + }; + + t.focus = function() { + dom.get(focussedId).focus(); + }; + + /** + * Destroys the KeyboardNavigation and unbinds any focus/blur event handles it might have added. + * + * @method destroy + */ + t.destroy = function() { + each(items, function(item) { + dom.unbind(dom.get(item.id), 'focus', itemFocussed); + dom.unbind(dom.get(item.id), 'blur', itemBlurred); + }); + + dom.unbind(dom.get(root), 'focus', rootFocussed); + dom.unbind(dom.get(root), 'keydown', rootKeydown); + + items = dom = root = t.focus = itemFocussed = itemBlurred = rootKeydown = rootFocussed = null; + t.destroy = function() {}; + }; + + t.moveFocus = function(dir, evt) { + var idx = -1, controls = t.controls, newFocus; + + if (!focussedId) + return; + + each(items, function(item, index) { + if (item.id === focussedId) { + idx = index; + return false; + } + }); + + idx += dir; + if (idx < 0) { + idx = items.length - 1; + } else if (idx >= items.length) { + idx = 0; + } + + newFocus = items[idx]; + dom.setAttrib(focussedId, 'tabindex', '-1'); + dom.setAttrib(newFocus.id, 'tabindex', '0'); + dom.get(newFocus.id).focus(); + + if (settings.actOnFocus) { + settings.onAction(newFocus.id); + } + + if (evt) + Event.cancel(evt); + }; + + rootKeydown = function(evt) { + var DOM_VK_LEFT = 37, DOM_VK_RIGHT = 39, DOM_VK_UP = 38, DOM_VK_DOWN = 40, DOM_VK_ESCAPE = 27, DOM_VK_ENTER = 14, DOM_VK_RETURN = 13, DOM_VK_SPACE = 32; + + switch (evt.keyCode) { + case DOM_VK_LEFT: + if (enableLeftRight) t.moveFocus(-1); + break; + + case DOM_VK_RIGHT: + if (enableLeftRight) t.moveFocus(1); + break; + + case DOM_VK_UP: + if (enableUpDown) t.moveFocus(-1); + break; + + case DOM_VK_DOWN: + if (enableUpDown) t.moveFocus(1); + break; + + case DOM_VK_ESCAPE: + if (settings.onCancel) { + settings.onCancel(); + Event.cancel(evt); + } + break; + + case DOM_VK_ENTER: + case DOM_VK_RETURN: + case DOM_VK_SPACE: + if (settings.onAction) { + settings.onAction(focussedId); + Event.cancel(evt); + } + break; + } + }; + + // Set up state and listeners for each item. + each(items, function(item, idx) { + var tabindex; + + if (!item.id) { + item.id = dom.uniqueId('_mce_item_'); + } + + if (excludeFromTabOrder) { + dom.bind(item.id, 'blur', itemBlurred); + tabindex = '-1'; + } else { + tabindex = (idx === 0 ? '0' : '-1'); + } + + dom.setAttrib(item.id, 'tabindex', tabindex); + dom.bind(dom.get(item.id), 'focus', itemFocussed); + }); + + // Setup initial state for root element. + if (items[0]){ + focussedId = items[0].id; + } + + dom.setAttrib(root, 'tabindex', '-1'); + + // Setup listeners for root element. + dom.bind(dom.get(root), 'focus', rootFocussed); + dom.bind(dom.get(root), 'keydown', rootKeydown); + } + }); +})(tinymce); diff --git a/js/tiny_mce/classes/ui/ListBox.js b/js/tiny_mce/classes/ui/ListBox.js index 54bd42ec77..ced8cb9ab4 100644 --- a/js/tiny_mce/classes/ui/ListBox.js +++ b/js/tiny_mce/classes/ui/ListBox.js @@ -17,6 +17,41 @@ * * @class tinymce.ui.ListBox * @extends tinymce.ui.Control + * @example + * // Creates a new plugin class and a custom listbox + * tinymce.create('tinymce.plugins.ExamplePlugin', { + * createControl: function(n, cm) { + * switch (n) { + * case 'mylistbox': + * var mlb = cm.createListBox('mylistbox', { + * title : 'My list box', + * onselect : function(v) { + * tinyMCE.activeEditor.windowManager.alert('Value selected:' + v); + * } + * }); + * + * // Add some values to the list box + * mlb.add('Some item 1', 'val1'); + * mlb.add('some item 2', 'val2'); + * mlb.add('some item 3', 'val3'); + * + * // Return the new listbox instance + * return mlb; + * } + * + * return null; + * } + * }); + * + * // Register plugin with a short name + * tinymce.PluginManager.add('example', tinymce.plugins.ExamplePlugin); + * + * // Initialize TinyMCE with the new plugin and button + * tinyMCE.init({ + * ... + * plugins : '-example', // - means TinyMCE will not try to load it + * theme_advanced_buttons1 : 'mylistbox' // Add the new example listbox to the toolbar + * }); */ tinymce.create('tinymce.ui.ListBox:tinymce.ui.Control', { /** @@ -26,11 +61,12 @@ * @method ListBox * @param {String} id Control id for the list box. * @param {Object} s Optional name/value settings object. + * @param {Editor} ed Optional the editor instance this button is for. */ - ListBox : function(id, s) { + ListBox : function(id, s, ed) { var t = this; - t.parent(id, s); + t.parent(id, s, ed); /** * Array of ListBox items. @@ -117,23 +153,27 @@ * @param {String} idx Index to select, pass -1 to select menu/title of select box. */ selectByIndex : function(idx) { - var t = this, e, o; + var t = this, e, o, label; if (idx != t.selectedIndex) { e = DOM.get(t.id + '_text'); + label = DOM.get(t.id + '_voiceDesc'); o = t.items[idx]; if (o) { t.selectedValue = o.value; t.selectedIndex = idx; DOM.setHTML(e, DOM.encode(o.title)); + DOM.setHTML(label, t.settings.title + " - " + o.title); DOM.removeClass(e, 'mceTitle'); + DOM.setAttrib(t.id, 'aria-valuenow', o.title); } else { DOM.setHTML(e, DOM.encode(t.settings.title)); + DOM.setHTML(label, DOM.encode(t.settings.title)); DOM.addClass(e, 'mceTitle'); t.selectedValue = t.selectedIndex = null; + DOM.setAttrib(t.id, 'aria-valuenow', t.settings.title); } - e = 0; } }, @@ -179,10 +219,11 @@ renderHTML : function() { var h = '', t = this, s = t.settings, cp = t.classPrefix; - h = ''; - h += ''; - h += ''; - h += '
    ' + DOM.createHTML('a', {id : t.id + '_text', href : 'javascript:;', 'class' : 'mceText', onclick : "return false;", onmousedown : 'return false;'}, DOM.encode(t.settings.title)) + '' + DOM.createHTML('a', {id : t.id + '_open', tabindex : -1, href : 'javascript:;', 'class' : 'mceOpen', onclick : "return false;", onmousedown : 'return false;'}, '') + '
    '; + h = ''; + h += ''; + h += ''; + h += ''; return h; }, @@ -193,7 +234,7 @@ * @method showMenu */ showMenu : function() { - var t = this, p1, p2, e = DOM.get(this.id), m; + var t = this, p2, e = DOM.get(this.id), m; if (t.isDisabled() || t.items.length == 0) return; @@ -206,7 +247,6 @@ t.isMenuRendered = true; } - p1 = DOM.getPos(this.settings.menu_container); p2 = DOM.getPos(e); m = t.menu; @@ -242,6 +282,8 @@ var t = this; if (t.menu && t.menu.isMenuVisible) { + DOM.removeClass(t.id, t.classPrefix + 'Selected'); + // Prevent double toogles by canceling the mouse click event to the button if (e && e.type == "mousedown" && (e.target.id == t.id + '_text' || e.target.id == t.id + '_open')) return; @@ -269,7 +311,10 @@ max_height : 150 }); - m.onHideMenu.add(t.hideMenu, t); + m.onHideMenu.add(function() { + t.hideMenu(); + t.focus(); + }); m.add({ title : t.settings.title, @@ -285,6 +330,7 @@ if (o.value === undefined) { m.add({ title : o.title, + role : "option", 'class' : 'mceMenuItemTitle', onclick : function() { if (t.settings.onselect('') !== false) @@ -293,6 +339,7 @@ }); } else { o.id = DOM.uniqueId(); + o.role= "option"; o.onclick = function() { if (t.settings.onselect(o.value) !== false) t.select(o.value); // Must be runned after @@ -316,40 +363,39 @@ var t = this, cp = t.classPrefix; Event.add(t.id, 'click', t.showMenu, t); - Event.add(t.id + '_text', 'focus', function() { + Event.add(t.id, 'keydown', function(evt) { + if (evt.keyCode == 32) { // Space + t.showMenu(evt); + Event.cancel(evt); + } + }); + Event.add(t.id, 'focus', function() { if (!t._focused) { - t.keyDownHandler = Event.add(t.id + '_text', 'keydown', function(e) { - var idx = -1, v, kc = e.keyCode; - - // Find current index - each(t.items, function(v, i) { - if (t.selectedValue == v.value) - idx = i; - }); - - // Move up/down - if (kc == 38) - v = t.items[idx - 1]; - else if (kc == 40) - v = t.items[idx + 1]; - else if (kc == 13) { + t.keyDownHandler = Event.add(t.id, 'keydown', function(e) { + if (e.keyCode == 40) { + t.showMenu(); + Event.cancel(e); + } + }); + t.keyPressHandler = Event.add(t.id, 'keypress', function(e) { + var v; + if (e.keyCode == 13) { // Fake select on enter v = t.selectedValue; t.selectedValue = null; // Needs to be null to fake change + Event.cancel(e); t.settings.onselect(v); - return Event.cancel(e); - } - - if (v) { - t.hideMenu(); - t.select(v.value); } }); } t._focused = 1; }); - Event.add(t.id + '_text', 'blur', function() {Event.remove(t.id + '_text', 'keydown', t.keyDownHandler); t._focused = 0;}); + Event.add(t.id, 'blur', function() { + Event.remove(t.id, 'keydown', t.keyDownHandler); + Event.remove(t.id, 'keypress', t.keyPressHandler); + t._focused = 0; + }); // Old IE doesn't have hover on all elements if (tinymce.isIE6 || !DOM.boxModel) { @@ -379,4 +425,4 @@ Event.clear(this.id + '_open'); } }); -})(tinymce); \ No newline at end of file +})(tinymce); diff --git a/js/tiny_mce/classes/ui/MenuButton.js b/js/tiny_mce/classes/ui/MenuButton.js index 6633bd4a38..48547244d3 100644 --- a/js/tiny_mce/classes/ui/MenuButton.js +++ b/js/tiny_mce/classes/ui/MenuButton.js @@ -17,6 +17,36 @@ * * @class tinymce.ui.MenuButton * @extends tinymce.ui.Control + * @example + * // Creates a new plugin class and a custom menu button + * tinymce.create('tinymce.plugins.ExamplePlugin', { + * createControl: function(n, cm) { + * switch (n) { + * case 'mymenubutton': + * var c = cm.createSplitButton('mysplitbutton', { + * title : 'My menu button', + * image : 'some.gif' + * }); + * + * c.onRenderMenu.add(function(c, m) { + * m.add({title : 'Some title', 'class' : 'mceMenuItemTitle'}).setDisabled(1); + * + * m.add({title : 'Some item 1', onclick : function() { + * alert('Some item 1 was clicked.'); + * }}); + * + * m.add({title : 'Some item 2', onclick : function() { + * alert('Some item 2 was clicked.'); + * }}); + * }); + * + * // Return the new menubutton instance + * return c; + * } + * + * return null; + * } + * }); */ tinymce.create('tinymce.ui.MenuButton:tinymce.ui.Button', { /** @@ -26,9 +56,10 @@ * @method MenuButton * @param {String} id Control id for the split button. * @param {Object} s Optional name/value settings object. + * @param {Editor} ed Optional the editor instance this button is for. */ - MenuButton : function(id, s) { - this.parent(id, s); + MenuButton : function(id, s, ed) { + this.parent(id, s, ed); /** * Fires when the menu is rendered. @@ -90,7 +121,10 @@ icons : t.settings.icons }); - m.onHideMenu.add(t.hideMenu, t); + m.onHideMenu.add(function() { + t.hideMenu(); + t.focus(); + }); t.onRenderMenu.dispatch(t, m); t.menu = m; diff --git a/js/tiny_mce/classes/ui/MenuItem.js b/js/tiny_mce/classes/ui/MenuItem.js index af76922ead..0273a92cab 100644 --- a/js/tiny_mce/classes/ui/MenuItem.js +++ b/js/tiny_mce/classes/ui/MenuItem.js @@ -41,6 +41,7 @@ */ setSelected : function(s) { this.setState('Selected', s); + this.setAriaProperty('checked', !!s); this.selected = s; }, diff --git a/js/tiny_mce/classes/ui/NativeListBox.js b/js/tiny_mce/classes/ui/NativeListBox.js index 1689e05f38..1cbb6735c9 100644 --- a/js/tiny_mce/classes/ui/NativeListBox.js +++ b/js/tiny_mce/classes/ui/NativeListBox.js @@ -41,6 +41,7 @@ */ setDisabled : function(s) { DOM.get(this.id).disabled = s; + this.setAriaProperty('disabled', s); }, /** @@ -156,8 +157,8 @@ h += DOM.createHTML('option', {value : it.value}, it.title); }); - h = DOM.createHTML('select', {id : t.id, 'class' : 'mceNativeListBox'}, h); - + h = DOM.createHTML('select', {id : t.id, 'class' : 'mceNativeListBox', 'aria-labelledby': t.id + '_aria'}, h); + h += DOM.createHTML('span', {id : t.id + '_aria', 'style': 'display: none'}, t.settings.title); return h; }, @@ -168,7 +169,7 @@ * @method postRender */ postRender : function() { - var t = this, ch; + var t = this, ch, changeListenerAdded = true; t.rendered = true; @@ -190,12 +191,20 @@ var bf; Event.remove(t.id, 'change', ch); + changeListenerAdded = false; bf = Event.add(t.id, 'blur', function() { + if (changeListenerAdded) return; + changeListenerAdded = true; Event.add(t.id, 'change', onChange); Event.remove(t.id, 'blur', bf); }); + //prevent default left and right keys on chrome - so that the keyboard navigation is used. + if (tinymce.isWebKit && (e.keyCode==37 ||e.keyCode==39)) { + return Event.prevent(e); + } + if (e.keyCode == 13 || e.keyCode == 32) { onChange(e); return Event.cancel(e); @@ -205,4 +214,4 @@ t.onPostRender.dispatch(t, DOM.get(t.id)); } }); -})(tinymce); \ No newline at end of file +})(tinymce); diff --git a/js/tiny_mce/classes/ui/Separator.js b/js/tiny_mce/classes/ui/Separator.js index 8ecd690391..9e6daf7932 100644 --- a/js/tiny_mce/classes/ui/Separator.js +++ b/js/tiny_mce/classes/ui/Separator.js @@ -26,6 +26,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { Separator : function(id, s) { this.parent(id, s); this.classPrefix = 'mceSeparator'; + this.setDisabled(true); }, /** @@ -36,6 +37,6 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { * @return {String} HTML for the separator control element. */ renderHTML : function() { - return tinymce.DOM.createHTML('span', {'class' : this.classPrefix}); + return tinymce.DOM.createHTML('span', {'class' : this.classPrefix, role : 'separator', 'aria-orientation' : 'vertical', tabindex : '-1'}); } }); diff --git a/js/tiny_mce/classes/ui/SplitButton.js b/js/tiny_mce/classes/ui/SplitButton.js index 945f5449f6..7242361987 100644 --- a/js/tiny_mce/classes/ui/SplitButton.js +++ b/js/tiny_mce/classes/ui/SplitButton.js @@ -16,6 +16,39 @@ * * @class tinymce.ui.SplitButton * @extends tinymce.ui.Button + * @example + * // Creates a new plugin class and a custom split button + * tinymce.create('tinymce.plugins.ExamplePlugin', { + * createControl: function(n, cm) { + * switch (n) { + * case 'mysplitbutton': + * var c = cm.createSplitButton('mysplitbutton', { + * title : 'My split button', + * image : 'some.gif', + * onclick : function() { + * alert('Button was clicked.'); + * } + * }); + * + * c.onRenderMenu.add(function(c, m) { + * m.add({title : 'Some title', 'class' : 'mceMenuItemTitle'}).setDisabled(1); + * + * m.add({title : 'Some item 1', onclick : function() { + * alert('Some item 1 was clicked.'); + * }}); + * + * m.add({title : 'Some item 2', onclick : function() { + * alert('Some item 2 was clicked.'); + * }}); + * }); + * + * // Return the new splitbutton instance + * return c; + * } + * + * return null; + * } + * }); */ tinymce.create('tinymce.ui.SplitButton:tinymce.ui.MenuButton', { /** @@ -25,9 +58,10 @@ * @method SplitButton * @param {String} id Control id for the split button. * @param {Object} s Optional name/value settings object. + * @param {Editor} ed Optional the editor instance this button is for. */ - SplitButton : function(id, s) { - this.parent(id, s); + SplitButton : function(id, s, ed) { + this.parent(id, s, ed); this.classPrefix = 'mceSplitButton'; }, @@ -44,18 +78,19 @@ h = ''; if (s.image) - h1 = DOM.createHTML('img ', {src : s.image, 'class' : 'mceAction ' + s['class']}); + h1 = DOM.createHTML('img ', {src : s.image, role: 'presentation', 'class' : 'mceAction ' + s['class']}); else h1 = DOM.createHTML('span', {'class' : 'mceAction ' + s['class']}, ''); - h += '' + DOM.createHTML('a', {id : t.id + '_action', href : 'javascript:;', 'class' : 'mceAction ' + s['class'], onclick : "return false;", onmousedown : 'return false;', title : s.title}, h1) + ''; + h1 += DOM.createHTML('span', {'class': 'mceVoiceLabel mceIconOnly', id: t.id + '_voice', style: 'display:none;'}, s.title); + h += '' + DOM.createHTML('a', {role: 'button', id : t.id + '_action', tabindex: '-1', href : 'javascript:;', 'class' : 'mceAction ' + s['class'], onclick : "return false;", onmousedown : 'return false;', title : s.title}, h1) + ''; - h1 = DOM.createHTML('span', {'class' : 'mceOpen ' + s['class']}); - h += '' + DOM.createHTML('a', {id : t.id + '_open', href : 'javascript:;', 'class' : 'mceOpen ' + s['class'], onclick : "return false;", onmousedown : 'return false;', title : s.title}, h1) + ''; + h1 = DOM.createHTML('span', {'class' : 'mceOpen ' + s['class']}, ''); + h += '' + DOM.createHTML('a', {role: 'button', id : t.id + '_open', tabindex: '-1', href : 'javascript:;', 'class' : 'mceOpen ' + s['class'], onclick : "return false;", onmousedown : 'return false;', title : s.title}, h1) + ''; h += ''; - - return DOM.createHTML('table', {id : t.id, 'class' : 'mceSplitButton mceSplitButtonEnabled ' + s['class'], cellpadding : '0', cellspacing : '0', onmousedown : 'return false;', title : s.title}, h); + h = DOM.createHTML('table', { role: 'presentation', 'class' : 'mceSplitButton mceSplitButtonEnabled ' + s['class'], cellpadding : '0', cellspacing : '0', title : s.title}, h); + return DOM.createHTML('div', {id : t.id, role: 'button', tabindex: '0', 'aria-labelledby': t.id + '_voice', 'aria-haspopup': 'true'}, h); }, /** @@ -65,18 +100,34 @@ * @method postRender */ postRender : function() { - var t = this, s = t.settings; + var t = this, s = t.settings, activate; if (s.onclick) { - Event.add(t.id + '_action', 'click', function() { - if (!t.isDisabled()) + activate = function(evt) { + if (!t.isDisabled()) { s.onclick(t.value); + Event.cancel(evt); + } + }; + Event.add(t.id + '_action', 'click', activate); + Event.add(t.id, ['click', 'keydown'], function(evt) { + var DOM_VK_SPACE = 32, DOM_VK_ENTER = 14, DOM_VK_RETURN = 13, DOM_VK_UP = 38, DOM_VK_DOWN = 40; + if ((evt.keyCode === 32 || evt.keyCode === 13 || evt.keyCode === 14) && !evt.altKey && !evt.ctrlKey && !evt.metaKey) { + activate(); + Event.cancel(evt); + } else if (evt.type === 'click' || evt.keyCode === DOM_VK_DOWN) { + t.showMenu(); + Event.cancel(evt); + } }); } - Event.add(t.id + '_open', 'click', t.showMenu, t); - Event.add(t.id + '_open', 'focus', function() {t._focused = 1;}); - Event.add(t.id + '_open', 'blur', function() {t._focused = 0;}); + Event.add(t.id + '_open', 'click', function (evt) { + t.showMenu(); + Event.cancel(evt); + }); + Event.add([t.id, t.id + '_open'], 'focus', function() {t._focused = 1;}); + Event.add([t.id, t.id + '_open'], 'blur', function() {t._focused = 0;}); // Old IE doesn't have hover on all elements if (tinymce.isIE6 || !DOM.boxModel) { @@ -97,6 +148,7 @@ Event.clear(this.id + '_action'); Event.clear(this.id + '_open'); + Event.clear(this.id); } }); })(tinymce); diff --git a/js/tiny_mce/classes/ui/Toolbar.js b/js/tiny_mce/classes/ui/Toolbar.js index 4c6142edf8..10b780ab84 100644 --- a/js/tiny_mce/classes/ui/Toolbar.js +++ b/js/tiny_mce/classes/ui/Toolbar.js @@ -8,6 +8,9 @@ * Contributing: http://tinymce.moxiecode.com/contributing */ +(function(tinymce) { +// Shorten class names +var dom = tinymce.DOM, each = tinymce.each; /** * This class is used to create toolbars a toolbar is a container for other controls like buttons etc. * @@ -23,7 +26,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { * @return {String} HTML for the toolbar control. */ renderHTML : function() { - var t = this, h = '', c, co, dom = tinymce.DOM, s = t.settings, i, pr, nx, cl; + var t = this, h = '', c, co, s = t.settings, i, pr, nx, cl; cl = t.controls; for (i=0; i')); - return dom.createHTML('table', {id : t.id, 'class' : 'mceToolbar' + (s['class'] ? ' ' + s['class'] : ''), cellpadding : '0', cellspacing : '0', align : t.settings.align || ''}, '' + h + ''); + return dom.createHTML('table', {id : t.id, 'class' : 'mceToolbar' + (s['class'] ? ' ' + s['class'] : ''), cellpadding : '0', cellspacing : '0', align : t.settings.align || '', role: 'presentation', tabindex: '-1'}, '' + h + ''); } }); +})(tinymce); diff --git a/js/tiny_mce/classes/ui/ToolbarGroup.js b/js/tiny_mce/classes/ui/ToolbarGroup.js new file mode 100644 index 0000000000..c96dbfa5f9 --- /dev/null +++ b/js/tiny_mce/classes/ui/ToolbarGroup.js @@ -0,0 +1,81 @@ +/** + * ToolbarGroup.js + * + * Copyright 2010, Moxiecode Systems AB + * Released under LGPL License. + * + * License: http://tinymce.moxiecode.com/license + * Contributing: http://tinymce.moxiecode.com/contributing + */ + +(function(tinymce) { +// Shorten class names +var dom = tinymce.DOM, each = tinymce.each, Event = tinymce.dom.Event; +/** + * This class is used to group a set of toolbars together and control the keyboard navigation and focus. + * + * @class tinymce.ui.ToolbarGroup + * @extends tinymce.ui.Container + */ +tinymce.create('tinymce.ui.ToolbarGroup:tinymce.ui.Container', { + /** + * Renders the toolbar group as a HTML string. + * + * @method renderHTML + * @return {String} HTML for the toolbar control. + */ + renderHTML : function() { + var t = this, h = [], controls = t.controls, each = tinymce.each, settings = t.settings; + + h.push('
    '); + //TODO: ACC test this out - adding a role = application for getting the landmarks working well. + h.push(""); + h.push(''); + each(controls, function(toolbar) { + h.push(toolbar.renderHTML()); + }); + h.push(""); + h.push('
    '); + + return h.join(''); + }, + + focus : function() { + var t = this; + dom.get(t.id).focus(); + }, + + postRender : function() { + var t = this, items = []; + + each(t.controls, function(toolbar) { + each (toolbar.controls, function(control) { + if (control.id) { + items.push(control); + } + }); + }); + + t.keyNav = new tinymce.ui.KeyboardNavigation({ + root: t.id, + items: items, + onCancel: function() { + //Move focus if webkit so that navigation back will read the item. + if (tinymce.isWebKit) { + dom.get(t.editor.id+"_ifr").focus(); + } + t.editor.focus(); + }, + excludeFromTabOrder: !t.settings.tab_focus_toolbar + }); + }, + + destroy : function() { + var self = this; + + self.parent(); + self.keyNav.destroy(); + Event.clear(self.id); + } +}); +})(tinymce); diff --git a/js/tiny_mce/classes/util/Cookie.js b/js/tiny_mce/classes/util/Cookie.js index 8209bd70a7..a390554a92 100644 --- a/js/tiny_mce/classes/util/Cookie.js +++ b/js/tiny_mce/classes/util/Cookie.js @@ -16,6 +16,15 @@ * * @class tinymce.util.Cookie * @static + * @example + * // Gets a cookie from the browser + * console.debug(tinymce.util.Cookie.get('mycookie')); + * + * // Gets a hash table cookie from the browser and takes out the x parameter from it + * console.debug(tinymce.util.Cookie.getHash('mycookie').x); + * + * // Sets a hash table cookie to the browser + * tinymce.util.Cookie.setHash({x : '1', y : '2'}); */ tinymce.create('static tinymce.util.Cookie', { /** diff --git a/js/tiny_mce/classes/util/Dispatcher.js b/js/tiny_mce/classes/util/Dispatcher.js index 86588a64d4..e97976db31 100644 --- a/js/tiny_mce/classes/util/Dispatcher.js +++ b/js/tiny_mce/classes/util/Dispatcher.js @@ -13,6 +13,12 @@ * All internal events inside TinyMCE uses this class. * * @class tinymce.util.Dispatcher + * @example + * // Creates a custom event + * this.onSomething = new tinymce.util.Dispatcher(this); + * + * // Dispatch/fire the event + * this.onSomething.dispatch('some string'); */ tinymce.create('tinymce.util.Dispatcher', { scope : null, diff --git a/js/tiny_mce/classes/util/JSON.js b/js/tiny_mce/classes/util/JSON.js index 0d271da868..5c69bc50b5 100644 --- a/js/tiny_mce/classes/util/JSON.js +++ b/js/tiny_mce/classes/util/JSON.js @@ -8,22 +8,11 @@ * Contributing: http://tinymce.moxiecode.com/contributing */ -/** - * JSON parser and serializer class. - * - * @class tinymce.util.JSON - * @static - */ -tinymce.create('static tinymce.util.JSON', { - /** - * Serializes the specified object as a JSON string. - * - * @method serialize - * @param {Object} o Object to serialize as a JSON string. - * @return {string} JSON string serialized from input. - */ - serialize : function(o) { - var i, v, s = tinymce.util.JSON.serialize, t; +(function() { + function serialize(o, quote) { + var i, v, t; + + quote = quote || '"'; if (o == null) return 'null'; @@ -33,7 +22,11 @@ tinymce.create('static tinymce.util.JSON', { if (t == 'string') { v = '\bb\tt\nn\ff\rr\""\'\'\\\\'; - return '"' + o.replace(/([\u0080-\uFFFF\x00-\x1f\"])/g, function(a, b) { + return quote + o.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g, function(a, b) { + // Make sure single quotes never get encoded inside double quotes for JSON compatibility + if (quote === '"' && a === "'") + return a; + i = v.indexOf(b); if (i + 1) @@ -42,42 +35,69 @@ tinymce.create('static tinymce.util.JSON', { a = b.charCodeAt().toString(16); return '\\u' + '0000'.substring(a.length) + a; - }) + '"'; + }) + quote; } if (t == 'object') { if (o.hasOwnProperty && o instanceof Array) { for (i=0, v = '['; i 0 ? ',' : '') + s(o[i]); + v += (i > 0 ? ',' : '') + serialize(o[i], quote); return v + ']'; } v = '{'; - for (i in o) - v += typeof o[i] != 'function' ? (v.length > 1 ? ',"' : '"') + i + '":' + s(o[i]) : ''; + for (i in o) { + if (o.hasOwnProperty(i)) { + v += typeof o[i] != 'function' ? (v.length > 1 ? ',' + quote : quote) + i + quote +':' + serialize(o[i], quote) : ''; + } + } return v + '}'; } return '' + o; - }, + }; /** - * Unserializes/parses the specified JSON string into a object. + * JSON parser and serializer class. * - * @method parse - * @param {string} s JSON String to parse into a JavaScript object. - * @return {Object} Object from input JSON string or undefined if it failed. + * @class tinymce.util.JSON + * @static + * @example + * // JSON parse a string into an object + * var obj = tinymce.util.JSON.parse(somestring); + * + * // JSON serialize a object into an string + * var str = tinymce.util.JSON.serialize(obj); */ - parse : function(s) { - try { - return eval('(' + s + ')'); - } catch (ex) { - // Ignore + tinymce.util.JSON = { + /** + * Serializes the specified object as a JSON string. + * + * @method serialize + * @param {Object} obj Object to serialize as a JSON string. + * @param {String} quote Optional quote string defaults to ". + * @return {string} JSON string serialized from input. + */ + serialize: serialize, + + /** + * Unserializes/parses the specified JSON string into a object. + * + * @method parse + * @param {string} s JSON String to parse into a JavaScript object. + * @return {Object} Object from input JSON string or undefined if it failed. + */ + parse: function(s) { + try { + return eval('(' + s + ')'); + } catch (ex) { + // Ignore + } } - } - /**#@-*/ -}); + /**#@-*/ + }; +})(); diff --git a/js/tiny_mce/classes/util/JSONRequest.js b/js/tiny_mce/classes/util/JSONRequest.js index 7e62b30500..9e502879ce 100644 --- a/js/tiny_mce/classes/util/JSONRequest.js +++ b/js/tiny_mce/classes/util/JSONRequest.js @@ -15,6 +15,28 @@ * This class enables you to use JSON-RPC to call backend methods. * * @class tinymce.util.JSONRequest + * @example + * var json = new tinymce.util.JSONRequest({ + * url : 'somebackend.php' + * }); + * + * // Send RPC call 1 + * json.send({ + * method : 'someMethod1', + * params : ['a', 'b'], + * success : function(result) { + * console.dir(result); + * } + * }); + * + * // Send RPC call 2 + * json.send({ + * method : 'someMethod2', + * params : ['a', 'b'], + * success : function(result) { + * console.dir(result); + * } + * }); */ tinymce.create('tinymce.util.JSONRequest', { /** @@ -57,7 +79,8 @@ }; o.error = function(ty, x) { - ecb.call(o.error_scope || o.scope, ty, x); + if (ecb) + ecb.call(o.error_scope || o.scope, ty, x); }; o.data = JSON.serialize({ diff --git a/js/tiny_mce/classes/util/Quirks.js b/js/tiny_mce/classes/util/Quirks.js new file mode 100644 index 0000000000..1194afd7c4 --- /dev/null +++ b/js/tiny_mce/classes/util/Quirks.js @@ -0,0 +1,229 @@ +(function(tinymce) { + var VK = tinymce.VK, BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE; + + /** + * Fixes a WebKit bug when deleting contents using backspace or delete key. + * WebKit will produce a span element if you delete across two block elements. + * + * Example: + *

    a

    |b

    + * + * Will produce this on backspace: + *

    ab

    + * + * This fixes the backspace to produce: + *

    a|b

    + * + * See bug: https://bugs.webkit.org/show_bug.cgi?id=45784 + * + * This code is a bit of a hack and hopefully it will be fixed soon in WebKit. + */ + function cleanupStylesWhenDeleting(ed) { + var dom = ed.dom, selection = ed.selection; + + ed.onKeyDown.add(function(ed, e) { + var rng, blockElm, node, clonedSpan, isDelete; + + isDelete = e.keyCode == DELETE; + if (isDelete || e.keyCode == BACKSPACE) { + e.preventDefault(); + rng = selection.getRng(); + + // Find root block + blockElm = dom.getParent(rng.startContainer, dom.isBlock); + + // On delete clone the root span of the next block element + if (isDelete) + blockElm = dom.getNext(blockElm, dom.isBlock); + + // Locate root span element and clone it since it would otherwise get merged by the "apple-style-span" on delete/backspace + if (blockElm) { + node = blockElm.firstChild; + + // Ignore empty text nodes + while (node && node.nodeType == 3 && node.nodeValue.length == 0) + node = node.nextSibling; + + if (node && node.nodeName === 'SPAN') { + clonedSpan = node.cloneNode(false); + } + } + + // Do the backspace/delete actiopn + ed.getDoc().execCommand(isDelete ? 'ForwardDelete' : 'Delete', false, null); + + // Find all odd apple-style-spans + blockElm = dom.getParent(rng.startContainer, dom.isBlock); + tinymce.each(dom.select('span.Apple-style-span,font.Apple-style-span', blockElm), function(span) { + var bm = selection.getBookmark(); + + if (clonedSpan) { + dom.replace(clonedSpan.cloneNode(false), span, true); + } else { + dom.remove(span, true); + } + + // Restore the selection + selection.moveToBookmark(bm); + }); + } + }); + }; + + /** + * WebKit and IE doesn't empty the editor if you select all contents and hit backspace or delete. This fix will check if the body is empty + * like a

    or

    and then forcefully remove all contents. + */ + function emptyEditorWhenDeleting(ed) { + ed.onKeyUp.add(function(ed, e) { + var keyCode = e.keyCode; + + if (keyCode == DELETE || keyCode == BACKSPACE) { + if (ed.dom.isEmpty(ed.getBody())) { + ed.setContent('', {format : 'raw'}); + ed.nodeChanged(); + return; + } + } + }); + }; + + /** + * WebKit on MacOS X has a weird issue where it some times fails to properly convert keypresses to input method keystrokes. + * So a fix where we just get the range and set the range back seems to do the trick. + */ + function inputMethodFocus(ed) { + ed.dom.bind(ed.getDoc(), 'focusin', function() { + ed.selection.setRng(ed.selection.getRng()); + }); + }; + + /** + * Backspacing in FireFox/IE from a paragraph into a horizontal rule results in a floating text node because the + * browser just deletes the paragraph - the browser fails to merge the text node with a horizontal rule so it is + * left there. TinyMCE sees a floating text node and wraps it in a paragraph on the key up event (ForceBlocks.js + * addRootBlocks), meaning the action does nothing. With this code, FireFox/IE matche the behaviour of other + * browsers + */ + function removeHrOnBackspace(ed) { + ed.onKeyDown.add(function(ed, e) { + if (e.keyCode === BACKSPACE) { + if (ed.selection.isCollapsed() && ed.selection.getRng(true).startOffset === 0) { + var node = ed.selection.getNode(); + var previousSibling = node.previousSibling; + if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "hr") { + ed.dom.remove(previousSibling); + tinymce.dom.Event.cancel(e); + } + } + } + }) + } + + /** + * Firefox 3.x has an issue where the body element won't get proper focus if you click out + * side it's rectangle. + */ + function focusBody(ed) { + // Fix for a focus bug in FF 3.x where the body element + // wouldn't get proper focus if the user clicked on the HTML element + if (!Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4 + ed.onMouseDown.add(function(ed, e) { + if (e.target.nodeName === "HTML") { + var body = ed.getBody(); + + // Blur the body it's focused but not correctly focused + body.blur(); + + // Refocus the body after a little while + setTimeout(function() { + body.focus(); + }, 0); + } + }); + } + }; + + /** + * WebKit has a bug where it isn't possible to select image, hr or anchor elements + * by clicking on them so we need to fake that. + */ + function selectControlElements(ed) { + ed.onClick.add(function(ed, e) { + e = e.target; + + // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250 + // WebKit can't even do simple things like selecting an image + // Needs tobe the setBaseAndExtend or it will fail to select floated images + if (/^(IMG|HR)$/.test(e.nodeName)) + ed.selection.getSel().setBaseAndExtent(e, 0, e, 1); + + if (e.nodeName == 'A' && ed.dom.hasClass(e, 'mceItemAnchor')) + ed.selection.select(e); + + ed.nodeChanged(); + }); + }; + + /** + * Fire a nodeChanged when the selection is changed on WebKit this fixes selection issues on iOS5. It only fires the nodeChange + * event every 50ms since it would other wise update the UI when you type and it hogs the CPU. + */ + function selectionChangeNodeChanged(ed) { + var lastRng, selectionTimer; + + ed.dom.bind(ed.getDoc(), 'selectionchange', function() { + if (selectionTimer) { + clearTimeout(selectionTimer); + selectionTimer = 0; + } + + selectionTimer = window.setTimeout(function() { + var rng = ed.selection.getRng(); + + // Compare the ranges to see if it was a real change or not + if (!lastRng || !tinymce.dom.RangeUtils.compareRanges(rng, lastRng)) { + ed.nodeChanged(); + lastRng = rng; + } + }, 50); + }); + } + + /** + * Screen readers on IE needs to have the role application set on the body. + */ + function ensureBodyHasRoleApplication(ed) { + document.body.setAttribute("role", "application"); + } + + tinymce.create('tinymce.util.Quirks', { + Quirks: function(ed) { + // WebKit + if (tinymce.isWebKit) { + cleanupStylesWhenDeleting(ed); + emptyEditorWhenDeleting(ed); + inputMethodFocus(ed); + selectControlElements(ed); + + // iOS + if (tinymce.isIDevice) { + selectionChangeNodeChanged(ed); + } + } + + // IE + if (tinymce.isIE) { + removeHrOnBackspace(ed); + emptyEditorWhenDeleting(ed); + ensureBodyHasRoleApplication(ed); + } + + // Gecko + if (tinymce.isGecko) { + removeHrOnBackspace(ed); + focusBody(ed); + } + } + }); +})(tinymce); diff --git a/js/tiny_mce/classes/util/URI.js b/js/tiny_mce/classes/util/URI.js index 7af644832c..e8cb3f9f75 100644 --- a/js/tiny_mce/classes/util/URI.js +++ b/js/tiny_mce/classes/util/URI.js @@ -25,7 +25,7 @@ * @param {Object} s Optional settings object. */ URI : function(u, s) { - var t = this, o, a, b; + var t = this, o, a, b, base_url; // Trim whitespace u = tinymce.trim(u); @@ -33,8 +33,9 @@ // Default settings s = t.settings = s || {}; - // Strange app protocol or local anchor - if (/^(mailto|tel|news|javascript|about|data):/i.test(u) || /^\s*#/.test(u)) { + // Strange app protocol that isn't http/https or local anchor + // For example: mailto,skype,tel etc. + if (/^([\w\-]+):([^\/]{2})/i.test(u) || /^\s*#/.test(u)) { t.source = u; return; } @@ -44,8 +45,10 @@ u = (s.base_uri ? s.base_uri.protocol || 'http' : 'http') + '://mce_host' + u; // Relative path http:// or protocol relative //path - if (!/^\w*:?\/\//.test(u)) - u = (s.base_uri.protocol || 'http') + '://mce_host' + t.toAbsPath(s.base_uri.path, u); + if (!/^[\w-]*:?\/\//.test(u)) { + base_url = s.base_uri ? s.base_uri.path : new tinymce.util.URI(location.href).directory; + u = ((s.base_uri && s.base_uri.protocol) || 'http') + '://mce_host' + t.toAbsPath(base_url, u); + } // Parse URL (Credits goes to Steave, http://blog.stevenlevithan.com/archives/parseuri) u = u.replace(/@@/g, '(mce_at)'); // Zope 3 workaround, they use @@something @@ -106,6 +109,9 @@ * @method toRelative * @param {String} u URI to convert into a relative path/URI. * @return {String} Relative URI from the point specified in the current URI instance. + * @example + * // Converts an absolute URL to an relative URL url will be somedir/somefile.htm + * var url = new tinymce.util.URI('http://www.site.com/dir/').toRelative('http://www.site.com/dir/somedir/somefile.htm'); */ toRelative : function(u) { var t = this, o; @@ -139,6 +145,9 @@ * @param {String} u URI to convert into a relative path/URI. * @param {Boolean} nh No host and protocol prefix. * @return {String} Absolute URI from the point specified in the current URI instance. + * @example + * // Converts an relative URL to an absolute URL url will be http://www.site.com/dir/somedir/somefile.htm + * var url = new tinymce.util.URI('http://www.site.com/dir/').toAbsolute('somedir/somefile.htm'); */ toAbsolute : function(u, nh) { var u = new tinymce.util.URI(u, {base_uri : this}); diff --git a/js/tiny_mce/classes/util/VK.js b/js/tiny_mce/classes/util/VK.js new file mode 100644 index 0000000000..08bc8fc29e --- /dev/null +++ b/js/tiny_mce/classes/util/VK.js @@ -0,0 +1,15 @@ +/** + * This file exposes a set of the common KeyCodes for use. Please grow it as needed. + */ + +(function(tinymce){ + tinymce.VK = { + DELETE: 46, + BACKSPACE: 8, + ENTER: 13, + TAB: 9, + SPACEBAR: 32, + UP: 38, + DOWN: 40 + } +})(tinymce); diff --git a/js/tiny_mce/classes/util/XHR.js b/js/tiny_mce/classes/util/XHR.js index a444c13643..cbbdd38149 100644 --- a/js/tiny_mce/classes/util/XHR.js +++ b/js/tiny_mce/classes/util/XHR.js @@ -12,6 +12,14 @@ * This class enables you to send XMLHTTPRequests cross browser. * @class tinymce.util.XHR * @static + * @example + * // Sends a low level Ajax request + * tinymce.util.XHR.send({ + * url : 'someurl', + * success : function(text) { + * console.debug(text); + * } + * }); */ tinymce.create('static tinymce.util.XHR', { /** diff --git a/js/tiny_mce/jquery.tinymce.js b/js/tiny_mce/jquery.tinymce.js index 4866c2a06b..8e61a3cddb 100644 --- a/js/tiny_mce/jquery.tinymce.js +++ b/js/tiny_mce/jquery.tinymce.js @@ -1 +1 @@ -(function(b){var e,d,a=[],c=window;b.fn.tinymce=function(j){var p=this,g,k,h,m,i,l="",n="";if(!p.length){return p}if(!j){return tinyMCE.get(p[0].id)}function o(){var r=[],q=0;if(f){f();f=null}p.each(function(t,u){var s,w=u.id,v=j.oninit;if(!w){u.id=w=tinymce.DOM.uniqueId()}s=new tinymce.Editor(w,j);r.push(s);if(v){s.onInit.add(function(){var x,y=v;if(++q==r.length){if(tinymce.is(y,"string")){x=(y.indexOf(".")===-1)?null:tinymce.resolve(y.replace(/\.\w+$/,""));y=tinymce.resolve(y)}y.apply(x||tinymce,r)}})}});b.each(r,function(t,s){s.render()})}if(!c.tinymce&&!d&&(g=j.script_url)){d=1;h=g.substring(0,g.lastIndexOf("/"));if(/_(src|dev)\.js/g.test(g)){n="_src"}m=g.lastIndexOf("?");if(m!=-1){l=g.substring(m+1)}c.tinyMCEPreInit=c.tinyMCEPreInit||{base:h,suffix:n,query:l};if(g.indexOf("gzip")!=-1){i=j.language||"en";g=g+(/\?/.test(g)?"&":"?")+"js=true&core=true&suffix="+escape(n)+"&themes="+escape(j.theme)+"&plugins="+escape(j.plugins)+"&languages="+i;if(!c.tinyMCE_GZ){tinyMCE_GZ={start:function(){tinymce.suffix=n;function q(r){tinymce.ScriptLoader.markDone(tinyMCE.baseURI.toAbsolute(r))}q("langs/"+i+".js");q("themes/"+j.theme+"/editor_template"+n+".js");q("themes/"+j.theme+"/langs/"+i+".js");b.each(j.plugins.split(","),function(s,r){if(r){q("plugins/"+r+"/editor_plugin"+n+".js");q("plugins/"+r+"/langs/"+i+".js")}})},end:function(){}}}}b.ajax({type:"GET",url:g,dataType:"script",cache:true,success:function(){tinymce.dom.Event.domLoaded=1;d=2;if(j.script_loaded){j.script_loaded()}o();b.each(a,function(q,r){r()})}})}else{if(d===1){a.push(o)}else{o()}}return p};b.extend(b.expr[":"],{tinymce:function(g){return g.id&&!!tinyMCE.get(g.id)}});function f(){function i(l){if(l==="remove"){this.each(function(n,o){var m=h(o);if(m){m.remove()}})}this.find("span.mceEditor,div.mceEditor").each(function(n,o){var m=tinyMCE.get(o.id.replace(/_parent$/,""));if(m){m.remove()}})}function k(n){var m=this,l;if(n!==e){i.call(m);m.each(function(p,q){var o;if(o=tinyMCE.get(q.id)){o.setContent(n)}})}else{if(m.length>0){if(l=tinyMCE.get(m[0].id)){return l.getContent()}}}}function h(m){var l=null;(m)&&(m.id)&&(c.tinymce)&&(l=tinyMCE.get(m.id));return l}function g(l){return !!((l)&&(l.length)&&(c.tinymce)&&(l.is(":tinymce")))}var j={};b.each(["text","html","val"],function(n,l){var o=j[l]=b.fn[l],m=(l==="text");b.fn[l]=function(s){var p=this;if(!g(p)){return o.apply(p,arguments)}if(s!==e){k.call(p.filter(":tinymce"),s);o.apply(p.not(":tinymce"),arguments);return p}else{var r="";var q=arguments;(m?p:p.eq(0)).each(function(u,v){var t=h(v);r+=t?(m?t.getContent().replace(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g,""):t.getContent()):o.apply(b(v),q)});return r}}});b.each(["append","prepend"],function(n,m){var o=j[m]=b.fn[m],l=(m==="prepend");b.fn[m]=function(q){var p=this;if(!g(p)){return o.apply(p,arguments)}if(q!==e){p.filter(":tinymce").each(function(s,t){var r=h(t);r&&r.setContent(l?q+r.getContent():r.getContent()+q)});o.apply(p.not(":tinymce"),arguments);return p}}});b.each(["remove","replaceWith","replaceAll","empty"],function(m,l){var n=j[l]=b.fn[l];b.fn[l]=function(){i.call(this,l);return n.apply(this,arguments)}});j.attr=b.fn.attr;b.fn.attr=function(n,q,o){var m=this;if((!n)||(n!=="value")||(!g(m))){return j.attr.call(m,n,q,o)}if(q!==e){k.call(m.filter(":tinymce"),q);j.attr.call(m.not(":tinymce"),n,q,o);return m}else{var p=m[0],l=h(p);return l?l.getContent():j.attr.call(b(p),n,q,o)}}}})(jQuery); \ No newline at end of file +(function(b){var e,d,a=[],c=window;b.fn.tinymce=function(j){var p=this,g,k,h,m,i,l="",n="";if(!p.length){return p}if(!j){return tinyMCE.get(p[0].id)}p.css("visibility","hidden");function o(){var r=[],q=0;if(f){f();f=null}p.each(function(t,u){var s,w=u.id,v=j.oninit;if(!w){u.id=w=tinymce.DOM.uniqueId()}s=new tinymce.Editor(w,j);r.push(s);s.onInit.add(function(){var x,y=v;p.css("visibility","");if(v){if(++q==r.length){if(tinymce.is(y,"string")){x=(y.indexOf(".")===-1)?null:tinymce.resolve(y.replace(/\.\w+$/,""));y=tinymce.resolve(y)}y.apply(x||tinymce,r)}}})});b.each(r,function(t,s){s.render()})}if(!c.tinymce&&!d&&(g=j.script_url)){d=1;h=g.substring(0,g.lastIndexOf("/"));if(/_(src|dev)\.js/g.test(g)){n="_src"}m=g.lastIndexOf("?");if(m!=-1){l=g.substring(m+1)}c.tinyMCEPreInit=c.tinyMCEPreInit||{base:h,suffix:n,query:l};if(g.indexOf("gzip")!=-1){i=j.language||"en";g=g+(/\?/.test(g)?"&":"?")+"js=true&core=true&suffix="+escape(n)+"&themes="+escape(j.theme)+"&plugins="+escape(j.plugins)+"&languages="+i;if(!c.tinyMCE_GZ){tinyMCE_GZ={start:function(){tinymce.suffix=n;function q(r){tinymce.ScriptLoader.markDone(tinyMCE.baseURI.toAbsolute(r))}q("langs/"+i+".js");q("themes/"+j.theme+"/editor_template"+n+".js");q("themes/"+j.theme+"/langs/"+i+".js");b.each(j.plugins.split(","),function(s,r){if(r){q("plugins/"+r+"/editor_plugin"+n+".js");q("plugins/"+r+"/langs/"+i+".js")}})},end:function(){}}}}b.ajax({type:"GET",url:g,dataType:"script",cache:true,success:function(){tinymce.dom.Event.domLoaded=1;d=2;if(j.script_loaded){j.script_loaded()}o();b.each(a,function(q,r){r()})}})}else{if(d===1){a.push(o)}else{o()}}return p};b.extend(b.expr[":"],{tinymce:function(g){return g.id&&!!tinyMCE.get(g.id)}});function f(){function i(l){if(l==="remove"){this.each(function(n,o){var m=h(o);if(m){m.remove()}})}this.find("span.mceEditor,div.mceEditor").each(function(n,o){var m=tinyMCE.get(o.id.replace(/_parent$/,""));if(m){m.remove()}})}function k(n){var m=this,l;if(n!==e){i.call(m);m.each(function(p,q){var o;if(o=tinyMCE.get(q.id)){o.setContent(n)}})}else{if(m.length>0){if(l=tinyMCE.get(m[0].id)){return l.getContent()}}}}function h(m){var l=null;(m)&&(m.id)&&(c.tinymce)&&(l=tinyMCE.get(m.id));return l}function g(l){return !!((l)&&(l.length)&&(c.tinymce)&&(l.is(":tinymce")))}var j={};b.each(["text","html","val"],function(n,l){var o=j[l]=b.fn[l],m=(l==="text");b.fn[l]=function(s){var p=this;if(!g(p)){return o.apply(p,arguments)}if(s!==e){k.call(p.filter(":tinymce"),s);o.apply(p.not(":tinymce"),arguments);return p}else{var r="";var q=arguments;(m?p:p.eq(0)).each(function(u,v){var t=h(v);r+=t?(m?t.getContent().replace(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g,""):t.getContent()):o.apply(b(v),q)});return r}}});b.each(["append","prepend"],function(n,m){var o=j[m]=b.fn[m],l=(m==="prepend");b.fn[m]=function(q){var p=this;if(!g(p)){return o.apply(p,arguments)}if(q!==e){p.filter(":tinymce").each(function(s,t){var r=h(t);r&&r.setContent(l?q+r.getContent():r.getContent()+q)});o.apply(p.not(":tinymce"),arguments);return p}}});b.each(["remove","replaceWith","replaceAll","empty"],function(m,l){var n=j[l]=b.fn[l];b.fn[l]=function(){i.call(this,l);return n.apply(this,arguments)}});j.attr=b.fn.attr;b.fn.attr=function(n,q,o){var m=this;if((!n)||(n!=="value")||(!g(m))){return j.attr.call(m,n,q,o)}if(q!==e){k.call(m.filter(":tinymce"),q);j.attr.call(m.not(":tinymce"),n,q,o);return m}else{var p=m[0],l=h(p);return l?l.getContent():j.attr.call(b(p),n,q,o)}}}})(jQuery); \ No newline at end of file diff --git a/js/tiny_mce/langs/en.js b/js/tiny_mce/langs/en.js index ea4a1b0e14..6379b0d958 100644 --- a/js/tiny_mce/langs/en.js +++ b/js/tiny_mce/langs/en.js @@ -1,170 +1 @@ -tinyMCE.addI18n({en:{ -common:{ -edit_confirm:"Do you want to use the WYSIWYG mode for this textarea?", -apply:"Apply", -insert:"Insert", -update:"Update", -cancel:"Cancel", -close:"Close", -browse:"Browse", -class_name:"Class", -not_set:"-- Not set --", -clipboard_msg:"Copy/Cut/Paste is not available in Mozilla and Firefox.\nDo you want more information about this issue?", -clipboard_no_support:"Currently not supported by your browser, use keyboard shortcuts instead.", -popup_blocked:"Sorry, but we have noticed that your popup-blocker has disabled a window that provides application functionality. You will need to disable popup blocking on this site in order to fully utilize this tool.", -invalid_data:"Error: Invalid values entered, these are marked in red.", -more_colors:"More colors" -}, -contextmenu:{ -align:"Alignment", -left:"Left", -center:"Center", -right:"Right", -full:"Full" -}, -insertdatetime:{ -date_fmt:"%Y-%m-%d", -time_fmt:"%H:%M:%S", -insertdate_desc:"Insert date", -inserttime_desc:"Insert time", -months_long:"January,February,March,April,May,June,July,August,September,October,November,December", -months_short:"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec", -day_long:"Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday", -day_short:"Sun,Mon,Tue,Wed,Thu,Fri,Sat,Sun" -}, -print:{ -print_desc:"Print" -}, -preview:{ -preview_desc:"Preview" -}, -directionality:{ -ltr_desc:"Direction left to right", -rtl_desc:"Direction right to left" -}, -layer:{ -insertlayer_desc:"Insert new layer", -forward_desc:"Move forward", -backward_desc:"Move backward", -absolute_desc:"Toggle absolute positioning", -content:"New layer..." -}, -save:{ -save_desc:"Save", -cancel_desc:"Cancel all changes" -}, -nonbreaking:{ -nonbreaking_desc:"Insert non-breaking space character" -}, -iespell:{ -iespell_desc:"Run spell checking", -download:"ieSpell not detected. Do you want to install it now?" -}, -advhr:{ -advhr_desc:"Horizontal rule" -}, -emotions:{ -emotions_desc:"Emotions" -}, -searchreplace:{ -search_desc:"Find", -replace_desc:"Find/Replace" -}, -advimage:{ -image_desc:"Insert/edit image" -}, -advlink:{ -link_desc:"Insert/edit link" -}, -xhtmlxtras:{ -cite_desc:"Citation", -abbr_desc:"Abbreviation", -acronym_desc:"Acronym", -del_desc:"Deletion", -ins_desc:"Insertion", -attribs_desc:"Insert/Edit Attributes" -}, -style:{ -desc:"Edit CSS Style" -}, -paste:{ -paste_text_desc:"Paste as Plain Text", -paste_word_desc:"Paste from Word", -selectall_desc:"Select All", -plaintext_mode_sticky:"Paste is now in plain text mode. Click again to toggle back to regular paste mode. After you paste something you will be returned to regular paste mode.", -plaintext_mode:"Paste is now in plain text mode. Click again to toggle back to regular paste mode." -}, -paste_dlg:{ -text_title:"Use CTRL+V on your keyboard to paste the text into the window.", -text_linebreaks:"Keep linebreaks", -word_title:"Use CTRL+V on your keyboard to paste the text into the window." -}, -table:{ -desc:"Inserts a new table", -row_before_desc:"Insert row before", -row_after_desc:"Insert row after", -delete_row_desc:"Delete row", -col_before_desc:"Insert column before", -col_after_desc:"Insert column after", -delete_col_desc:"Remove column", -split_cells_desc:"Split merged table cells", -merge_cells_desc:"Merge table cells", -row_desc:"Table row properties", -cell_desc:"Table cell properties", -props_desc:"Table properties", -paste_row_before_desc:"Paste table row before", -paste_row_after_desc:"Paste table row after", -cut_row_desc:"Cut table row", -copy_row_desc:"Copy table row", -del:"Delete table", -row:"Row", -col:"Column", -cell:"Cell" -}, -autosave:{ -unload_msg:"The changes you made will be lost if you navigate away from this page.", -restore_content:"Restore auto-saved content.", -warning_message:"If you restore the saved content, you will lose all the content that is currently in the editor.\n\nAre you sure you want to restore the saved content?." -}, -fullscreen:{ -desc:"Toggle fullscreen mode" -}, -media:{ -desc:"Insert / edit embedded media", -edit:"Edit embedded media" -}, -fullpage:{ -desc:"Document properties" -}, -template:{ -desc:"Insert predefined template content" -}, -visualchars:{ -desc:"Visual control characters on/off." -}, -spellchecker:{ -desc:"Toggle spellchecker", -menu:"Spellchecker settings", -ignore_word:"Ignore word", -ignore_words:"Ignore all", -langs:"Languages", -wait:"Please wait...", -sug:"Suggestions", -no_sug:"No suggestions", -no_mpell:"No misspellings found." -}, -pagebreak:{ -desc:"Insert page break." -}, -advlist:{ -types:"Types", -def:"Default", -lower_alpha:"Lower alpha", -lower_greek:"Lower greek", -lower_roman:"Lower roman", -upper_alpha:"Upper alpha", -upper_roman:"Upper roman", -circle:"Circle", -disc:"Disc", -square:"Square" -}}}); \ No newline at end of file +tinyMCE.addI18n({en:{common:{"more_colors":"More Colors...","invalid_data":"Error: Invalid values entered, these are marked in red.","popup_blocked":"Sorry, but we have noticed that your popup-blocker has disabled a window that provides application functionality. You will need to disable popup blocking on this site in order to fully utilize this tool.","clipboard_no_support":"Currently not supported by your browser, use keyboard shortcuts instead.","clipboard_msg":"Copy/Cut/Paste is not available in Mozilla and Firefox.\nDo you want more information about this issue?","not_set":"-- Not Set --","class_name":"Class",browse:"Browse",close:"Close",cancel:"Cancel",update:"Update",insert:"Insert",apply:"Apply","edit_confirm":"Do you want to use the WYSIWYG mode for this textarea?","invalid_data_number":"{#field} must be a number","invalid_data_min":"{#field} must be a number greater than {#min}","invalid_data_size":"{#field} must be a number or percentage",value:"(value)"},contextmenu:{full:"Full",right:"Right",center:"Center",left:"Left",align:"Alignment"},insertdatetime:{"day_short":"Sun,Mon,Tue,Wed,Thu,Fri,Sat,Sun","day_long":"Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday","months_short":"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec","months_long":"January,February,March,April,May,June,July,August,September,October,November,December","inserttime_desc":"Insert Time","insertdate_desc":"Insert Date","time_fmt":"%H:%M:%S","date_fmt":"%Y-%m-%d"},print:{"print_desc":"Print"},preview:{"preview_desc":"Preview"},directionality:{"rtl_desc":"Direction Right to Left","ltr_desc":"Direction Left to Right"},layer:{content:"New layer...","absolute_desc":"Toggle Absolute Positioning","backward_desc":"Move Backward","forward_desc":"Move Forward","insertlayer_desc":"Insert New Layer"},save:{"save_desc":"Save","cancel_desc":"Cancel All Changes"},nonbreaking:{"nonbreaking_desc":"Insert Non-Breaking Space Character"},iespell:{download:"ieSpell not detected. Do you want to install it now?","iespell_desc":"Check Spelling"},advhr:{"delta_height":"","delta_width":"","advhr_desc":"Insert Horizontal Line"},emotions:{"delta_height":"","delta_width":"","emotions_desc":"Emotions"},searchreplace:{"replace_desc":"Find/Replace","delta_width":"","delta_height":"","search_desc":"Find"},advimage:{"delta_width":"","image_desc":"Insert/Edit Image","delta_height":""},advlink:{"delta_height":"","delta_width":"","link_desc":"Insert/Edit Link"},xhtmlxtras:{"attribs_delta_height":"","attribs_delta_width":"","ins_delta_height":"","ins_delta_width":"","del_delta_height":"","del_delta_width":"","acronym_delta_height":"","acronym_delta_width":"","abbr_delta_height":"","abbr_delta_width":"","cite_delta_height":"","cite_delta_width":"","attribs_desc":"Insert/Edit Attributes","ins_desc":"Insertion","del_desc":"Deletion","acronym_desc":"Acronym","abbr_desc":"Abbreviation","cite_desc":"Citation"},style:{"delta_height":"","delta_width":"",desc:"Edit CSS Style"},paste:{"plaintext_mode_stick":"Paste is now in plain text mode. Click again to toggle back to regular paste mode.","plaintext_mode":"Paste is now in plain text mode. Click again to toggle back to regular paste mode. After you paste something you will be returned to regular paste mode.","selectall_desc":"Select All","paste_word_desc":"Paste from Word","paste_text_desc":"Paste as Plain Text"},"paste_dlg":{"word_title":"Use Ctrl+V on your keyboard to paste the text into the window.","text_linebreaks":"Keep Linebreaks","text_title":"Use Ctrl+V on your keyboard to paste the text into the window."},table:{"merge_cells_delta_height":"","merge_cells_delta_width":"","table_delta_height":"","table_delta_width":"","cellprops_delta_height":"","cellprops_delta_width":"","rowprops_delta_height":"","rowprops_delta_width":"",cell:"Cell",col:"Column",row:"Row",del:"Delete Table","copy_row_desc":"Copy Table Row","cut_row_desc":"Cut Table Row","paste_row_after_desc":"Paste Table Row After","paste_row_before_desc":"Paste Table Row Before","props_desc":"Table Properties","cell_desc":"Table Cell Properties","row_desc":"Table Row Properties","merge_cells_desc":"Merge Table Cells","split_cells_desc":"Split Merged Table Cells","delete_col_desc":"Delete Column","col_after_desc":"Insert Column After","col_before_desc":"Insert Column Before","delete_row_desc":"Delete Row","row_after_desc":"Insert Row After","row_before_desc":"Insert Row Before",desc:"Insert/Edit Table"},autosave:{"warning_message":"If you restore the saved content, you will lose all the content that is currently in the editor.\n\nAre you sure you want to restore the saved content?","restore_content":"Restore auto-saved content.","unload_msg":"The changes you made will be lost if you navigate away from this page."},fullscreen:{desc:"Toggle Full Screen Mode"},media:{"delta_height":"","delta_width":"",edit:"Edit Embedded Media",desc:"Insert/Edit Embedded Media"},fullpage:{desc:"Document Properties","delta_width":"","delta_height":""},template:{desc:"Insert Predefined Template Content"},visualchars:{desc:"Show/Hide Visual Control Characters"},spellchecker:{desc:"Toggle Spell Checker",menu:"Spell Checker Settings","ignore_word":"Ignore Word","ignore_words":"Ignore All",langs:"Languages",wait:"Please wait...",sug:"Suggestions","no_sug":"No Suggestions","no_mpell":"No misspellings found.","learn_word":"Learn word"},pagebreak:{desc:"Insert Page Break for Printing"},advlist:{types:"Types",def:"Default","lower_alpha":"Lower Alpha","lower_greek":"Lower Greek","lower_roman":"Lower Roman","upper_alpha":"Upper Alpha","upper_roman":"Upper Roman",circle:"Circle",disc:"Disc",square:"Square"},colors:{"333300":"Dark olive","993300":"Burnt orange","000000":"Black","003300":"Dark green","003366":"Dark azure","000080":"Navy Blue","333399":"Indigo","333333":"Very dark gray","800000":"Maroon",FF6600:"Orange","808000":"Olive","008000":"Green","008080":"Teal","0000FF":"Blue","666699":"Grayish blue","808080":"Gray",FF0000:"Red",FF9900:"Amber","99CC00":"Yellow green","339966":"Sea green","33CCCC":"Turquoise","3366FF":"Royal blue","800080":"Purple","999999":"Medium gray",FF00FF:"Magenta",FFCC00:"Gold",FFFF00:"Yellow","00FF00":"Lime","00FFFF":"Aqua","00CCFF":"Sky blue","993366":"Brown",C0C0C0:"Silver",FF99CC:"Pink",FFCC99:"Peach",FFFF99:"Light yellow",CCFFCC:"Pale green",CCFFFF:"Pale cyan","99CCFF":"Light sky blue",CC99FF:"Plum",FFFFFF:"White"},aria:{"rich_text_area":"Rich Text Area"},wordcount:{words:"Words:"}}}); \ No newline at end of file diff --git a/js/tiny_mce/plugins/advhr/langs/en_dlg.js b/js/tiny_mce/plugins/advhr/langs/en_dlg.js index 873bfd8d38..0c3bf15e6f 100644 --- a/js/tiny_mce/plugins/advhr/langs/en_dlg.js +++ b/js/tiny_mce/plugins/advhr/langs/en_dlg.js @@ -1,5 +1 @@ -tinyMCE.addI18n('en.advhr_dlg',{ -width:"Width", -size:"Height", -noshade:"No shadow" -}); \ No newline at end of file +tinyMCE.addI18n('en.advhr_dlg',{size:"Height",noshade:"No Shadow",width:"Width",normal:"Normal",widthunits:"Units"}); \ No newline at end of file diff --git a/js/tiny_mce/plugins/advhr/rule.htm b/js/tiny_mce/plugins/advhr/rule.htm index fc37b2aecd..843e1f8f0b 100644 --- a/js/tiny_mce/plugins/advhr/rule.htm +++ b/js/tiny_mce/plugins/advhr/rule.htm @@ -8,43 +8,44 @@ - +
    - - - - - - - - - - - - - -
    - - -
    + + + + + + + + + + + + + +
    + + + +
    diff --git a/js/tiny_mce/plugins/advimage/editor_plugin.js b/js/tiny_mce/plugins/advimage/editor_plugin.js index 4c7a9c3a88..d613a61393 100644 --- a/js/tiny_mce/plugins/advimage/editor_plugin.js +++ b/js/tiny_mce/plugins/advimage/editor_plugin.js @@ -1 +1 @@ -(function(){tinymce.create("tinymce.plugins.AdvancedImagePlugin",{init:function(a,b){a.addCommand("mceAdvImage",function(){if(a.dom.getAttrib(a.selection.getNode(),"class").indexOf("mceItem")!=-1){return}a.windowManager.open({file:b+"/image.htm",width:480+parseInt(a.getLang("advimage.delta_width",0)),height:385+parseInt(a.getLang("advimage.delta_height",0)),inline:1},{plugin_url:b})});a.addButton("image",{title:"advimage.image_desc",cmd:"mceAdvImage"})},getInfo:function(){return{longname:"Advanced image",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",infourl:"http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/advimage",version:tinymce.majorVersion+"."+tinymce.minorVersion}}});tinymce.PluginManager.add("advimage",tinymce.plugins.AdvancedImagePlugin)})(); \ No newline at end of file +(function(){tinymce.create("tinymce.plugins.AdvancedImagePlugin",{init:function(a,b){a.addCommand("mceAdvImage",function(){if(a.dom.getAttrib(a.selection.getNode(),"class","").indexOf("mceItem")!=-1){return}a.windowManager.open({file:b+"/image.htm",width:480+parseInt(a.getLang("advimage.delta_width",0)),height:385+parseInt(a.getLang("advimage.delta_height",0)),inline:1},{plugin_url:b})});a.addButton("image",{title:"advimage.image_desc",cmd:"mceAdvImage"})},getInfo:function(){return{longname:"Advanced image",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",infourl:"http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/advimage",version:tinymce.majorVersion+"."+tinymce.minorVersion}}});tinymce.PluginManager.add("advimage",tinymce.plugins.AdvancedImagePlugin)})(); \ No newline at end of file diff --git a/js/tiny_mce/plugins/advimage/editor_plugin_src.js b/js/tiny_mce/plugins/advimage/editor_plugin_src.js index 2625dd2131..d2678cbcf2 100644 --- a/js/tiny_mce/plugins/advimage/editor_plugin_src.js +++ b/js/tiny_mce/plugins/advimage/editor_plugin_src.js @@ -14,7 +14,7 @@ // Register commands ed.addCommand('mceAdvImage', function() { // Internal image object like a flash placeholder - if (ed.dom.getAttrib(ed.selection.getNode(), 'class').indexOf('mceItem') != -1) + if (ed.dom.getAttrib(ed.selection.getNode(), 'class', '').indexOf('mceItem') != -1) return; ed.windowManager.open({ diff --git a/js/tiny_mce/plugins/advimage/image.htm b/js/tiny_mce/plugins/advimage/image.htm index 79cff3f19f..ed16b3d4a9 100644 --- a/js/tiny_mce/plugins/advimage/image.htm +++ b/js/tiny_mce/plugins/advimage/image.htm @@ -10,13 +10,14 @@ - - + + + @@ -25,15 +26,15 @@
    {#advimage_dlg.general} - +
    - @@ -60,7 +61,7 @@
    {#advimage_dlg.tab_appearance} -
    +
    - + - - - -
    - x - px + + x + + px
      + - + @@ -109,7 +108,7 @@ @@ -118,7 +117,7 @@ @@ -129,7 +128,7 @@ - -
    @@ -142,18 +145,18 @@
    {#advimage_dlg.swap_image} - + -
    +
    - @@ -161,12 +164,12 @@ - @@ -178,7 +181,7 @@
    {#advimage_dlg.misc} -
    + +
    - - + + -
      
    + +
    - - + + -
      
    +
    @@ -211,12 +214,12 @@ -
    + +
    - - + + -
      
    @@ -227,6 +230,6 @@ - + diff --git a/js/tiny_mce/plugins/advimage/js/image.js b/js/tiny_mce/plugins/advimage/js/image.js index 3bda86a2d3..546b69c0de 100644 --- a/js/tiny_mce/plugins/advimage/js/image.js +++ b/js/tiny_mce/plugins/advimage/js/image.js @@ -9,13 +9,13 @@ var ImageDialog = { }, init : function(ed) { - var f = document.forms[0], nl = f.elements, ed = tinyMCEPopup.editor, dom = ed.dom, n = ed.selection.getNode(); + var f = document.forms[0], nl = f.elements, ed = tinyMCEPopup.editor, dom = ed.dom, n = ed.selection.getNode(), fl = tinyMCEPopup.getParam('external_image_list', 'tinyMCEImageList'); tinyMCEPopup.resizeToInnerSize(); this.fillClassList('class_list'); - this.fillFileList('src_list', 'tinyMCEImageList'); - this.fillFileList('over_list', 'tinyMCEImageList'); - this.fillFileList('out_list', 'tinyMCEImageList'); + this.fillFileList('src_list', fl); + this.fillFileList('over_list', fl); + this.fillFileList('out_list', fl); TinyMCE_EditableSelects.init(); if (n.nodeName == 'IMG') { @@ -142,7 +142,7 @@ var ImageDialog = { } tinymce.extend(args, { - src : nl.src.value, + src : nl.src.value.replace(/ /g, '%20'), width : nl.width.value, height : nl.height.value, alt : nl.alt.value, @@ -171,12 +171,18 @@ var ImageDialog = { if (el && el.nodeName == 'IMG') { ed.dom.setAttribs(el, args); } else { - ed.execCommand('mceInsertContent', false, '', {skip_undo : 1}); - ed.dom.setAttribs('__mce_tmp', args); - ed.dom.setAttrib('__mce_tmp', 'id', ''); + tinymce.each(args, function(value, name) { + if (value === "") { + delete args[name]; + } + }); + + ed.execCommand('mceInsertContent', false, tinyMCEPopup.editor.dom.createHTML('img', args), {skip_undo : 1}); ed.undoManager.add(); } + tinyMCEPopup.editor.execCommand('mceRepaint'); + tinyMCEPopup.editor.focus(); tinyMCEPopup.close(); }, @@ -285,7 +291,7 @@ var ImageDialog = { fillFileList : function(id, l) { var dom = tinyMCEPopup.dom, lst = dom.get(id), v, cl; - l = window[l]; + l = typeof(l) === 'function' ? l() : window[l]; lst.options.length = 0; if (l && l.length > 0) { @@ -359,7 +365,7 @@ var ImageDialog = { }, updateStyle : function(ty) { - var dom = tinyMCEPopup.dom, st, v, f = document.forms[0], img = dom.create('img', {style : dom.get('style').value}); + var dom = tinyMCEPopup.dom, b, bStyle, bColor, v, isIE = tinymce.isIE, f = document.forms[0], img = dom.create('img', {style : dom.get('style').value}); if (tinyMCEPopup.editor.settings.inline_styles) { // Handle align @@ -378,14 +384,27 @@ var ImageDialog = { // Handle border if (ty == 'border') { + b = img.style.border ? img.style.border.split(' ') : []; + bStyle = dom.getStyle(img, 'border-style'); + bColor = dom.getStyle(img, 'border-color'); + dom.setStyle(img, 'border', ''); v = f.border.value; if (v || v == '0') { if (v == '0') - img.style.border = '0'; - else - img.style.border = v + 'px solid black'; + img.style.border = isIE ? '0' : '0 none none'; + else { + if (b.length == 3 && b[isIE ? 2 : 1]) + bStyle = b[isIE ? 2 : 1]; + else if (!bStyle || bStyle == 'none') + bStyle = 'solid'; + if (b.length == 3 && b[isIE ? 0 : 2]) + bColor = b[isIE ? 0 : 2]; + else if (!bColor || bColor == 'none') + bColor = 'black'; + img.style.border = v + 'px ' + bStyle + ' ' + bColor; + } } } diff --git a/js/tiny_mce/plugins/advimage/langs/en_dlg.js b/js/tiny_mce/plugins/advimage/langs/en_dlg.js index f493d196fa..5f122e2cd3 100644 --- a/js/tiny_mce/plugins/advimage/langs/en_dlg.js +++ b/js/tiny_mce/plugins/advimage/langs/en_dlg.js @@ -1,43 +1 @@ -tinyMCE.addI18n('en.advimage_dlg',{ -tab_general:"General", -tab_appearance:"Appearance", -tab_advanced:"Advanced", -general:"General", -title:"Title", -preview:"Preview", -constrain_proportions:"Constrain proportions", -langdir:"Language direction", -langcode:"Language code", -long_desc:"Long description link", -style:"Style", -classes:"Classes", -ltr:"Left to right", -rtl:"Right to left", -id:"Id", -map:"Image map", -swap_image:"Swap image", -alt_image:"Alternative image", -mouseover:"for mouse over", -mouseout:"for mouse out", -misc:"Miscellaneous", -example_img:"Appearance preview image", -missing_alt:"Are you sure you want to continue without including an Image Description? Without it the image may not be accessible to some users with disabilities, or to those using a text browser, or browsing the Web with images turned off.", -dialog_title:"Insert/edit image", -src:"Image URL", -alt:"Image description", -list:"Image list", -border:"Border", -dimensions:"Dimensions", -vspace:"Vertical space", -hspace:"Horizontal space", -align:"Alignment", -align_baseline:"Baseline", -align_top:"Top", -align_middle:"Middle", -align_bottom:"Bottom", -align_texttop:"Text top", -align_textbottom:"Text bottom", -align_left:"Left", -align_right:"Right", -image_list:"Image list" -}); \ No newline at end of file +tinyMCE.addI18n('en.advimage_dlg',{"image_list":"Image List","align_right":"Right","align_left":"Left","align_textbottom":"Text Bottom","align_texttop":"Text Top","align_bottom":"Bottom","align_middle":"Middle","align_top":"Top","align_baseline":"Baseline",align:"Alignment",hspace:"Horizontal Space",vspace:"Vertical Space",dimensions:"Dimensions",border:"Border",list:"Image List",alt:"Image Description",src:"Image URL","dialog_title":"Insert/Edit Image","missing_alt":"Are you sure you want to continue without including an Image Description? Without it the image may not be accessible to some users with disabilities, or to those using a text browser, or browsing the Web with images turned off.","example_img":"Appearance Preview Image",misc:"Miscellaneous",mouseout:"For Mouse Out",mouseover:"For Mouse Over","alt_image":"Alternative Image","swap_image":"Swap Image",map:"Image Map",id:"ID",rtl:"Right to Left",ltr:"Left to Right",classes:"Classes",style:"Style","long_desc":"Long Description Link",langcode:"Language Code",langdir:"Language Direction","constrain_proportions":"Constrain Proportions",preview:"Preview",title:"Title",general:"General","tab_advanced":"Advanced","tab_appearance":"Appearance","tab_general":"General",width:"Width",height:"Height"}); \ No newline at end of file diff --git a/js/tiny_mce/plugins/advlink/js/advlink.js b/js/tiny_mce/plugins/advlink/js/advlink.js index b78e82f76b..837c937c66 100644 --- a/js/tiny_mce/plugins/advlink/js/advlink.js +++ b/js/tiny_mce/plugins/advlink/js/advlink.js @@ -30,8 +30,6 @@ function init() { document.getElementById('hrefbrowsercontainer').innerHTML = getBrowserHTML('hrefbrowser','href','file','advlink'); document.getElementById('popupurlbrowsercontainer').innerHTML = getBrowserHTML('popupurlbrowser','popupurl','file','advlink'); - document.getElementById('linklisthrefcontainer').innerHTML = getLinkListHTML('linklisthref','href'); - document.getElementById('anchorlistcontainer').innerHTML = getAnchorListHTML('anchorlist','href'); document.getElementById('targetlistcontainer').innerHTML = getTargetListHTML('targetlist','target'); // Link list @@ -41,6 +39,13 @@ function init() { else document.getElementById("linklisthrefcontainer").innerHTML = html; + // Anchor list + html = getAnchorListHTML('anchorlist','href'); + if (html == "") + document.getElementById("anchorlistrow").style.display = 'none'; + else + document.getElementById("anchorlistcontainer").innerHTML = html; + // Resize some elements if (isVisible('hrefbrowser')) document.getElementById('href').style.width = '260px'; @@ -360,20 +365,22 @@ function setAttrib(elm, attrib, value) { } function getAnchorListHTML(id, target) { - var inst = tinyMCEPopup.editor; - var nodes = inst.dom.select('a.mceItemAnchor,img.mceItemAnchor'), name, i; - var html = ""; - - html += ''; + if (html == "") + return ""; + + html = ''; return html; } @@ -389,7 +396,6 @@ function insertAction() { // Remove element if there is no href if (!document.forms[0].href.value) { - tinyMCEPopup.execCommand("mceBeginUndoLevel"); i = inst.selection.getBookmark(); inst.dom.remove(elm, 1); inst.selection.moveToBookmark(i); @@ -398,12 +404,10 @@ function insertAction() { return; } - tinyMCEPopup.execCommand("mceBeginUndoLevel"); - // Create new anchor elements if (elm == null) { inst.getDoc().execCommand("unlink", false, null); - tinyMCEPopup.execCommand("CreateLink", false, "#mce_temp_url#", {skip_undo : 1}); + tinyMCEPopup.execCommand("mceInsertLink", false, "#mce_temp_url#", {skip_undo : 1}); elementArray = tinymce.grep(inst.dom.select("a"), function(n) {return inst.dom.getAttrib(n, 'href') == '#mce_temp_url#';}); for (i=0; i - -
    -
    + + + + -
    + diff --git a/js/tiny_mce/plugins/emotions/img/smiley-foot-in-mouth.gif b/js/tiny_mce/plugins/emotions/img/smiley-foot-in-mouth.gif index 16f68cc1e91a9b8ec6cfa0ba4e0c86f94b177f1a..c7cf1011dad0e7500e29a278b0d395b253871109 100644 GIT binary patch delta 268 zcmV+n0rURY0@eZ$M@dFFIbjk25&-lc0J)nSkq}gWCJF-k?l%Ac03rDV1poja04x9i z000sI5&%F2)8HqHP-&K zLM9f)qQm8C1c`;hGBJ_}h73g_p=1mM^9RXLyb^bpf>AvH6dpB#1`P%p2?GxRIXezw z7zGP+3jz%k3l2poa}fvt8yf%!5pp93KMDvH92^x0V-7i+EDszR8Bewx4-y8WehLpR z33wR8E)NQeD=m2M7#-8v+{*T!alCd#3 z1p^Ir4Fi-428xUrkd&O950`xk9-RXQ4F(zs0}qJ@2n?ee0ving0HzHddj}8`MGLmU z8UhImO%o-q#)2Cf00I^Q4_>?<5El-{nhOg84HOFw7eOBf7YG&%hkqFs2p3Q(*Aoat UP6!j@HP+?<{sHjFeFy;nJK@hu1^@s6 diff --git a/js/tiny_mce/plugins/emotions/img/smiley-laughing.gif b/js/tiny_mce/plugins/emotions/img/smiley-laughing.gif index 1606c119e75678c4031f384e0d50849906e8f533..82c5b182e61d32bd394acae551eff180f1eebd26 100644 GIT binary patch delta 269 zcmV+o0rLLX0@nf%M@dFFIbjk25&-lc0K%p+kq}gWQaug+`&s}103rDV1poja04x9i z000sI5&%F2)ZizIQ)!l?Xh>A$7VskoPwh#@I9<*GEDONFahaqh2L%Pf;kcR*isE3L zI2Im3A3bNpK_?^9QN%xH_GCzT(kPR|yLU7KDX_2pAAJa~A*# z2^9-;8v_d!2?_v3DRU7A5(5Jg2N7~31{ezo9uTY$9)TD+oGcI(4STm0Q4*wpwG|r; zF2ODk2{(g0w~m91nJ)p)P~b0v=TXuJkV0)pK%`hRH&5(aq`SQQ0|4*~)Y zj~;||h6NmskCzV%0uYjV3kVjWqM-;FoP`G(WCRQW7Z(8x1b=r16AlX=0u2`oB@7o0 z3LXm%B?eNu8w9Vj0v=tq9}^cH3JDbp8v_d!2?`w-K_3Pe2MZcXM;Z$U7f&hF4hJ0z U3mpdz;x*Id4FABSjR*k%I|lSgp8x;= diff --git a/js/tiny_mce/plugins/emotions/img/smiley-sealed.gif b/js/tiny_mce/plugins/emotions/img/smiley-sealed.gif index b33d3cca1e7b8e62dc689880074d5c61f619520d..fe66220c24b4da4526818a5d68f75a06d9985a29 100644 GIT binary patch delta 249 zcmVc6jM@dFFIbjk25&-lc0Klpjkq}gWQauaR#xDQ>03rDV1poja04x9i z000sI5&%F2z~Co}Q)!l?s2E!2QdA zP6EM+V$0o5egPxNVelL@nuEvS5Py&l$*VJWNgmaeBy#Fc3>i3Y6%ZGOhlmgrZX^W= z8v_Gi8wdrADnAk!5DyO!7!pA_ZyYQc4;~&as7@LN99jl)E**3kv`;r%EDsR|k-xtS zxe5rp69ipb1YdXxI|zjY71PuN7Z3uIlM@dFFIbjk25&+Z_08%{*kq}gWz^WJ3#xDQ>03rDV0RR9W04x9i z000sI5&!@N!r&*0Q)!lCN*2gA3>#8RD(M*Bp=pO_IJA@^(ve^wj|T)J=|mKx31Xqy zL>`%J1fp3WXcLY>lX-YN*)vvS;FJ&#NtObb*E5pGS7DH60e65|cm@i5WOEb=0u&Si zj}nDqhYt}3FPE1Kk%oN{C!Z4r8Io|G1cs;z1QS;X3KIcN6RD}J9RVd52oM(p6~Dj) z7Z3;+U=_R<%FD|T6+s^g3=I$w7#0>7TMZ0QDH9g~2t-Z@0md~SvlqY>7q2NH06S3N BNLv5^ diff --git a/js/tiny_mce/plugins/emotions/img/smiley-smile.gif b/js/tiny_mce/plugins/emotions/img/smiley-smile.gif index e6a9e60d5ddd1243fbbf2197b4dc6cd9c1b58b93..fd27edfaaa29a70a8c4563c0eab9f18c74d374fd 100644 GIT binary patch delta 270 zcmV+p0rCFX0@wl&M@dFFIbjk25&-lc0C`{zkq}gWCJF+oiUI%t03rDV1poja04x9i z000sI5&%F2)!-+JP-&K7%&eE7zha(Mk#X<1{oe683qw@BL@`@2pbbn6B`H)6*`?PtqlrLxC~PZrGN+% zE&vU~#TXL^jVmoMhlVv1IR^#>3>*Rg2{s7;0vrr}2N2W=TMrHb0uEwT<0=*g4h65Q U1r7!lv`7aQ0sjH?nF;{_JD=x5hyVZp delta 271 zcmV+q0r39V0@(r(M@dFFIbjk25&+Z?0453okq}gWd0-BziUI%t03rDV0RR9W04x9i z000sI5&!@N*5D_KP-&K9N=8QJd6^MGDoHFCB1y=#5VX{x;y`RB48~-Ga8wQ>h#?`F zTrSD$M3OMjAdy9t(NHAIO37j-Eo?9tg#s>zZwlT+1~Gwm8h(Hn69^dx2V!s-3=a<+ z0|Oj(k{b($iHwq&l93aYdI$^y9-5LK0}m612L=TU90DB)k_jCG91M5|5EKp@jU52N z03D4R4iqJ;3BC^w0s;=jUAi9-78(f%i;WBz2niY%K_3Se1`Y*FM+FWB7EmeH6b2d& V4jKj&;x*Of0sjHsjr$M+06RN6NSOct diff --git a/js/tiny_mce/plugins/emotions/img/smiley-surprised.gif b/js/tiny_mce/plugins/emotions/img/smiley-surprised.gif index cb99cdd9136fa30462a9f57aa6a0adeb7e4124e7..0cc9bb71cca4cdeafbb248ce7e07c3708c1cbd64 100644 GIT binary patch delta 264 zcmV+j0r&pa0@4ByM@dFFIbjk25&-lc0Q=+`kq}gWC=3F`xI_Q|03rDV1poja04x9i z000sI5&%F2&)_GDP-&KpRzaw7*n2_6C!6#^a!K{=W%7!?bX615c=6B?o`2^cO2 zcoM=c7zsElEfNF+&d-7v$p#C7H3(h^g0}_-5C=(GGudGrRNg8V1`i7u92^)64+a;o OM+X-H`T_N$Apkpuj7YZt delta 268 zcmV+n0rURS0@eZ$M@dFFIbjk25&+Z?04NLskq}gW`{WwLxI_Q|03rDV0RR9W04x9i z000sI5&!@N)8HqHP-&K9N=AmpFq#oTDp_dDHci{26|~f$@kn?Cm5RWVcr*?ph(R$C zG%6BFr6HIo3^YhYGr?3AiY&T{^|8V7w7SrQHo z1RaiZ6q6i?idh5$nwygp7?wp6oeuy28l4>&4~Yf~qLT=v2%Qpp2M`ty9FQFur2rir z1RM_*B?nWxk+Q-ZU$-9+7Yzyt5(*g^X$cAq7eOBf7X}Xt7#tiJ3l9btP$|+D21HH< S7T-0|;sN{t?%*Fn0028#VNAdP diff --git a/js/tiny_mce/plugins/emotions/img/smiley-wink.gif b/js/tiny_mce/plugins/emotions/img/smiley-wink.gif index 9faf1aff8f4b28e02f4f414975fe1859c43b6b54..0631c7616ec8624ddeee02b633326f697ee72f80 100644 GIT binary patch delta 276 zcmV+v0qg$X0^R};M@dFFIbjk25&-lc0MWuJkq}gWC=CMr@j3ti03rDV1poja04x9i z000sI5&%F2+u$dPQfZc>h%k}mS_`xd!)9v?GD=uwG!#n1P{Bx25L@HjmHz$20{ z93>Ts1_E((z8wcwL#Z4|1qR^|i5v)w!ugXVXaI_D!lF>PGz3-%90v;n3^s`y30029B7)0Cv delta 277 zcmV+w0qXwV0^b4/i);if(f&&f[1]){l=f[1].match(/\s*(\w+\s*=\s*".*?"|\w+\s*=\s*'.*?'|\w+\s*=\s*\w+|\w+)\s*/g);if(l){for(c=0,e=l.length;c",a);h.head=f.substring(0,a+1);j=f.indexOf("\n'}h.head+=d.getParam("fullpage_default_doctype",'');h.head+="\n\n\n"+d.getParam("fullpage_default_title","Untitled document")+"\n";if(g=d.getParam("fullpage_default_encoding")){h.head+='\n'}if(g=d.getParam("fullpage_default_font_family")){i+="font-family: "+g+";"}if(g=d.getParam("fullpage_default_font_size")){i+="font-size: "+g+";"}if(g=d.getParam("fullpage_default_text_color")){i+="color: "+g+";"}h.head+="\n\n";h.foot="\n\n"}},_getContent:function(a,c){var b=this;if(!c.source_view||!a.getParam("fullpage_hide_in_source_view")){c.content=tinymce.trim(b.head)+"\n"+tinymce.trim(c.content)+"\n"+tinymce.trim(b.foot)}}});tinymce.PluginManager.add("fullpage",tinymce.plugins.FullPagePlugin)})(); \ No newline at end of file +(function(){var b=tinymce.each,a=tinymce.html.Node;tinymce.create("tinymce.plugins.FullPagePlugin",{init:function(c,d){var e=this;e.editor=c;c.addCommand("mceFullPageProperties",function(){c.windowManager.open({file:d+"/fullpage.htm",width:430+parseInt(c.getLang("fullpage.delta_width",0)),height:495+parseInt(c.getLang("fullpage.delta_height",0)),inline:1},{plugin_url:d,data:e._htmlToData()})});c.addButton("fullpage",{title:"fullpage.desc",cmd:"mceFullPageProperties"});c.onBeforeSetContent.add(e._setContent,e);c.onGetContent.add(e._getContent,e)},getInfo:function(){return{longname:"Fullpage",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",infourl:"http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/fullpage",version:tinymce.majorVersion+"."+tinymce.minorVersion}},_htmlToData:function(){var f=this._parseHeader(),h={},c,i,g,e=this.editor;function d(l,j){var k=l.attr(j);return k||""}h.fontface=e.getParam("fullpage_default_fontface","");h.fontsize=e.getParam("fullpage_default_fontsize","");i=f.firstChild;if(i.type==7){h.xml_pi=true;g=/encoding="([^"]+)"/.exec(i.value);if(g){h.docencoding=g[1]}}i=f.getAll("#doctype")[0];if(i){h.doctype=""}i=f.getAll("title")[0];if(i&&i.firstChild){h.metatitle=i.firstChild.value}b(f.getAll("meta"),function(m){var k=m.attr("name"),j=m.attr("http-equiv"),l;if(k){h["meta"+k.toLowerCase()]=m.attr("content")}else{if(j=="Content-Type"){l=/charset\s*=\s*(.*)\s*/gi.exec(m.attr("content"));if(l){h.docencoding=l[1]}}}});i=f.getAll("html")[0];if(i){h.langcode=d(i,"lang")||d(i,"xml:lang")}i=f.getAll("link")[0];if(i&&i.attr("rel")=="stylesheet"){h.stylesheet=i.attr("href")}i=f.getAll("body")[0];if(i){h.langdir=d(i,"dir");h.style=d(i,"style");h.visited_color=d(i,"vlink");h.link_color=d(i,"link");h.active_color=d(i,"alink")}return h},_dataToHtml:function(g){var f,d,h,j,k,e=this.editor.dom;function c(n,l,m){n.attr(l,m?m:undefined)}function i(l){if(d.firstChild){d.insert(l,d.firstChild)}else{d.append(l)}}f=this._parseHeader();d=f.getAll("head")[0];if(!d){j=f.getAll("html")[0];d=new a("head",1);if(j.firstChild){j.insert(d,j.firstChild,true)}else{j.append(d)}}j=f.firstChild;if(g.xml_pi){k='version="1.0"';if(g.docencoding){k+=' encoding="'+g.docencoding+'"'}if(j.type!=7){j=new a("xml",7);f.insert(j,f.firstChild,true)}j.value=k}else{if(j&&j.type==7){j.remove()}}j=f.getAll("#doctype")[0];if(g.doctype){if(!j){j=new a("#doctype",10);if(g.xml_pi){f.insert(j,f.firstChild)}else{i(j)}}j.value=g.doctype.substring(9,g.doctype.length-1)}else{if(j){j.remove()}}j=f.getAll("title")[0];if(g.metatitle){if(!j){j=new a("title",1);j.append(new a("#text",3)).value=g.metatitle;i(j)}}if(g.docencoding){j=null;b(f.getAll("meta"),function(l){if(l.attr("http-equiv")=="Content-Type"){j=l}});if(!j){j=new a("meta",1);j.attr("http-equiv","Content-Type");j.shortEnded=true;i(j)}j.attr("content","text/html; charset="+g.docencoding)}b("keywords,description,author,copyright,robots".split(","),function(m){var l=f.getAll("meta"),n,p,o=g["meta"+m];for(n=0;n"))},_parseHeader:function(){return new tinymce.html.DomParser({validate:false,root_name:"#document"}).parse(this.head)},_setContent:function(g,d){var m=this,i,c,h=d.content,f,l="",e=m.editor.dom,j;function k(n){return n.replace(/<\/?[A-Z]+/g,function(o){return o.toLowerCase()})}if(d.format=="raw"&&m.head){return}if(d.source_view&&g.getParam("fullpage_hide_in_source_view")){return}h=h.replace(/<(\/?)BODY/gi,"<$1body");i=h.indexOf("",i);m.head=k(h.substring(0,i+1));c=h.indexOf("\n"}f=m._parseHeader();b(f.getAll("style"),function(n){if(n.firstChild){l+=n.firstChild.value}});j=f.getAll("body")[0];if(j){e.setAttribs(m.editor.getBody(),{style:j.attr("style")||"",dir:j.attr("dir")||"",vLink:j.attr("vlink")||"",link:j.attr("link")||"",aLink:j.attr("alink")||""})}e.remove("fullpage_styles");if(l){e.add(m.editor.getDoc().getElementsByTagName("head")[0],"style",{id:"fullpage_styles"},l);j=e.get("fullpage_styles");if(j.styleSheet){j.styleSheet.cssText=l}}},_getDefaultHeader:function(){var f="",c=this.editor,e,d="";if(c.getParam("fullpage_default_xml_pi")){f+='\n'}f+=c.getParam("fullpage_default_doctype",'');f+="\n\n\n";if(e=c.getParam("fullpage_default_title")){f+=""+e+"\n"}if(e=c.getParam("fullpage_default_encoding")){f+='\n'}if(e=c.getParam("fullpage_default_font_family")){d+="font-family: "+e+";"}if(e=c.getParam("fullpage_default_font_size")){d+="font-size: "+e+";"}if(e=c.getParam("fullpage_default_text_color")){d+="color: "+e+";"}f+="\n\n";return f},_getContent:function(d,e){var c=this;if(!e.source_view||!d.getParam("fullpage_hide_in_source_view")){e.content=tinymce.trim(c.head)+"\n"+tinymce.trim(e.content)+"\n"+tinymce.trim(c.foot)}}});tinymce.PluginManager.add("fullpage",tinymce.plugins.FullPagePlugin)})(); \ No newline at end of file diff --git a/js/tiny_mce/plugins/fullpage/editor_plugin_src.js b/js/tiny_mce/plugins/fullpage/editor_plugin_src.js index a2c9df8987..23de7c5a1a 100644 --- a/js/tiny_mce/plugins/fullpage/editor_plugin_src.js +++ b/js/tiny_mce/plugins/fullpage/editor_plugin_src.js @@ -9,6 +9,8 @@ */ (function() { + var each = tinymce.each, Node = tinymce.html.Node; + tinymce.create('tinymce.plugins.FullPagePlugin', { init : function(ed, url) { var t = this; @@ -24,7 +26,7 @@ inline : 1 }, { plugin_url : url, - head_html : t.head + data : t._htmlToData() }); }); @@ -32,7 +34,6 @@ ed.addButton('fullpage', {title : 'fullpage.desc', cmd : 'mceFullPageProperties'}); ed.onBeforeSetContent.add(t._setContent, t); - ed.onSetContent.add(t._setBodyAttribs, t); ed.onGetContent.add(t._getContent, t); }, @@ -48,106 +49,357 @@ // Private plugin internal methods - _setBodyAttribs : function(ed, o) { - var bdattr, i, len, kv, k, v, t, attr = this.head.match(/body(.*?)>/i); + _htmlToData : function() { + var headerFragment = this._parseHeader(), data = {}, nodes, elm, matches, editor = this.editor; - if (attr && attr[1]) { - bdattr = attr[1].match(/\s*(\w+\s*=\s*".*?"|\w+\s*=\s*'.*?'|\w+\s*=\s*\w+|\w+)\s*/g); + function getAttr(elm, name) { + var value = elm.attr(name); - if (bdattr) { - for(i = 0, len = bdattr.length; i < len; i++) { - kv = bdattr[i].split('='); - k = kv[0].replace(/\s/,''); - v = kv[1]; + return value || ''; + }; - if (v) { - v = v.replace(/^\s+/,'').replace(/\s+$/,''); - t = v.match(/^["'](.*)["']$/); + // Default some values + data.fontface = editor.getParam("fullpage_default_fontface", ""); + data.fontsize = editor.getParam("fullpage_default_fontsize", ""); + + // Parse XML PI + elm = headerFragment.firstChild; + if (elm.type == 7) { + data.xml_pi = true; + matches = /encoding="([^"]+)"/.exec(elm.value); + if (matches) + data.docencoding = matches[1]; + } - if (t) - v = t[1]; - } else - v = k; + // Parse doctype + elm = headerFragment.getAll('#doctype')[0]; + if (elm) + data.doctype = '"; - ed.dom.setAttrib(ed.getBody(), 'style', v); - } + // Parse title element + elm = headerFragment.getAll('title')[0]; + if (elm && elm.firstChild) { + data.metatitle = elm.firstChild.value; + } + + // Parse meta elements + each(headerFragment.getAll('meta'), function(meta) { + var name = meta.attr('name'), httpEquiv = meta.attr('http-equiv'), matches; + + if (name) + data['meta' + name.toLowerCase()] = meta.attr('content'); + else if (httpEquiv == "Content-Type") { + matches = /charset\s*=\s*(.*)\s*/gi.exec(meta.attr('content')); + + if (matches) + data.docencoding = matches[1]; } + }); + + // Parse html attribs + elm = headerFragment.getAll('html')[0]; + if (elm) + data.langcode = getAttr(elm, 'lang') || getAttr(elm, 'xml:lang'); + + // Parse stylesheet + elm = headerFragment.getAll('link')[0]; + if (elm && elm.attr('rel') == 'stylesheet') + data.stylesheet = elm.attr('href'); + + // Parse body parts + elm = headerFragment.getAll('body')[0]; + if (elm) { + data.langdir = getAttr(elm, 'dir'); + data.style = getAttr(elm, 'style'); + data.visited_color = getAttr(elm, 'vlink'); + data.link_color = getAttr(elm, 'link'); + data.active_color = getAttr(elm, 'alink'); } + + return data; }, - _createSerializer : function() { - return new tinymce.dom.Serializer({ - dom : this.editor.dom, - apply_source_formatting : true + _dataToHtml : function(data) { + var headerFragment, headElement, html, elm, value, dom = this.editor.dom; + + function setAttr(elm, name, value) { + elm.attr(name, value ? value : undefined); + }; + + function addHeadNode(node) { + if (headElement.firstChild) + headElement.insert(node, headElement.firstChild); + else + headElement.append(node); + }; + + headerFragment = this._parseHeader(); + headElement = headerFragment.getAll('head')[0]; + if (!headElement) { + elm = headerFragment.getAll('html')[0]; + headElement = new Node('head', 1); + + if (elm.firstChild) + elm.insert(headElement, elm.firstChild, true); + else + elm.append(headElement); + } + + // Add/update/remove XML-PI + elm = headerFragment.firstChild; + if (data.xml_pi) { + value = 'version="1.0"'; + + if (data.docencoding) + value += ' encoding="' + data.docencoding + '"'; + + if (elm.type != 7) { + elm = new Node('xml', 7); + headerFragment.insert(elm, headerFragment.firstChild, true); + } + + elm.value = value; + } else if (elm && elm.type == 7) + elm.remove(); + + // Add/update/remove doctype + elm = headerFragment.getAll('#doctype')[0]; + if (data.doctype) { + if (!elm) { + elm = new Node('#doctype', 10); + + if (data.xml_pi) + headerFragment.insert(elm, headerFragment.firstChild); + else + addHeadNode(elm); + } + + elm.value = data.doctype.substring(9, data.doctype.length - 1); + } else if (elm) + elm.remove(); + + // Add/update/remove title + elm = headerFragment.getAll('title')[0]; + if (data.metatitle) { + if (!elm) { + elm = new Node('title', 1); + elm.append(new Node('#text', 3)).value = data.metatitle; + addHeadNode(elm); + } + } + + // Add meta encoding + if (data.docencoding) { + elm = null; + each(headerFragment.getAll('meta'), function(meta) { + if (meta.attr('http-equiv') == 'Content-Type') + elm = meta; + }); + + if (!elm) { + elm = new Node('meta', 1); + elm.attr('http-equiv', 'Content-Type'); + elm.shortEnded = true; + addHeadNode(elm); + } + + elm.attr('content', 'text/html; charset=' + data.docencoding); + } + + // Add/update/remove meta + each('keywords,description,author,copyright,robots'.split(','), function(name) { + var nodes = headerFragment.getAll('meta'), i, meta, value = data['meta' + name]; + + for (i = 0; i < nodes.length; i++) { + meta = nodes[i]; + + if (meta.attr('name') == name) { + if (value) + meta.attr('content', value); + else + meta.remove(); + + return; + } + } + + if (value) { + elm = new Node('meta', 1); + elm.attr('name', name); + elm.attr('content', value); + elm.shortEnded = true; + + addHeadNode(elm); + } }); + + // Add/update/delete link + elm = headerFragment.getAll('link')[0]; + if (elm && elm.attr('rel') == 'stylesheet') { + if (data.stylesheet) + elm.attr('href', data.stylesheet); + else + elm.remove(); + } else if (data.stylesheet) { + elm = new Node('link', 1); + elm.attr({ + rel : 'stylesheet', + text : 'text/css', + href : data.stylesheet + }); + elm.shortEnded = true; + + addHeadNode(elm); + } + + // Update body attributes + elm = headerFragment.getAll('body')[0]; + if (elm) { + setAttr(elm, 'dir', data.langdir); + setAttr(elm, 'style', data.style); + setAttr(elm, 'vlink', data.visited_color); + setAttr(elm, 'link', data.link_color); + setAttr(elm, 'alink', data.active_color); + + // Update iframe body as well + dom.setAttribs(this.editor.getBody(), { + style : data.style, + dir : data.dir, + vLink : data.visited_color, + link : data.link_color, + aLink : data.active_color + }); + } + + // Set html attributes + elm = headerFragment.getAll('html')[0]; + if (elm) { + setAttr(elm, 'lang', data.langcode); + setAttr(elm, 'xml:lang', data.langcode); + } + + // Serialize header fragment and crop away body part + html = new tinymce.html.Serializer({ + validate: false, + indent: true, + apply_source_formatting : true, + indent_before: 'head,html,body,meta,title,script,link,style', + indent_after: 'head,html,body,meta,title,script,link,style' + }).serialize(headerFragment); + + this.head = html.substring(0, html.indexOf('')); + }, + + _parseHeader : function() { + // Parse the contents with a DOM parser + return new tinymce.html.DomParser({ + validate: false, + root_name: '#document' + }).parse(this.head); }, _setContent : function(ed, o) { - var t = this, sp, ep, c = o.content, v, st = ''; + var self = this, startPos, endPos, content = o.content, headerFragment, styles = '', dom = self.editor.dom, elm; + + function low(s) { + return s.replace(/<\/?[A-Z]+/g, function(a) { + return a.toLowerCase(); + }) + }; // Ignore raw updated if we already have a head, this will fix issues with undo/redo keeping the head/foot separate - if (o.format == 'raw' && t.head) + if (o.format == 'raw' && self.head) return; if (o.source_view && ed.getParam('fullpage_hide_in_source_view')) return; // Parse out head, body and footer - c = c.replace(/<(\/?)BODY/gi, '<$1body'); - sp = c.indexOf('', sp); - t.head = c.substring(0, sp + 1); + if (startPos != -1) { + startPos = content.indexOf('>', startPos); + self.head = low(content.substring(0, startPos + 1)); - ep = c.indexOf('\n'; + elm = headerFragment.getAll('body')[0]; + if (elm) { + dom.setAttribs(self.editor.getBody(), { + style : elm.attr('style') || '', + dir : elm.attr('dir') || '', + vLink : elm.attr('vlink') || '', + link : elm.attr('link') || '', + aLink : elm.attr('alink') || '' + }); + } - t.head += ed.getParam('fullpage_default_doctype', ''); - t.head += '\n\n\n' + ed.getParam('fullpage_default_title', 'Untitled document') + '\n'; + dom.remove('fullpage_styles'); - if (v = ed.getParam('fullpage_default_encoding')) - t.head += '\n'; + if (styles) { + dom.add(self.editor.getDoc().getElementsByTagName('head')[0], 'style', {id : 'fullpage_styles'}, styles); - if (v = ed.getParam('fullpage_default_font_family')) - st += 'font-family: ' + v + ';'; + // Needed for IE 6/7 + elm = dom.get('fullpage_styles'); + if (elm.styleSheet) + elm.styleSheet.cssText = styles; + } + }, - if (v = ed.getParam('fullpage_default_font_size')) - st += 'font-size: ' + v + ';'; + _getDefaultHeader : function() { + var header = '', editor = this.editor, value, styles = ''; - if (v = ed.getParam('fullpage_default_text_color')) - st += 'color: ' + v + ';'; + if (editor.getParam('fullpage_default_xml_pi')) + header += '\n'; - t.head += '\n\n'; - t.foot = '\n\n'; - } + header += editor.getParam('fullpage_default_doctype', ''); + header += '\n\n\n'; + + if (value = editor.getParam('fullpage_default_title')) + header += '' + value + '\n'; + + if (value = editor.getParam('fullpage_default_encoding')) + header += '\n'; + + if (value = editor.getParam('fullpage_default_font_family')) + styles += 'font-family: ' + value + ';'; + + if (value = editor.getParam('fullpage_default_font_size')) + styles += 'font-size: ' + value + ';'; + + if (value = editor.getParam('fullpage_default_text_color')) + styles += 'color: ' + value + ';'; + + header += '\n\n'; + + return header; }, _getContent : function(ed, o) { - var t = this; + var self = this; if (!o.source_view || !ed.getParam('fullpage_hide_in_source_view')) - o.content = tinymce.trim(t.head) + '\n' + tinymce.trim(o.content) + '\n' + tinymce.trim(t.foot); + o.content = tinymce.trim(self.head) + '\n' + tinymce.trim(o.content) + '\n' + tinymce.trim(self.foot); } }); // Register plugin tinymce.PluginManager.add('fullpage', tinymce.plugins.FullPagePlugin); -})(); \ No newline at end of file +})(); diff --git a/js/tiny_mce/plugins/fullpage/fullpage.htm b/js/tiny_mce/plugins/fullpage/fullpage.htm index c32afaf2d9..14ab8652ea 100644 --- a/js/tiny_mce/plugins/fullpage/fullpage.htm +++ b/js/tiny_mce/plugins/fullpage/fullpage.htm @@ -8,13 +8,12 @@ - -
    + + @@ -72,9 +71,9 @@
       -
    -
    - - +
     
    @@ -147,7 +146,7 @@
    - +
     
    @@ -158,7 +157,7 @@
    - +
     
    @@ -173,15 +172,15 @@ - + - + - + - +
    @@ -195,7 +194,7 @@
    - +
    @@ -205,7 +204,7 @@
    - +
     
    @@ -217,7 +216,7 @@
    - +
     
    @@ -225,16 +224,6 @@
       
    @@ -254,310 +243,9 @@
    -
    - - -
    - - -
    - {#fullpage_dlg.head_elements} - -
    -
    -
    - - -
    -
    - - -
    -
    -
    - -
    -
    - -
    - {#fullpage_dlg.meta_element} - - - - - - - - - - - - - - -
    - - -
    - -
    - {#fullpage_dlg.title_element} - - - - - - -
    - - -
    - -
    - {#fullpage_dlg.script_element} - - - -
    - -
    -
    - - - - - - - - - - - - - - - - - -
    - - - - -
     
    -
    - -
    - -
    -
    - - -
    - -
    - {#fullpage_dlg.style_element} - - - -
    - -
    -
    - - - - - - - - - -
    -
    - -
    - -
    -
    - - -
    - -
    - {#fullpage_dlg.base_element} - - - - - - - - - +
    - - -
    - - - -
    - {#fullpage_dlg.comment_element} - - - -
    @@ -566,6 +254,6 @@ - + diff --git a/js/tiny_mce/plugins/fullpage/js/fullpage.js b/js/tiny_mce/plugins/fullpage/js/fullpage.js index a1bb719a38..3f672ad3ba 100644 --- a/js/tiny_mce/plugins/fullpage/js/fullpage.js +++ b/js/tiny_mce/plugins/fullpage/js/fullpage.js @@ -8,464 +8,225 @@ * Contributing: http://tinymce.moxiecode.com/contributing */ -tinyMCEPopup.requireLangPack(); - -var doc; - -var defaultDocTypes = - 'XHTML 1.0 Transitional=,' + - 'XHTML 1.0 Frameset=,' + - 'XHTML 1.0 Strict=,' + - 'XHTML 1.1=,' + - 'HTML 4.01 Transitional=,' + - 'HTML 4.01 Strict=,' + - 'HTML 4.01 Frameset='; - -var defaultEncodings = - 'Western european (iso-8859-1)=iso-8859-1,' + - 'Central European (iso-8859-2)=iso-8859-2,' + - 'Unicode (UTF-8)=utf-8,' + - 'Chinese traditional (Big5)=big5,' + - 'Cyrillic (iso-8859-5)=iso-8859-5,' + - 'Japanese (iso-2022-jp)=iso-2022-jp,' + - 'Greek (iso-8859-7)=iso-8859-7,' + - 'Korean (iso-2022-kr)=iso-2022-kr,' + - 'ASCII (us-ascii)=us-ascii'; - -var defaultMediaTypes = - 'all=all,' + - 'screen=screen,' + - 'print=print,' + - 'tty=tty,' + - 'tv=tv,' + - 'projection=projection,' + - 'handheld=handheld,' + - 'braille=braille,' + - 'aural=aural'; - -var defaultFontNames = 'Arial=arial,helvetica,sans-serif;Courier New=courier new,courier,monospace;Georgia=georgia,times new roman,times,serif;Tahoma=tahoma,arial,helvetica,sans-serif;Times New Roman=times new roman,times,serif;Verdana=verdana,arial,helvetica,sans-serif;Impact=impact;WingDings=wingdings'; -var defaultFontSizes = '10px,11px,12px,13px,14px,15px,16px'; - -function init() { - var f = document.forms['fullpage'], el = f.elements, e, i, p, doctypes, encodings, mediaTypes, fonts, ed = tinyMCEPopup.editor, dom = tinyMCEPopup.dom, style; - - // Setup doctype select box - doctypes = ed.getParam("fullpage_doctypes", defaultDocTypes).split(','); - for (i=0; i 1) - addSelectValue(f, 'doctypes', p[0], p[1]); - } - - // Setup fonts select box - fonts = ed.getParam("fullpage_fonts", defaultFontNames).split(';'); - for (i=0; i 1) - addSelectValue(f, 'fontface', p[0], p[1]); - } - - // Setup fontsize select box - fonts = ed.getParam("fullpage_fontsizes", defaultFontSizes).split(','); - for (i=0; i 1) { - addSelectValue(f, 'element_style_media', p[0], p[1]); - addSelectValue(f, 'element_link_media', p[0], p[1]); - } - } - - // Setup encodings select box - encodings = ed.getParam("fullpage_encodings", defaultEncodings).split(','); - for (i=0; i 1) { - addSelectValue(f, 'docencoding', p[0], p[1]); - addSelectValue(f, 'element_script_charset', p[0], p[1]); - addSelectValue(f, 'element_link_charset', p[0], p[1]); - } - } - - document.getElementById('bgcolor_pickcontainer').innerHTML = getColorPickerHTML('bgcolor_pick','bgcolor'); - document.getElementById('link_color_pickcontainer').innerHTML = getColorPickerHTML('link_color_pick','link_color'); - //document.getElementById('hover_color_pickcontainer').innerHTML = getColorPickerHTML('hover_color_pick','hover_color'); - document.getElementById('visited_color_pickcontainer').innerHTML = getColorPickerHTML('visited_color_pick','visited_color'); - document.getElementById('active_color_pickcontainer').innerHTML = getColorPickerHTML('active_color_pick','active_color'); - document.getElementById('textcolor_pickcontainer').innerHTML = getColorPickerHTML('textcolor_pick','textcolor'); - document.getElementById('stylesheet_browsercontainer').innerHTML = getBrowserHTML('stylesheetbrowser','stylesheet','file','fullpage'); - document.getElementById('link_href_pickcontainer').innerHTML = getBrowserHTML('link_href_browser','element_link_href','file','fullpage'); - document.getElementById('script_src_pickcontainer').innerHTML = getBrowserHTML('script_src_browser','element_script_src','file','fullpage'); - document.getElementById('bgimage_pickcontainer').innerHTML = getBrowserHTML('bgimage_browser','bgimage','image','fullpage'); - - // Resize some elements - if (isVisible('stylesheetbrowser')) - document.getElementById('stylesheet').style.width = '220px'; - - if (isVisible('link_href_browser')) - document.getElementById('element_link_href').style.width = '230px'; - - if (isVisible('bgimage_browser')) - document.getElementById('bgimage').style.width = '210px'; - - // Add iframe - dom.add(document.body, 'iframe', {id : 'documentIframe', src : 'javascript:""', style : {display : 'none'}}); - doc = dom.get('documentIframe').contentWindow.document; - h = tinyMCEPopup.getWindowArg('head_html'); - - // Preprocess the HTML disable scripts and urls - h = h.replace(/ '; - } + if (!url) + return url; - return im; - }); - } - }); + if (force_absolute) + return editor.documentBaseURI.toAbsolute(url); + + return urlConverter.call(urlConverterScope, url, 'src', 'object'); }, getInfo : function() { @@ -202,213 +226,665 @@ }; }, - // Private methods - _objectsToSpans : function(ed, o) { - var t = this, h = o.content; + /** + * Converts the JSON data object to an img node. + */ + dataToImg : function(data, force_absolute) { + var self = this, editor = self.editor, baseUri = editor.documentBaseURI, sources, attrs, img, i; - h = h.replace(/]*>\s*write(Flash|ShockWave|WindowsMedia|QuickTime|RealMedia)\(\{([^\)]*)\}\);\s*<\/script>/gi, function(a, b, c) { - var o = t._parse(c); + data.params.src = self.convertUrl(data.params.src, force_absolute); - return '' + attrs = data.video.attrs; + if (attrs) + attrs.src = self.convertUrl(attrs.src, force_absolute); + + if (attrs) + attrs.poster = self.convertUrl(attrs.poster, force_absolute); + + sources = toArray(data.video.sources); + if (sources) { + for (i = 0; i < sources.length; i++) + sources[i].src = self.convertUrl(sources[i].src, force_absolute); + } + + img = self.editor.dom.create('img', { + id : data.id, + style : data.style, + align : data.align, + hspace : data.hspace, + vspace : data.vspace, + src : self.editor.theme.url + '/img/trans.gif', + 'class' : 'mceItemMedia mceItem' + self.getType(data.type).name, + 'data-mce-json' : JSON.serialize(data, "'") }); - h = h.replace(/]*)>/gi, ''); - h = h.replace(/]*)\/?>/gi, ''); - h = h.replace(/]*)>/gi, ''); - h = h.replace(/<\/(object)([^>]*)>/gi, ''); - h = h.replace(/<\/embed>/gi, ''); - h = h.replace(/]*)>/gi, function(a, b) {return ''}); - h = h.replace(/\/ class=\"mceItemParam\"><\/span>/gi, 'class="mceItemParam">'); + img.width = data.width || (data.type == 'audio' ? "300" : "320"); + img.height = data.height || (data.type == 'audio' ? "32" : "240"); + + return img; + }, + + /** + * Converts the JSON data object to a HTML string. + */ + dataToHtml : function(data, force_absolute) { + return this.editor.serializer.serialize(this.dataToImg(data, force_absolute), {forced_root_block : '', force_absolute : force_absolute}); + }, + + /** + * Converts the JSON data object to a HTML string. + */ + htmlToData : function(html) { + var fragment, img, data; + + data = { + type : 'flash', + video: {sources:[]}, + params: {} + }; + + fragment = this.editor.parser.parse(html); + img = fragment.getAll('img')[0]; + + if (img) { + data = JSON.parse(img.attr('data-mce-json')); + data.type = this.getType(img.attr('class')).name.toLowerCase(); + + // Add some extra properties to the data object + tinymce.each(rootAttributes, function(name) { + var value = img.attr(name); - o.content = h; + if (value) + data[name] = value; + }); + } + + return data; + }, + + /** + * Get type item by extension, class, clsid or mime type. + * + * @method getType + * @param {String} value Value to get type item by. + * @return {Object} Type item object or undefined. + */ + getType : function(value) { + var i, values, typeItem; + + // Find type by checking the classes + values = tinymce.explode(value, ' '); + for (i = 0; i < values.length; i++) { + typeItem = this.lookup[values[i]]; + + if (typeItem) + return typeItem; + } }, - _buildObj : function(o, n) { - var ob, ed = this.editor, dom = ed.dom, p = this._parse(n.title), stc; - - stc = ed.getParam('media_strict', true) && o.type == 'application/x-shockwave-flash'; - - p.width = o.width = dom.getAttrib(n, 'width') || 100; - p.height = o.height = dom.getAttrib(n, 'height') || 100; - - if (p.src) - p.src = ed.convertURL(p.src, 'src', n); - - if (stc) { - ob = dom.create('span', { - id : p.id, - _mce_name : 'object', - type : 'application/x-shockwave-flash', - data : p.src, - style : dom.getAttrib(n, 'style'), - width : o.width, - height : o.height + /** + * Converts a tinymce.html.Node image element to video/object/embed. + */ + imgToObject : function(node, args) { + var self = this, editor = self.editor, video, object, embed, iframe, name, value, data, + source, sources, params, param, typeItem, i, item, mp4Source, replacement, + posterSrc, style, audio; + + // Adds the flash player + function addPlayer(video_src, poster_src) { + var baseUri, flashVars, flashVarsOutput, params, flashPlayer; + + flashPlayer = editor.getParam('flash_video_player_url', self.convertUrl(self.url + '/moxieplayer.swf')); + if (flashPlayer) { + baseUri = editor.documentBaseURI; + data.params.src = flashPlayer; + + // Convert the movie url to absolute urls + if (editor.getParam('flash_video_player_absvideourl', true)) { + video_src = baseUri.toAbsolute(video_src || '', true); + poster_src = baseUri.toAbsolute(poster_src || '', true); + } + + // Generate flash vars + flashVarsOutput = ''; + flashVars = editor.getParam('flash_video_player_flashvars', {url : '$url', poster : '$poster'}); + tinymce.each(flashVars, function(value, name) { + // Replace $url and $poster variables in flashvars value + value = value.replace(/\$url/, video_src || ''); + value = value.replace(/\$poster/, poster_src || ''); + + if (value.length > 0) + flashVarsOutput += (flashVarsOutput ? '&' : '') + name + '=' + escape(value); + }); + + if (flashVarsOutput.length) + data.params.flashvars = flashVarsOutput; + + params = editor.getParam('flash_video_player_params', { + allowfullscreen: true, + allowscriptaccess: true + }); + + tinymce.each(params, function(value, name) { + data.params[name] = "" + value; + }); + } + }; + + data = node.attr('data-mce-json'); + if (!data) + return; + + data = JSON.parse(data); + typeItem = this.getType(node.attr('class')); + + style = node.attr('data-mce-style') + if (!style) { + style = node.attr('style'); + + if (style) + style = editor.dom.serializeStyle(editor.dom.parseStyle(style, 'img')); + } + + // Handle iframe + if (typeItem.name === 'Iframe') { + replacement = new Node('iframe', 1); + + tinymce.each(rootAttributes, function(name) { + var value = node.attr(name); + + if (name == 'class' && value) + value = value.replace(/mceItem.+ ?/g, ''); + + if (value && value.length > 0) + replacement.attr(name, value); }); - } else { - ob = dom.create('span', { - id : p.id, - _mce_name : 'object', - classid : "clsid:" + o.classid, - style : dom.getAttrib(n, 'style'), - codebase : o.codebase, - width : o.width, - height : o.height + + for (name in data.params) + replacement.attr(name, data.params[name]); + + replacement.attr({ + style: style, + src: data.params.src }); + + node.replace(replacement); + + return; } - each (p, function(v, k) { - if (!/^(width|height|codebase|classid|id|_cx|_cy)$/.test(k)) { - // Use url instead of src in IE for Windows media - if (o.type == 'application/x-mplayer2' && k == 'src' && !p.url) - k = 'url'; + // Handle scripts + if (this.editor.settings.media_use_script) { + replacement = new Node('script', 1).attr('type', 'text/javascript'); + + value = new Node('#text', 3); + value.value = 'write' + typeItem.name + '(' + JSON.serialize(tinymce.extend(data.params, { + width: node.attr('width'), + height: node.attr('height') + })) + ');'; + + replacement.append(value); + node.replace(replacement); - if (v) - dom.add(ob, 'span', {_mce_name : 'param', name : k, '_mce_value' : v}); + return; + } + + // Add HTML5 video element + if (typeItem.name === 'Video' && data.video.sources[0]) { + // Create new object element + video = new Node('video', 1).attr(tinymce.extend({ + id : node.attr('id'), + width: node.attr('width'), + height: node.attr('height'), + style : style + }, data.video.attrs)); + + // Get poster source and use that for flash fallback + if (data.video.attrs) + posterSrc = data.video.attrs.poster; + + sources = data.video.sources = toArray(data.video.sources); + for (i = 0; i < sources.length; i++) { + if (/\.mp4$/.test(sources[i].src)) + mp4Source = sources[i].src; } - }); - if (!stc) - dom.add(ob, 'span', tinymce.extend({_mce_name : 'embed', type : o.type, style : dom.getAttrib(n, 'style')}, p)); + if (!sources[0].type) { + video.attr('src', sources[0].src); + sources.splice(0, 1); + } - return ob; - }, + for (i = 0; i < sources.length; i++) { + source = new Node('source', 1).attr(sources[i]); + source.shortEnded = true; + video.append(source); + } - _spansToImgs : function(p) { - var t = this, dom = t.editor.dom, im, ci; + // Create flash fallback for video if we have a mp4 source + if (mp4Source) { + addPlayer(mp4Source, posterSrc); + typeItem = self.getType('flash'); + } else + data.params.src = ''; + } - each(dom.select('span', p), function(n) { - // Convert object into image - if (dom.getAttrib(n, 'class') == 'mceItemObject') { - ci = dom.getAttrib(n, "classid").toLowerCase().replace(/\s+/g, ''); + // Add HTML5 audio element + if (typeItem.name === 'Audio' && data.video.sources[0]) { + // Create new object element + audio = new Node('audio', 1).attr(tinymce.extend({ + id : node.attr('id'), + width: node.attr('width'), + height: node.attr('height'), + style : style + }, data.video.attrs)); + + // Get poster source and use that for flash fallback + if (data.video.attrs) + posterSrc = data.video.attrs.poster; + + sources = data.video.sources = toArray(data.video.sources); + if (!sources[0].type) { + audio.attr('src', sources[0].src); + sources.splice(0, 1); + } - switch (ci) { - case 'clsid:d27cdb6e-ae6d-11cf-96b8-444553540000': - dom.replace(t._createImg('mceItemFlash', n), n); - break; + for (i = 0; i < sources.length; i++) { + source = new Node('source', 1).attr(sources[i]); + source.shortEnded = true; + audio.append(source); + } - case 'clsid:166b1bca-3f9c-11cf-8075-444553540000': - dom.replace(t._createImg('mceItemShockWave', n), n); - break; + data.params.src = ''; + } - case 'clsid:6bf52a52-394a-11d3-b153-00c04f79faa6': - case 'clsid:22d6f312-b0f6-11d0-94ab-0080c74c7e95': - case 'clsid:05589fa1-c356-11ce-bf01-00aa0055595a': - dom.replace(t._createImg('mceItemWindowsMedia', n), n); - break; + if (typeItem.name === 'EmbeddedAudio') { + embed = new Node('embed', 1); + embed.shortEnded = true; + embed.attr({ + id: node.attr('id'), + width: node.attr('width'), + height: node.attr('height'), + style : style, + type: node.attr('type') + }); - case 'clsid:02bf25d5-8c17-4b23-bc80-d3488abddc6b': - dom.replace(t._createImg('mceItemQuickTime', n), n); - break; + for (name in data.params) + embed.attr(name, data.params[name]); - case 'clsid:cfcdaa03-8be4-11cf-b84b-0020afbbccfa': - dom.replace(t._createImg('mceItemRealMedia', n), n); - break; + tinymce.each(rootAttributes, function(name) { + if (data[name] && name != 'type') + embed.attr(name, data[name]); + }); - default: - dom.replace(t._createImg('mceItemFlash', n), n); - } - - return; + data.params.src = ''; + } + + // Do we have a params src then we can generate object + if (data.params.src) { + // Is flv movie add player for it + if (/\.flv$/i.test(data.params.src)) + addPlayer(data.params.src, ''); + + if (args && args.force_absolute) + data.params.src = editor.documentBaseURI.toAbsolute(data.params.src); + + // Create new object element + object = new Node('object', 1).attr({ + id : node.attr('id'), + width: node.attr('width'), + height: node.attr('height'), + style : style + }); + + tinymce.each(rootAttributes, function(name) { + var value = data[name]; + + if (name == 'class' && value) + value = value.replace(/mceItem.+ ?/g, ''); + + if (value && name != 'type') + object.attr(name, value); + }); + + // Add params + for (name in data.params) { + param = new Node('param', 1); + param.shortEnded = true; + value = data.params[name]; + + // Windows media needs to use url instead of src for the media URL + if (name === 'src' && typeItem.name === 'WindowsMedia') + name = 'url'; + + param.attr({name: name, value: value}); + object.append(param); } - // Convert embed into image - if (dom.getAttrib(n, 'class') == 'mceItemEmbed') { - switch (dom.getAttrib(n, 'type')) { - case 'application/x-shockwave-flash': - dom.replace(t._createImg('mceItemFlash', n), n); - break; + // Setup add type and classid if strict is disabled + if (this.editor.getParam('media_strict', true)) { + object.attr({ + data: data.params.src, + type: typeItem.mimes[0] + }); + } else { + object.attr({ + classid: "clsid:" + typeItem.clsids[0], + codebase: typeItem.codebase + }); - case 'application/x-director': - dom.replace(t._createImg('mceItemShockWave', n), n); - break; + embed = new Node('embed', 1); + embed.shortEnded = true; + embed.attr({ + id: node.attr('id'), + width: node.attr('width'), + height: node.attr('height'), + style : style, + type: typeItem.mimes[0] + }); - case 'application/x-mplayer2': - dom.replace(t._createImg('mceItemWindowsMedia', n), n); - break; + for (name in data.params) + embed.attr(name, data.params[name]); - case 'video/quicktime': - dom.replace(t._createImg('mceItemQuickTime', n), n); - break; + tinymce.each(rootAttributes, function(name) { + if (data[name] && name != 'type') + embed.attr(name, data[name]); + }); - case 'audio/x-pn-realaudio-plugin': - dom.replace(t._createImg('mceItemRealMedia', n), n); - break; + object.append(embed); + } - default: - dom.replace(t._createImg('mceItemFlash', n), n); - } - } - }); + // Insert raw HTML + if (data.object_html) { + value = new Node('#text', 3); + value.raw = true; + value.value = data.object_html; + object.append(value); + } + + // Append object to video element if it exists + if (video) + video.append(object); + } + + if (video) { + // Insert raw HTML + if (data.video_html) { + value = new Node('#text', 3); + value.raw = true; + value.value = data.video_html; + video.append(value); + } + } + + if (audio) { + // Insert raw HTML + if (data.video_html) { + value = new Node('#text', 3); + value.raw = true; + value.value = data.video_html; + audio.append(value); + } + } + + var n = video || audio || object || embed; + if (n) + node.replace(n); + else + node.remove(); }, - _createImg : function(cl, n) { - var im, dom = this.editor.dom, pa = {}, ti = '', args; + /** + * Converts a tinymce.html.Node video/object/embed to an img element. + * + * The video/object/embed will be converted into an image placeholder with a JSON data attribute like this: + * + * + * The JSON structure will be like this: + * {'params':{'flashvars':'something','quality':'high','src':'someurl'}, 'video':{'sources':[{src: 'someurl', type: 'video/mp4'}]}} + */ + objectToImg : function(node) { + var object, embed, video, iframe, img, name, id, width, height, style, i, html, + param, params, source, sources, data, type, lookup = this.lookup, + matches, attrs, urlConverter = this.editor.settings.url_converter, + urlConverterScope = this.editor.settings.url_converter_scope, + hspace, vspace, align, bgcolor; + + function getInnerHTML(node) { + return new tinymce.html.Serializer({ + inner: true, + validate: false + }).serialize(node); + }; - args = ['id', 'name', 'width', 'height', 'bgcolor', 'align', 'flashvars', 'src', 'wmode', 'allowfullscreen', 'quality', 'data']; + function lookupAttribute(o, attr) { + return lookup[(o.attr(attr) || '').toLowerCase()]; + } - // Create image - im = dom.create('img', { - src : this.url + '/img/trans.gif', - width : dom.getAttrib(n, 'width') || 100, - height : dom.getAttrib(n, 'height') || 100, - style : dom.getAttrib(n, 'style'), - 'class' : cl - }); + function lookupExtension(src) { + var ext = src.replace(/^.*\.([^.]+)$/, '$1'); + return lookup[ext.toLowerCase() || '']; + } - // Setup base parameters - each(args, function(na) { - var v = dom.getAttrib(n, na); + // If node isn't in document + if (!node.parent) + return; - if (v) - pa[na] = v; - }); + // Handle media scripts + if (node.name === 'script') { + if (node.firstChild) + matches = scriptRegExp.exec(node.firstChild.value); + + if (!matches) + return; + + type = matches[1]; + data = {video : {}, params : JSON.parse(matches[2])}; + width = data.params.width; + height = data.params.height; + } - // Add optional parameters - each(dom.select('span', n), function(n) { - if (dom.hasClass(n, 'mceItemParam')) - pa[dom.getAttrib(n, 'name')] = dom.getAttrib(n, '_mce_value'); + // Setup data objects + data = data || { + video : {}, + params : {} + }; + + // Setup new image object + img = new Node('img', 1); + img.attr({ + src : this.editor.theme.url + '/img/trans.gif' }); - // Use src not movie - if (pa.movie) { - pa.src = pa.movie; - delete pa.movie; + // Video element + name = node.name; + if (name === 'video' || name == 'audio') { + video = node; + object = node.getAll('object')[0]; + embed = node.getAll('embed')[0]; + width = video.attr('width'); + height = video.attr('height'); + id = video.attr('id'); + data.video = {attrs : {}, sources : []}; + + // Get all video attributes + attrs = data.video.attrs; + for (name in video.attributes.map) + attrs[name] = video.attributes.map[name]; + + source = node.attr('src'); + if (source) + data.video.sources.push({src : urlConverter.call(urlConverterScope, source, 'src', node.name)}); + + // Get all sources + sources = video.getAll("source"); + for (i = 0; i < sources.length; i++) { + source = sources[i].remove(); + + data.video.sources.push({ + src: urlConverter.call(urlConverterScope, source.attr('src'), 'src', 'source'), + type: source.attr('type'), + media: source.attr('media') + }); + } + + // Convert the poster URL + if (attrs.poster) + attrs.poster = urlConverter.call(urlConverterScope, attrs.poster, 'poster', node.name); + } + + // Object element + if (node.name === 'object') { + object = node; + embed = node.getAll('embed')[0]; + } + + // Embed element + if (node.name === 'embed') + embed = node; + + // Iframe element + if (node.name === 'iframe') { + iframe = node; + type = 'Iframe'; } - // No src try data - if (!pa.src) { - pa.src = pa.data; - delete pa.data; + if (object) { + // Get width/height + width = width || object.attr('width'); + height = height || object.attr('height'); + style = style || object.attr('style'); + id = id || object.attr('id'); + hspace = hspace || object.attr('hspace'); + vspace = vspace || object.attr('vspace'); + align = align || object.attr('align'); + bgcolor = bgcolor || object.attr('bgcolor'); + data.name = object.attr('name'); + + // Get all object params + params = object.getAll("param"); + for (i = 0; i < params.length; i++) { + param = params[i]; + name = param.remove().attr('name'); + + if (!excludedAttrs[name]) + data.params[name] = param.attr('value'); + } + + data.params.src = data.params.src || object.attr('data'); } - // Merge with embed args - n = dom.select('.mceItemEmbed', n)[0]; - if (n) { - each(args, function(na) { - var v = dom.getAttrib(n, na); + if (embed) { + // Get width/height + width = width || embed.attr('width'); + height = height || embed.attr('height'); + style = style || embed.attr('style'); + id = id || embed.attr('id'); + hspace = hspace || embed.attr('hspace'); + vspace = vspace || embed.attr('vspace'); + align = align || embed.attr('align'); + bgcolor = bgcolor || embed.attr('bgcolor'); + + // Get all embed attributes + for (name in embed.attributes.map) { + if (!excludedAttrs[name] && !data.params[name]) + data.params[name] = embed.attributes.map[name]; + } + } - if (v && !pa[na]) - pa[na] = v; + if (iframe) { + // Get width/height + width = iframe.attr('width'); + height = iframe.attr('height'); + style = style || iframe.attr('style'); + id = iframe.attr('id'); + hspace = iframe.attr('hspace'); + vspace = iframe.attr('vspace'); + align = iframe.attr('align'); + bgcolor = iframe.attr('bgcolor'); + + tinymce.each(rootAttributes, function(name) { + img.attr(name, iframe.attr(name)); }); + + // Get all iframe attributes + for (name in iframe.attributes.map) { + if (!excludedAttrs[name] && !data.params[name]) + data.params[name] = iframe.attributes.map[name]; + } } - delete pa.width; - delete pa.height; + // Use src not movie + if (data.params.movie) { + data.params.src = data.params.src || data.params.movie; + delete data.params.movie; + } - im.title = this._serialize(pa); + // Convert the URL to relative/absolute depending on configuration + if (data.params.src) + data.params.src = urlConverter.call(urlConverterScope, data.params.src, 'src', 'object'); - return im; - }, + if (video) { + if (node.name === 'video') + type = lookup.video.name; + else if (node.name === 'audio') + type = lookup.audio.name; + } - _parse : function(s) { - return tinymce.util.JSON.parse('{' + s + '}'); - }, + if (object && !type) + type = (lookupAttribute(object, 'clsid') || lookupAttribute(object, 'classid') || lookupAttribute(object, 'type') || {}).name; + + if (embed && !type) + type = (lookupAttribute(embed, 'type') || lookupExtension(data.params.src) || {}).name; - _serialize : function(o) { - return tinymce.util.JSON.serialize(o).replace(/[{}]/g, ''); + // for embedded audio we preserve the original specified type + if (embed && type == 'EmbeddedAudio') { + data.params.type = embed.attr('type'); + } + + // Replace the video/object/embed element with a placeholder image containing the data + node.replace(img); + + // Remove embed + if (embed) + embed.remove(); + + // Serialize the inner HTML of the object element + if (object) { + html = getInnerHTML(object.remove()); + + if (html) + data.object_html = html; + } + + // Serialize the inner HTML of the video element + if (video) { + html = getInnerHTML(video.remove()); + + if (html) + data.video_html = html; + } + + data.hspace = hspace; + data.vspace = vspace; + data.align = align; + data.bgcolor = bgcolor; + + // Set width/height of placeholder + img.attr({ + id : id, + 'class' : 'mceItemMedia mceItem' + (type || 'Flash'), + style : style, + width : width || (node.name == 'audio' ? "300" : "320"), + height : height || (node.name == 'audio' ? "32" : "240"), + hspace : hspace, + vspace : vspace, + align : align, + bgcolor : bgcolor, + "data-mce-json" : JSON.serialize(data, "'") + }); } }); // Register plugin tinymce.PluginManager.add('media', tinymce.plugins.MediaPlugin); -})(); \ No newline at end of file +})(); diff --git a/js/tiny_mce/plugins/media/js/media.js b/js/tiny_mce/plugins/media/js/media.js index 86cfa98563..45d88fe1b4 100644 --- a/js/tiny_mce/plugins/media/js/media.js +++ b/js/tiny_mce/plugins/media/js/media.js @@ -1,630 +1,464 @@ -tinyMCEPopup.requireLangPack(); +(function() { + var url; -var oldWidth, oldHeight, ed, url; + if (url = tinyMCEPopup.getParam("media_external_list_url")) + document.write(''); -if (url = tinyMCEPopup.getParam("media_external_list_url")) - document.write(''); - -function init() { - var pl = "", f, val; - var type = "flash", fe, i; - - ed = tinyMCEPopup.editor; - - tinyMCEPopup.resizeToInnerSize(); - f = document.forms[0] - - fe = ed.selection.getNode(); - if (/mceItem(Flash|ShockWave|WindowsMedia|QuickTime|RealMedia)/.test(ed.dom.getAttrib(fe, 'class'))) { - pl = fe.title; - - switch (ed.dom.getAttrib(fe, 'class')) { - case 'mceItemFlash': - type = 'flash'; - break; - - case 'mceItemFlashVideo': - type = 'flv'; - break; - - case 'mceItemShockWave': - type = 'shockwave'; - break; - - case 'mceItemWindowsMedia': - type = 'wmp'; - break; - - case 'mceItemQuickTime': - type = 'qt'; - break; - - case 'mceItemRealMedia': - type = 'rmp'; - break; - } - - document.forms[0].insert.value = ed.getLang('update', 'Insert', true); + function get(id) { + return document.getElementById(id); } - document.getElementById('filebrowsercontainer').innerHTML = getBrowserHTML('filebrowser','src','media','media'); - document.getElementById('qtsrcfilebrowsercontainer').innerHTML = getBrowserHTML('qtsrcfilebrowser','qt_qtsrc','media','media'); - document.getElementById('bgcolor_pickcontainer').innerHTML = getColorPickerHTML('bgcolor_pick','bgcolor'); - - var html = getMediaListHTML('medialist','src','media','media'); - if (html == "") - document.getElementById("linklistrow").style.display = 'none'; - else - document.getElementById("linklistcontainer").innerHTML = html; - - // Resize some elements - if (isVisible('filebrowser')) - document.getElementById('src').style.width = '230px'; - - // Setup form - if (pl != "") { - pl = tinyMCEPopup.editor.plugins.media._parse(pl); - - switch (type) { - case "flash": - setBool(pl, 'flash', 'play'); - setBool(pl, 'flash', 'loop'); - setBool(pl, 'flash', 'menu'); - setBool(pl, 'flash', 'swliveconnect'); - setStr(pl, 'flash', 'quality'); - setStr(pl, 'flash', 'scale'); - setStr(pl, 'flash', 'salign'); - setStr(pl, 'flash', 'wmode'); - setStr(pl, 'flash', 'base'); - setStr(pl, 'flash', 'flashvars'); - break; - - case "qt": - setBool(pl, 'qt', 'loop'); - setBool(pl, 'qt', 'autoplay'); - setBool(pl, 'qt', 'cache'); - setBool(pl, 'qt', 'controller'); - setBool(pl, 'qt', 'correction'); - setBool(pl, 'qt', 'enablejavascript'); - setBool(pl, 'qt', 'kioskmode'); - setBool(pl, 'qt', 'autohref'); - setBool(pl, 'qt', 'playeveryframe'); - setBool(pl, 'qt', 'tarsetcache'); - setStr(pl, 'qt', 'scale'); - setStr(pl, 'qt', 'starttime'); - setStr(pl, 'qt', 'endtime'); - setStr(pl, 'qt', 'tarset'); - setStr(pl, 'qt', 'qtsrcchokespeed'); - setStr(pl, 'qt', 'volume'); - setStr(pl, 'qt', 'qtsrc'); - break; - - case "shockwave": - setBool(pl, 'shockwave', 'sound'); - setBool(pl, 'shockwave', 'progress'); - setBool(pl, 'shockwave', 'autostart'); - setBool(pl, 'shockwave', 'swliveconnect'); - setStr(pl, 'shockwave', 'swvolume'); - setStr(pl, 'shockwave', 'swstretchstyle'); - setStr(pl, 'shockwave', 'swstretchhalign'); - setStr(pl, 'shockwave', 'swstretchvalign'); - break; - - case "wmp": - setBool(pl, 'wmp', 'autostart'); - setBool(pl, 'wmp', 'enabled'); - setBool(pl, 'wmp', 'enablecontextmenu'); - setBool(pl, 'wmp', 'fullscreen'); - setBool(pl, 'wmp', 'invokeurls'); - setBool(pl, 'wmp', 'mute'); - setBool(pl, 'wmp', 'stretchtofit'); - setBool(pl, 'wmp', 'windowlessvideo'); - setStr(pl, 'wmp', 'balance'); - setStr(pl, 'wmp', 'baseurl'); - setStr(pl, 'wmp', 'captioningid'); - setStr(pl, 'wmp', 'currentmarker'); - setStr(pl, 'wmp', 'currentposition'); - setStr(pl, 'wmp', 'defaultframe'); - setStr(pl, 'wmp', 'playcount'); - setStr(pl, 'wmp', 'rate'); - setStr(pl, 'wmp', 'uimode'); - setStr(pl, 'wmp', 'volume'); - break; - - case "rmp": - setBool(pl, 'rmp', 'autostart'); - setBool(pl, 'rmp', 'loop'); - setBool(pl, 'rmp', 'autogotourl'); - setBool(pl, 'rmp', 'center'); - setBool(pl, 'rmp', 'imagestatus'); - setBool(pl, 'rmp', 'maintainaspect'); - setBool(pl, 'rmp', 'nojava'); - setBool(pl, 'rmp', 'prefetch'); - setBool(pl, 'rmp', 'shuffle'); - setStr(pl, 'rmp', 'console'); - setStr(pl, 'rmp', 'controls'); - setStr(pl, 'rmp', 'numloop'); - setStr(pl, 'rmp', 'scriptcallbacks'); - break; - } - - setStr(pl, null, 'src'); - setStr(pl, null, 'id'); - setStr(pl, null, 'name'); - setStr(pl, null, 'vspace'); - setStr(pl, null, 'hspace'); - setStr(pl, null, 'bgcolor'); - setStr(pl, null, 'align'); - setStr(pl, null, 'width'); - setStr(pl, null, 'height'); - - if ((val = ed.dom.getAttrib(fe, "width")) != "") - pl.width = f.width.value = val; - - if ((val = ed.dom.getAttrib(fe, "height")) != "") - pl.height = f.height.value = val; + function clone(obj) { + var i, len, copy, attr; - oldWidth = pl.width ? parseInt(pl.width) : 0; - oldHeight = pl.height ? parseInt(pl.height) : 0; - } else - oldWidth = oldHeight = 0; + if (null == obj || "object" != typeof obj) + return obj; - selectByValue(f, 'media_type', type); - changedType(type); - updateColor('bgcolor_pick', 'bgcolor'); - - TinyMCE_EditableSelects.init(); - generatePreview(); -} - -function insertMedia() { - var fe, f = document.forms[0], h; - - tinyMCEPopup.restoreSelection(); - - if (!AutoValidator.validate(f)) { - tinyMCEPopup.alert(ed.getLang('invalid_data')); - return false; - } + // Handle Array + if ('length' in obj) { + copy = []; - f.width.value = f.width.value == "" ? 100 : f.width.value; - f.height.value = f.height.value == "" ? 100 : f.height.value; - - fe = ed.selection.getNode(); - if (fe != null && /mceItem(Flash|ShockWave|WindowsMedia|QuickTime|RealMedia)/.test(ed.dom.getAttrib(fe, 'class'))) { - switch (f.media_type.options[f.media_type.selectedIndex].value) { - case "flash": - fe.className = "mceItemFlash"; - break; - - case "flv": - fe.className = "mceItemFlashVideo"; - break; - - case "shockwave": - fe.className = "mceItemShockWave"; - break; - - case "qt": - fe.className = "mceItemQuickTime"; - break; - - case "wmp": - fe.className = "mceItemWindowsMedia"; - break; + for (i = 0, len = obj.length; i < len; ++i) { + copy[i] = clone(obj[i]); + } - case "rmp": - fe.className = "mceItemRealMedia"; - break; + return copy; } - if (fe.width != f.width.value || fe.height != f.height.value) - ed.execCommand('mceRepaint'); - - fe.title = serializeParameters(); - fe.width = f.width.value; - fe.height = f.height.value; - fe.style.width = f.width.value + (f.width.value.indexOf('%') == -1 ? 'px' : ''); - fe.style.height = f.height.value + (f.height.value.indexOf('%') == -1 ? 'px' : ''); - fe.align = f.align.options[f.align.selectedIndex].value; - } else { - h = ' 0) { - var html = ""; - - html += ''; - - return html; + return copy; } - return ""; -} + function getVal(id) { + var elm = get(id); -function getType(v) { - var fo, i, c, el, x, f = document.forms[0]; + if (elm.nodeName == "SELECT") + return elm.options[elm.selectedIndex].value; - fo = ed.getParam("media_types", "flash=swf;flv=flv;shockwave=dcr;qt=mov,qt,mpg,mp3,mp4,mpeg;shockwave=dcr;wmp=avi,wmv,wm,asf,asx,wmx,wvx;rmp=rm,ra,ram").split(';'); - - // YouTube - if (v.match(/watch\?v=(.+)(.*)/)) { - f.width.value = '425'; - f.height.value = '350'; - f.src.value = 'http://www.youtube.com/v/' + v.match(/v=(.*)(.*)/)[0].split('=')[1]; - return 'flash'; - } + if (elm.type == "checkbox") + return elm.checked; - // Google video - if (v.indexOf('http://video.google.com/videoplay?docid=') == 0) { - f.width.value = '425'; - f.height.value = '326'; - f.src.value = 'http://video.google.com/googleplayer.swf?docId=' + v.substring('http://video.google.com/videoplay?docid='.length) + '&hl=en'; - return 'flash'; + return elm.value; } - for (i=0; i 0 ? s.substring(0, s.length - 1) : s; - - return s; -} - -function setBool(pl, p, n) { - if (typeof(pl[n]) == "undefined") - return; - - document.forms[0].elements[p + "_" + n].checked = pl[n] != 'false'; -} - -function setStr(pl, p, n) { - var f = document.forms[0], e = f.elements[(p != null ? p + "_" : '') + n]; - - if (typeof(pl[n]) == "undefined") - return; - - if (e.type == "text") - e.value = pl[n]; - else - selectByValue(f, (p != null ? p + "_" : '') + n, pl[n]); -} - -function getBool(p, n, d, tv, fv) { - var v = document.forms[0].elements[p + "_" + n].checked; - - tv = typeof(tv) == 'undefined' ? 'true' : "'" + jsEncode(tv) + "'"; - fv = typeof(fv) == 'undefined' ? 'false' : "'" + jsEncode(fv) + "'"; - - return (v == d) ? '' : n + (v ? ':' + tv + ',' : ":\'" + fv + "\',"); -} + window.Media = { + init : function() { + var html, editor, self = this; -function getStr(p, n, d) { - var e = document.forms[0].elements[(p != null ? p + "_" : "") + n]; - var v = e.type == "text" ? e.value : e.options[e.selectedIndex].value; + self.editor = editor = tinyMCEPopup.editor; - if (n == 'src') - v = tinyMCEPopup.editor.convertURL(v, 'src', null); + // Setup file browsers and color pickers + get('filebrowsercontainer').innerHTML = getBrowserHTML('filebrowser','src','media','media'); + get('qtsrcfilebrowsercontainer').innerHTML = getBrowserHTML('qtsrcfilebrowser','quicktime_qtsrc','media','media'); + get('bgcolor_pickcontainer').innerHTML = getColorPickerHTML('bgcolor_pick','bgcolor'); + get('video_altsource1_filebrowser').innerHTML = getBrowserHTML('video_filebrowser_altsource1','video_altsource1','media','media'); + get('video_altsource2_filebrowser').innerHTML = getBrowserHTML('video_filebrowser_altsource2','video_altsource2','media','media'); + get('audio_altsource1_filebrowser').innerHTML = getBrowserHTML('audio_filebrowser_altsource1','audio_altsource1','media','media'); + get('audio_altsource2_filebrowser').innerHTML = getBrowserHTML('audio_filebrowser_altsource2','audio_altsource2','media','media'); + get('video_poster_filebrowser').innerHTML = getBrowserHTML('filebrowser_poster','video_poster','media','image'); - return ((n == d || v == '') ? '' : n + ":'" + jsEncode(v) + "',"); -} + html = self.getMediaListHTML('medialist', 'src', 'media', 'media'); + if (html == "") + get("linklistrow").style.display = 'none'; + else + get("linklistcontainer").innerHTML = html; -function getInt(p, n, d) { - var e = document.forms[0].elements[(p != null ? p + "_" : "") + n]; - var v = e.type == "text" ? e.value : e.options[e.selectedIndex].value; - - return ((n == d || v == '') ? '' : n + ":" + v.replace(/[^0-9]+/g, '') + ","); -} - -function jsEncode(s) { - s = s.replace(new RegExp('\\\\', 'g'), '\\\\'); - s = s.replace(new RegExp('"', 'g'), '\\"'); - s = s.replace(new RegExp("'", 'g'), "\\'"); + if (isVisible('filebrowser')) + get('src').style.width = '230px'; + + if (isVisible('video_filebrowser_altsource1')) + get('video_altsource1').style.width = '220px'; + + if (isVisible('video_filebrowser_altsource2')) + get('video_altsource2').style.width = '220px'; + + if (isVisible('audio_filebrowser_altsource1')) + get('audio_altsource1').style.width = '220px'; + + if (isVisible('audio_filebrowser_altsource2')) + get('audio_altsource2').style.width = '220px'; + + if (isVisible('filebrowser_poster')) + get('video_poster').style.width = '220px'; + + editor.dom.setOuterHTML(get('media_type'), self.getMediaTypeHTML(editor)); + + self.setDefaultDialogSettings(editor); + self.data = clone(tinyMCEPopup.getWindowArg('data')); + self.dataToForm(); + self.preview(); + + updateColor('bgcolor_pick', 'bgcolor'); + }, + + insert : function() { + var editor = tinyMCEPopup.editor; + + this.formToData(); + editor.execCommand('mceRepaint'); + tinyMCEPopup.restoreSelection(); + editor.selection.setNode(editor.plugins.media.dataToImg(this.data)); + tinyMCEPopup.close(); + }, + + preview : function() { + get('prev').innerHTML = this.editor.plugins.media.dataToHtml(this.data, true); + }, + + moveStates : function(to_form, field) { + var data = this.data, editor = this.editor, + mediaPlugin = editor.plugins.media, ext, src, typeInfo, defaultStates, src; + + defaultStates = { + // QuickTime + quicktime_autoplay : true, + quicktime_controller : true, + + // Flash + flash_play : true, + flash_loop : true, + flash_menu : true, + + // WindowsMedia + windowsmedia_autostart : true, + windowsmedia_enablecontextmenu : true, + windowsmedia_invokeurls : true, + + // RealMedia + realmedia_autogotourl : true, + realmedia_imagestatus : true + }; + + function parseQueryParams(str) { + var out = {}; + + if (str) { + tinymce.each(str.split('&'), function(item) { + var parts = item.split('='); + + out[unescape(parts[0])] = unescape(parts[1]); + }); + } + + return out; + }; + + function setOptions(type, names) { + var i, name, formItemName, value, list; + + if (type == data.type || type == 'global') { + names = tinymce.explode(names); + for (i = 0; i < names.length; i++) { + name = names[i]; + formItemName = type == 'global' ? name : type + '_' + name; + + if (type == 'global') + list = data; + else if (type == 'video' || type == 'audio') { + list = data.video.attrs; + + if (!list && !to_form) + data.video.attrs = list = {}; + } else + list = data.params; + + if (list) { + if (to_form) { + setVal(formItemName, list[name], type == 'video' || type == 'audio' ? name : ''); + } else { + delete list[name]; + + value = getVal(formItemName); + if ((type == 'video' || type == 'audio') && value === true) + value = name; + + if (defaultStates[formItemName]) { + if (value !== defaultStates[formItemName]) { + value = "" + value; + list[name] = value; + } + } else if (value) { + value = "" + value; + list[name] = value; + } + } + } + } + } + } - return s; -} + if (!to_form) { + data.type = get('media_type').options[get('media_type').selectedIndex].value; + data.width = getVal('width'); + data.height = getVal('height'); -function generatePreview(c) { - var f = document.forms[0], p = document.getElementById('prev'), h = '', cls, pl, n, type, codebase, wp, hp, nw, nh; + // Switch type based on extension + src = getVal('src'); + if (field == 'src') { + ext = src.replace(/^.*\.([^.]+)$/, '$1'); + if (typeInfo = mediaPlugin.getType(ext)) + data.type = typeInfo.name.toLowerCase(); - p.innerHTML = ''; + setVal('media_type', data.type); + } - nw = parseInt(f.width.value); - nh = parseInt(f.height.value); + if (data.type == "video" || data.type == "audio") { + if (!data.video.sources) + data.video.sources = []; - if (f.width.value != "" && f.height.value != "") { - if (f.constrain.checked) { - if (c == 'width' && oldWidth != 0) { - wp = nw / oldWidth; - nh = Math.round(wp * nh); - f.height.value = nh; - } else if (c == 'height' && oldHeight != 0) { - hp = nh / oldHeight; - nw = Math.round(hp * nw); - f.width.value = nw; + data.video.sources[0] = {src: getVal('src')}; + } } - } - } - if (f.width.value != "") - oldWidth = nw; - - if (f.height.value != "") - oldHeight = nh; - - // After constrain - pl = serializeParameters(); - - switch (f.media_type.options[f.media_type.selectedIndex].value) { - case "flash": - cls = 'clsid:D27CDB6E-AE6D-11cf-96B8-444553540000'; - codebase = 'http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0'; - type = 'application/x-shockwave-flash'; - break; - - case "shockwave": - cls = 'clsid:166B1BCA-3F9C-11CF-8075-444553540000'; - codebase = 'http://download.macromedia.com/pub/shockwave/cabs/director/sw.cab#version=8,5,1,0'; - type = 'application/x-director'; - break; - - case "qt": - cls = 'clsid:02BF25D5-8C17-4B23-BC80-D3488ABDDC6B'; - codebase = 'http://www.apple.com/qtactivex/qtplugin.cab#version=6,0,2,0'; - type = 'video/quicktime'; - break; - - case "wmp": - cls = ed.getParam('media_wmp6_compatible') ? 'clsid:05589FA1-C356-11CE-BF01-00AA0055595A' : 'clsid:6BF52A52-394A-11D3-B153-00C04F79FAA6'; - codebase = 'http://activex.microsoft.com/activex/controls/mplayer/en/nsmp2inf.cab#Version=5,1,52,701'; - type = 'application/x-mplayer2'; - break; - - case "rmp": - cls = 'clsid:CFCDAA03-8BE4-11cf-B84B-0020AFBBCCFA'; - codebase = 'http://activex.microsoft.com/activex/controls/mplayer/en/nsmp2inf.cab#Version=5,1,52,701'; - type = 'audio/x-pn-realaudio-plugin'; - break; - } + // Hide all fieldsets and show the one active + get('video_options').style.display = 'none'; + get('audio_options').style.display = 'none'; + get('flash_options').style.display = 'none'; + get('quicktime_options').style.display = 'none'; + get('shockwave_options').style.display = 'none'; + get('windowsmedia_options').style.display = 'none'; + get('realmedia_options').style.display = 'none'; + get('embeddedaudio_options').style.display = 'none'; + + if (get(data.type + '_options')) + get(data.type + '_options').style.display = 'block'; + + setVal('media_type', data.type); + + setOptions('flash', 'play,loop,menu,swliveconnect,quality,scale,salign,wmode,base,flashvars'); + setOptions('quicktime', 'loop,autoplay,cache,controller,correction,enablejavascript,kioskmode,autohref,playeveryframe,targetcache,scale,starttime,endtime,target,qtsrcchokespeed,volume,qtsrc'); + setOptions('shockwave', 'sound,progress,autostart,swliveconnect,swvolume,swstretchstyle,swstretchhalign,swstretchvalign'); + setOptions('windowsmedia', 'autostart,enabled,enablecontextmenu,fullscreen,invokeurls,mute,stretchtofit,windowlessvideo,balance,baseurl,captioningid,currentmarker,currentposition,defaultframe,playcount,rate,uimode,volume'); + setOptions('realmedia', 'autostart,loop,autogotourl,center,imagestatus,maintainaspect,nojava,prefetch,shuffle,console,controls,numloop,scriptcallbacks'); + setOptions('video', 'poster,autoplay,loop,muted,preload,controls'); + setOptions('audio', 'autoplay,loop,preload,controls'); + setOptions('embeddedaudio', 'autoplay,loop,controls'); + setOptions('global', 'id,name,vspace,hspace,bgcolor,align,width,height'); + + if (to_form) { + if (data.type == 'video') { + if (data.video.sources[0]) + setVal('src', data.video.sources[0].src); + + src = data.video.sources[1]; + if (src) + setVal('video_altsource1', src.src); + + src = data.video.sources[2]; + if (src) + setVal('video_altsource2', src.src); + } else if (data.type == 'audio') { + if (data.video.sources[0]) + setVal('src', data.video.sources[0].src); + + src = data.video.sources[1]; + if (src) + setVal('audio_altsource1', src.src); + + src = data.video.sources[2]; + if (src) + setVal('audio_altsource2', src.src); + } else { + // Check flash vars + if (data.type == 'flash') { + tinymce.each(editor.getParam('flash_video_player_flashvars', {url : '$url', poster : '$poster'}), function(value, name) { + if (value == '$url') + data.params.src = parseQueryParams(data.params.flashvars)[name] || data.params.src || ''; + }); + } + + setVal('src', data.params.src); + } + } else { + src = getVal("src"); + + // YouTube *NEW* + if (src.match(/youtu.be\/[a-z1-9.-_]+/)) { + data.width = 425; + data.height = 350; + data.params.frameborder = '0'; + data.type = 'iframe'; + src = 'http://www.youtube.com/embed/' + src.match(/youtu.be\/([a-z1-9.-_]+)/)[1]; + setVal('src', src); + setVal('media_type', data.type); + } + + // YouTube + if (src.match(/youtube.com(.+)v=([^&]+)/)) { + data.width = 425; + data.height = 350; + data.params.frameborder = '0'; + data.type = 'iframe'; + src = 'http://www.youtube.com/embed/' + src.match(/v=([^&]+)/)[1]; + setVal('src', src); + setVal('media_type', data.type); + } + + // Google video + if (src.match(/video.google.com(.+)docid=([^&]+)/)) { + data.width = 425; + data.height = 326; + data.type = 'flash'; + src = 'http://video.google.com/googleplayer.swf?docId=' + src.match(/docid=([^&]+)/)[1] + '&hl=en'; + setVal('src', src); + setVal('media_type', data.type); + } + + if (data.type == 'video') { + if (!data.video.sources) + data.video.sources = []; + + data.video.sources[0] = {src : src}; + + src = getVal("video_altsource1"); + if (src) + data.video.sources[1] = {src : src}; + + src = getVal("video_altsource2"); + if (src) + data.video.sources[2] = {src : src}; + } else if (data.type == 'audio') { + if (!data.video.sources) + data.video.sources = []; + + data.video.sources[0] = {src : src}; + + src = getVal("audio_altsource1"); + if (src) + data.video.sources[1] = {src : src}; + + src = getVal("audio_altsource2"); + if (src) + data.video.sources[2] = {src : src}; + } else + data.params.src = src; + + // Set default size + setVal('width', data.width || (data.type == 'audio' ? 300 : 320)); + setVal('height', data.height || (data.type == 'audio' ? 32 : 240)); + } + }, + + dataToForm : function() { + this.moveStates(true); + }, + + formToData : function(field) { + if (field == "width" || field == "height") + this.changeSize(field); + + if (field == 'source') { + this.moveStates(false, field); + setVal('source', this.editor.plugins.media.dataToHtml(this.data)); + this.panel = 'source'; + } else { + if (this.panel == 'source') { + this.data = clone(this.editor.plugins.media.htmlToData(getVal('source'))); + this.dataToForm(); + this.panel = ''; + } + + this.moveStates(false, field); + this.preview(); + } + }, + + beforeResize : function() { + this.width = parseInt(getVal('width') || (this.data.type == 'audio' ? "300" : "320"), 10); + this.height = parseInt(getVal('height') || (this.data.type == 'audio' ? "32" : "240"), 10); + }, + + changeSize : function(type) { + var width, height, scale, size; + + if (get('constrain').checked) { + width = parseInt(getVal('width') || (this.data.type == 'audio' ? "300" : "320"), 10); + height = parseInt(getVal('height') || (this.data.type == 'audio' ? "32" : "240"), 10); + + if (type == 'width') { + this.height = Math.round((width / this.width) * height); + setVal('height', this.height); + } else { + this.width = Math.round((height / this.height) * width); + setVal('width', this.width); + } + } + }, - if (pl == '') { - p.innerHTML = ''; - return; - } + getMediaListHTML : function() { + if (typeof(tinyMCEMediaList) != "undefined" && tinyMCEMediaList.length > 0) { + var html = ""; - pl = tinyMCEPopup.editor.plugins.media._parse(pl); + html += ''; - // Avoid annoying warning about insecure items - if (!tinymce.isIE || document.location.protocol != 'https:') { - h += ''; + return html; + } - for (n in pl) { - h += ''; + return ""; + }, - // Add extra url parameter if it's an absolute URL - if (n == 'src' && pl[n].indexOf('://') != -1) - h += ''; + getMediaTypeHTML : function(editor) { + function option(media_type){ + return '' + } + var html = ""; + html += ''; + return html; + }, + + setDefaultDialogSettings : function(editor) { + var defaultDialogSettings = editor.getParam("media_dialog_defaults", {}); + tinymce.each(defaultDialogSettings, function(v, k) { + setVal(k, v); + }); } - } - - h += ' - -
    -
    + + + @@ -24,28 +25,21 @@
    {#media_dlg.general} - +
    -
    - +
    - - - + @@ -56,10 +50,10 @@
    + + + - +
     
    - +
    - - + +
    x    x   
    @@ -78,18 +72,18 @@
    {#media_dlg.advanced} - +
    - + - + - + - +
    - @@ -100,9 +94,9 @@ - +
    - +
     
    @@ -111,192 +105,284 @@
    -
    - {#media_dlg.flash_options} +
    + {#media_dlg.html5_video_options} - +
    - + + - + + - + + - + + +
    - + + + + + +
     
    - + + + + + +
     
    - + + + + + +
     
    - + + +
    + -
    - + -
    + - - + +
    - + + + + + +
    + - - + + + +
    +
    + + + + + +
    +
    + + + +
    + + +
    + {#media_dlg.embedded_audio_options} + -
    - + -
    + - - + +
    - + - -
    + - - + +
    - - - - - - - - - +
    + + + + + +
    +
    -
    - {#media_dlg.flv_options} +
    + {#media_dlg.html5_audio_options} - +
    - + - - - - - - - - + + - - - - + + +
    - + + + + + +
     
    + + + + + +
     
    +
    + +
    + -
    - + -
    + - - + +
    - + + + +
    + + + + + +
    +
    + - - + +
    + + +
    + {#media_dlg.flash_options} + + + + + + + + + + + + + + + + + @@ -304,45 +390,57 @@
    + + + +
    + + + +
    - +
    - - + +
    - +
    - - + +
    - +
    - - + +
    - +
    - - + +
    + + + + + + + + + + + +
    -
    +
    {#media_dlg.qt_options} - +
    @@ -350,19 +448,19 @@ @@ -370,19 +468,19 @@ @@ -390,19 +488,19 @@ @@ -410,27 +508,27 @@ - - + - - + + - - + + - - + + - - + + - - + + - - + + - +
    - +
    - - + +
    - +
    - - + +
    - +
    - - + +
    - +
    - - + +
    - +
    - - + +
    - +
    - - + +
    - +
    - - + +
    - +
    - - + +
    - +
    - - + +
    - +
    - - + +
    - - - - - -
     
    + + + + + +
     
    -
    +
    {#media_dlg.wmp_options} - +
    @@ -504,19 +602,19 @@ @@ -524,19 +622,19 @@ @@ -544,86 +642,86 @@ - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + +
    - +
    - - + +
    - +
    - - + +
    - +
    - - + +
    - +
    - - + +
    - +
    - - + +
    - +
    - - + +
    - +
    - - + +
    - +
    - - + +
    -
    +
    {#media_dlg.rmp_options} - +
    @@ -631,19 +729,19 @@ @@ -651,19 +749,19 @@ @@ -671,19 +769,19 @@ @@ -691,10 +789,10 @@ @@ -705,19 +803,19 @@ - - + + - - + + - - + + - - + +
    - +
    - - + +
    - +
    - - + +
    - +
    - - + +
    - +
    - - + +
    - +
    - - + +
    - +
    - - + +
    - +
    - - + +
    - +
    - - + +
    - +
    - - + +
    @@ -725,11 +823,11 @@
    {#media_dlg.shockwave_options} - +
    - +
    - @@ -738,13 +836,13 @@
    - @@ -754,7 +852,7 @@ - @@ -765,18 +863,18 @@
    - +
    - +
    - +
    - +
    @@ -786,18 +884,18 @@
    - +
    - +
    - +
    - +
    @@ -806,6 +904,13 @@
    + +
    +
    + {#media_dlg.source} + +
    +
    diff --git a/js/tiny_mce/plugins/media/moxieplayer.swf b/js/tiny_mce/plugins/media/moxieplayer.swf new file mode 100644 index 0000000000000000000000000000000000000000..585d772d6d3c23626fddfa58c4220b056783e148 GIT binary patch literal 19980 zcmV)FK)=63S5pccivR$4+TFbgSX0N>I6O1=CN~KI0x05+ilP_+ai@wRfyT`lc5tI?Yn|XIMxcY?Oy z*i4q}Kw8#jSn_Obf`c7YGj%SaIeEEeMlw?urZ?-e^w~CRSmV&fKqyleX|UvGX>C#3 zoE)=Br={e=1~;-AExG}NwE6l*2D8>`Y#mmLNc-4KHnTn|I@6M&4~#SG2M0C{j4tiZ zutgM#oLS0fl-o45w0Ee^k`U0Iu}%y`t=^if?c`GHN;ff3=28;e}f%GP1; zEw-Zu_Ad%`P~GBMqZm?BQu3*SgAJAf@c4KXVn2Q@=bDc{ZCRyLR9pQ>M+>rgjAMHR?_Mj5f$Os~u>w`bz$#y z%KZF}f*Gqu^;+JRQn zP(AEfX4)w?Pw&`z3W}vMT`d$kpe-IdHOK)*Eb0Br+@@Ia#a^ zWdVi~Whs!&xKy&7LsOA)c5fW+=p%0p%sD!^%T$=N*#dI?uLyL*{}sV#vf1=j+rQwn z4ikC(&!#~|*=-(y`6jE$Z9~eLm%H$nKe2K#%FL`>jQ6Kj4pP~9k7gT(Z$9qM4gIXjN7;>V&f&hitYiU6E$4B`A;ndqkYV9#&qTj68!upZi{q81~~f zKR3?ZC;9KYC~Awm9FT5tcFmfVvvVKlp>lWcpRwF`@Sm~X+r@uEEN>_OlSxu09J|!G zbh=Id1HmmvrT*Ijl#5r+5|oQq{vwov`rluMQYG)2ev|U_&xgjK+}ZvMn)_t`1?|1L z{v&32Q>8}4H8YzjORqO;bFBKz+D>Ysl@SkP({R^WZVp+k+0+kuu{9_i}-AMF*aMY)D`)_wl;Apk1SJWrrD)ab>Lv?OO;U5 zmZd7FDd-?;j$RM_ORaerHGCf!LAOo+&kSMHrf#~fS*s7YZS@b9(8;dT95$wOa$&}K zRyR5OZ`ejBLAEq4{_n7p&N;oOa{gtG|66TlM9XIXp~<9jQf77j*O*PwqKLe=bU6V3 z*V>O#U%@fui2r)d|65%K8ap^`C7WZjQkz+xhg4oHRPK!b714dt|BCqDy#EEays2Bw zrc5`xB(9oR>2#2qJ2yF^d^c9?!w$UjkhgT5eQJ}+J?~C)&^X+EF4}Ccdhby->+|(y z7r~ZgMky0xwhC<7#BH+TEdIxiLgTWIf*5W#`ycrTEy}tGE>h(=AOAn`9B78#jI@;x z@Gu~iPmUY)Y{(hse^ZZXm5KSP_IZulX({lbUCh4G^+_xf9V zVUA*SFik!ao)U)|MG_Vn-qHD^x`(nQhvL;bXInT$$olyV&Vd!#^At;RX)l$ zhcq=D{~_tt?$`Hqi#kqdJ}r88`1<(GK1t16wAeUpn!m2{pB{d*lB!1Q6B83dPM<%2 zxJs2O#ZrcefxIz1s5+~Ou9Aqv-|M{_jf)%dHU9OAKedk5nCF& z^|$;!{cU$vZ>@4HFztu)i!aJTUO#`}{WLJSXU}DEGddsaC4ISff4wtt-Fo-#Eq@yL z^v=03&z~?#FPQVupzl()H6gPm`5!K z|2==r?3OX(2JU%z`sQDA23M>Wi6O=C*EihfiCGy0vJ9sl{RsagBxe)-Du?C!=>V@&V1 zwtCtk=*-EsM~@!uVc)o`wU9V3epR)oH-E?E{kkr8tpED7uij2wowg_S_*d^<-ZcNZ zC2QctTZ2I;IyIaYni68|6yd#P$YTQ_OFaBXJ(|?9hPG|Vnm+4<#I@~&oRcIa<6Y7V_s>%qlIZBCAP^lr?%e(g#s-Mf2N{pzy= z0p~o#8{1XSXXTo&|2F8#iI9+x^NqG%+MB)mr6Kqeb)%ST2aV>s=kktyeXv8(wrP_m z`>yZ$T8C(lzVkj3(c%?!7T?lqPtxuzJtDH;*PpH4FWy#=eV}kI!gw;r5x87am1v(DuA;LYssB zp?&K=f%7u7uBm8o{Q5`p*N=Qs;?=6&{^n5~#usPp3?1;9chrl(&#q7Hc^=Nr-Km%S zUvH}to<8iw>cVr$Yi7OiTvYV^+Sz;FKKu2;&%?Pn7Z*nzd-g29Fzo0uzgDMAt!r7* z#`Rn>(7g2V0n6(|SdL@etM3fGnZNPDy`L*q6BqB$XSX)?J9XpMq6txB`qq4#^<=L{ zScTRTYJT}*!K{pNGqhp5p7_3oV>q&T*2TLQT4Y`P{(0-kpQoA*j+>F1@?@v)se;_Y zhaTr`X?<(knBElx$?|gP~ zz@$5Ux0rh`czn3`x1Lp-?{5`$;o$gLr~AEJ)3ZjaJF&VyCRG2Z$LVwaL%;bObV~c9 z+tP~zv^OJ9&1*Yv(eo=`wSBPN{&>QZ*_FmGn{uj3m4i20om)2HlP)i2Z2ozR>5{t5 ztd7@q#(%SZ<;lDT*P>&$=lrpEL=D}=ZKn@E?N)uot?Ibu=YK~>=l=e@&nu|QD}3~% zEkC*1o`mhkXSZs&w$;w(mp{8&x&4=Oe9l!0nL42B$j_!%zWn6LU&XryuF*ujv|QRu z-x;9A(<{Ttda+=2pRK9yu8s}s)U?Z%jAMUP`>kqX+kHzuo<8zL(+gu7RJ!=AwopC0 zPTKlEqYq6wFGOC7i~7@N&e?(24pzVZRY}T%LG>Tz&7XXu4|j0a^G(;roPA1m?Hlzj zd+DIjsk??IfB*U1yI*eK-{w@$&|d3)SB}?Q&+4{4;-lt~A)9`S^{TgR&YSp>`$dsy z3!ZcK*ROy2>8BqTzihdEly3BvJ@V{58H4*J_1*vF{Ax)rT3@L1++=^5aA>l8!_7Zt zSA1}F;oJG`A2&W2zf}L#{#I=lPM+|%&g}=uzT1Odepd16D~!;~h0n*gxV&xAqD9wg z@2XUAdEn95;!HY`oQ#_%=Ukoh=bkr@JYIv29|>}QxTkWvy?LcpFMj#rBgK>5X9I5! z=-)qoP3o>!nlC-;6gIuEIIY^po9?~+J!;JM%R}r##||2B_<4a@qL8|~R)+U`Yi+Vnbo_H6#pQ*+y-T-Y;U z`Qn$cLte~HhH$Aqu{WGs* zbo`WhZ@;~q{POMdD`R(`I5s`Zqp<1LNe@O38(0$mU_$+yLe_vu+^)oP-uD*;?0Gx+ z$xEN>&&PaMb$w0q-vMLpEkEb2o40554<*9k=TB#!>^Cs<{O^rZ+Pru>)pTT0aLu0D zTgz__={5PqZ}%GP-{tje(_7!CyLl>yCG*xjT)n#eS2f!X?_#Wes$RDv%~Cs0c>ZS1 zz%|VWwDF&}`^~Kz(+2cEvf@JRbKAB(-PBUOa{ne-v$WzLOX6P`FFyY0;P#8vZa++$ ze(8G0X5+8+&p5Gj@sN<@_&o#q2K4FHCN!#o1Zpq8QSpkr*+QDivE5zpm=A}vL$Sgt!<#^@>Bgs zE**N}L6u4e4J-SNytZMnHgDg^+uwRtsuFr2D=A^mfY~cg^q6pBS=fWkQ=hJUyib`s z+PeP2?e#C8T)*?p*_6b4-I}ZWC?5W`;KGJk=7F`$(NDL%c=AJJ-sM^oe%=<|yic3Z z-K+W!=wI+n{}oRsH3}VfqQ|LjhxGByV)}PqQ0uvu&x%i)2DMu=Fec;dwX2D``_(^x z(Cls2I{WXd4%a)DyvPS{`}RW)K$JUgXr3xhr%GwohxrOJ)FMsWRl0zwf7%P zy}jwlj)Sr5UvBRA_^-xiW^_LJD!*sF%)S+7t!(R;zO~VGxlg47H*P|>!W;`Bn>PBM5X-m+G z_0dD`d&M#gS}{Sz&@z!v14ndW_)_F{lS6_lFc`NPt+y7 zys(HZ%(+JAHkiL$@^1YK^T3nMza9Gd7n{t>Pd#gJCh3OXfUzFBjb{>XZ&UX)nU+sH zvc|>7&L1Xs|7z+q>FckrCe%6IbLP)KRoI^JYTBp=nPZDIb?g3db>aEGZ>Q--dx=H^2rZPms{rT{w*j=>malERpyd|&y&qxG`(C{c^ zP|aFhqtd*jwN-~Mi5|SrVTAv`{~7k?VcNd7mX3QWj5LXFv<8lQYu~+ zNHrP#^*WnZSG_IKrnTjXINou2W^*FDM_^@HTwXSdL521fi&?MD=1kc(J`WOU*SxF@ z7(!$q(`2PXO52-kS=t<~SC`I;E|xs2zLX=8MUO-lpNrKZatEzV%WLy&7S9eM5s@(o zaMe%mU#5;uM{9z>SRszuPjAVRcc(9cv-_F#lIYxA?GSlVt~T3BKeCW?vjCrp&Ja86 zbK|oO7C*6i<*<>41DcZf%E!UEx&$XHKRv}Rg!96JNj@c>WSQMU^lWVf+ zmEyMu?b+pugC83I%ab69GwIEl9^D)_i|w_!QgPmrM58uGFYTg*ofs?=A5A(4S%X7$ zmR!Aee0O%QuFs7zn_z)_KKR^0Dvqs49AL^8vyvDX8MuWCSahkCRdR!~%v`qKCKuU) zoF{BEtX{UBZKl*}%hhYM7GIGix(hKDIvtx{RazCNHJSC9-oURrf~TV_ zwkk~7CYwoXHVxIQ=!Hl=tdNr(A8lqP6Q<54t4$C4WmQ;pTC<*U&(muYr4h1Dbfwj3 z8RRT@XX;@cEJK{-fYO5#S?1vInINJb9qFME7rm03P(-@bOGP19z0Z_)u0G3>ua7pH zS+zKlv%kAgNev*Y@!0}>X!2}+<#5DXVXHmpes|O6TJ^aRJiWY8P%KdvJ=jgYnD-Jl zBRNMbs>EUk%b;wrLMc{=!l^2KRpdY~y##fTrZ--(;`Nz~<~!>RHjkWAF;_a|rJFWe zZ?2d{HGn1`#M!M@mgcz3lBYREM3&~bk`q`PNE4lZ641|8u+?Q*!GRvOd$F<$S!T0z>J%TJm?#IrJ=g*X4pC9W z)6cY`tKz=^rUyZL$&5=2hne2RPjviD2~>qGwO4zxJLh=FQ03%*d$h}eEDp! zKhhj+E>g4oUO{^5 zw7@~MRpd}*$22*PS*rj!N>pKXnFCLw$(98BD$ZN!C^4f^Nn5^E z@XFF_t$Dfn%ueE5oW-7Wf;>Iro*)|0I$NIBoCx~LL!2cO7(eHlyVr@k90y7t$XJ;! z_bOXgA)aA&E>nqz3TfhU%AHHy%a!Zx%6%&C&Znxh_oRt4+j5nlrC=iL@;$c-)v?i; z`PyuqKC|ptm7H?tGOVKAs`i0(N-H%>UhRk2^voVJ?j4pT&t@`P<*p`w*NT)uu3SE0 zFmD{_LrF3t0=Y<*^L5vyq9Wrmz~G8+6{Hz#_oZnti9Ef8rJf+%xi&Z({rbTHRa(Jr zISZbdjw8Yd`Iq`Fk#2sA%BhN-Y*j5sO*`BC-P+vz0hv>2O2wCy-tQey_ik>wUZA?Z zSC(0pr5(&t*{cYeQ;2VlXjVpD8=CQUdtJ(K#1}3Mr=RFb>g6mm#H_b^i@qkD1hFgyDsYyS zfY1SSdN%z~Q|Zp7gsFP}59}+e_A9fvqM+6Ir?nTGWgucq>SSF(mr;T7an?jSYpS`F zVL2yF5Km4o$FsNN`KgEaWSgLHsij_}+1{nuPaVpPGlO^z0lje z;6xLTqnCT3NeA9!ZPGy@*2P0snq|t(wdBgE=LZtj%bDrzr30rc&!*25d#cJ7n{@DX zmqbxYq~iM}3H1PSd1^d)>SXycwF3v4Ze$EYspMMPvYxHCDH{$U=$iQKOcQ8Yg~{5% zqRV53FV}(TXp%e2)__t!&`FIc4XZLbF)=nNF|B)Sbcfy^TDpl=@M<%MD7tly?wuCX zIX>oNIh}&eY|;(z=oX!v7%SE)n6JdDy}EVRbnG6Rm?*Y*I^~=-diKzCPVNGoIz@Nw z80+wwbf9AT>Al3Jn_=EqyyczQ5cS(ZapPjaqV%z#(JLwlAaF)6xxl6d-=K9wYPj_(kwWEBwP z8sb4_v;8EJo9%Tj$uMSY^B>mL60w`i|G6#dhzm>8^m8bl1bW zSZx)`1l*CLnhPphS$@!5vn*0PT`U$)=|#k<|3aji`#nSCd&bJowazq_k7Iw|z$eA_ zN>XIH%2JRK*g4U8&lMaCAWs^Sqi6HQdI=*g1z)e9&8Vd3H#sSedInaCO(R!@Xe+5L z@YdRF;77UwSSi~LE0>pRmcprpBPL--n+It&N(^R;1r(KDKfo)IDkqmG<7F*V^kKX1 zm^d?K9`t;siRVw5>DC~n=Q6spM;8rMCY@uWd&DZld>2iRSP5)Z?hpm@qM3>CU|G>p zdqsEX5ZfUw2}CL>x?`*i{ID3K$($(z=E2U}tlR+o5Z(k*!RtWK{9Ka^rQT2|jk6U7 z*ENlzRAI;*UY1G1?p}DRpFJ|@3bNhFRIq#!Xlb*;k`4A>OZQ8vV=7tEH8zPdl$4z4 z5!*E>wtHG!_vkLM3Ks8+b6{C<4qTQgTOvB_PWGd1JY5|>G1e^;|9XgZ49({Qx#Mjh z1Tr{RX8LslpOa@b!d{sTJXH`1X89R4Nm6{5*zR5|O>>?X2~$3}5l>I9L{#dc+d>bi zCoBW?aAWdvtzi86bE3^f21oV8-88pHG~4kzK^I8p%PW&!^f8?gynrD9ksQQWsg2E9(&0%`6|czfkI6$pX#dsmsfyA);8S z;57M>ZkELp^dBf$X279D-9J~U0`)k6oEU34I=rXl#E{9uVZYLvriDw`A@X$jSTx#_ zW--&nX)~;@@H(>mpr(S$3ydtacd{&p&m#r3kgL-}^kf8oS+30kPZ`wg5E+OQQ`bDr z@Ht?5c%#J%f^W&S$#P-ys8mBeIOdrw*0SUd;USFG%M1`2F$80}I*8br2r<{GqZ*PyMdV+cixtlu}2 zHo&jg$w{J5YTG(}!%{-}hBW}h296p^gpNh2umT;qPW@0ug4Yk$>tuP^dN6c3deXNc z?uP>f@WISlogUYdzGG_!PZ|@BBCE&Nkb{8OiqAG-#GG)#`xbv_bVV_-bmW=)q z9X)T*-Pp0Ual%jb?XWLAGtF=BABQe)EInh;+2d(8QeU!VH|9tEkLETFLj`M2_q3P=p-5%6h1 ziUb7=$Fh-FHUKXtf;&6ybnk9B_`7?&E;JvHwFH z@EFUU;DE72f@G73Y&m@%hNKd%(zwbgQpHZH6p|`Mq)IWVBKLm_EznlCN)P(1@+ssc zRZ=R2`S0PNxE5C#j;kEx7|wpk!9Z2$Xz;j?tL)^V`XR3J82e%#6Rhx5;_${19!;7y zYu=(|E3c1K;kdP|O_W%~GViu++F@C^Pqd>(<%ipIG2y-)oE4b!48R=~vEh|E#V61* zmVb=HJ6G>gqiZWHQPjoZ8ZIWNTgz4+pVY(Q-DPbOlUlZthlS(h`aK%-Y=!IZ-~~(s z!V8>Kz=D9~0#OLWL%>P_dkWY~z}^B@3B0d>{RC1$koXIHC4sLj@Kpr9svxZ{;2MIo zrXURz_*#NcN8o}4Sv`Rd7UT^CE<}(u6nM2DZ6ruT1x2`k8w-4dpll-Grh>AWplmKE zTL{XQf@h?l{7CR@EhyUvUTpoS^I|C_4$tctM#UC_fgI zodsnVLD^Nn8bR4jP<|pPy9+o;kR=PMo&xSA;NAl6BjCP*UlYNvnSfITE=|Dcf?spN zuZ7^J75p*;Kb_#0DfsCHKZD@cPw+DeekQ@Mzu-4O@G}d3S%P1-;Aav1as;A;(*f5pka#hm;^`06Y%^9Kk~1QGjERKMv22g}@W=I0kppKLD-*Tm$$M;5xt!fSUlf z0R95F4R8nGE--cv3-x*m+ygAse}sjQrvT3Yo&&r9cnR(4EfQ`AblL_%#tC-vMR`z%Wk0!vVeq7zt1S@D0GX0HXj#1B?L}3os5~JSXtqaYFbc zuBt>nfs@E50wha?Hj_CyoL8X@-PtLC2#1?|56`Ip(*UM(LYp{wJy5+fIQe%(CZ9lL zaJHaj7Qk$PIRJA3<^jwHSOBmPU=hG#fF%G+IU!~pz zaq?(6Fbli3gA-zR0_+0V&B=$siI4jkDE$Jk4`4r6RRQOajtSUD;du-IdSFmf_#`J^ z1A1aN_K@#^=ROSP;u4H~nX3v`;|k=i0bJ+g*EuNv1-)(q+yS@?^SuXfAK(GN-%$4u z;1MTpD3$wr$_uev9wM*mCGd}-`UxkWhrQ(KV1=G?Ue7o$`E!8rP&ESJ1>~O+FCpd? z%q?Ar0o4=(YANP56yI?2x6u3!U>NTu|Ba#zE_vf#OVn{`5JCmrth8$-I0DeNN%! zdtppGotICe5_o;Xd3n+2T;Sn57%yf4@VJl{ z!j}QOfJI#e&(*yA2=S6%qjT5*?OOqkWBDBDw+;Huh39%Y=5}8G6O`8h>;RYt_45JJ zvNWDQwz2&)5`2(=|Rdxa0Jpe!RRaHXxe&Fm7FZ4JJa0K8e00>f#W4!zk_K|M@ zaftz;h?xv~bR2L`0G#CIL2`LGw}Uvp%E6e>1R)t#Drtc@3+xje2Vl&7kd$rbkpjyw z1}6#%Qkn>^X5vJl1>OO!9|A=fP@!;ENH8yxE96MVDHIsM9a-erIj9E=O1KZo?mtRe zuXM|}mE>YS3F1AyluCEP-ZW9Ua^7WTOG<0YG-2L@CQ6FnQI;k8lsQ=~=Z8Z+yc{Vw zr80mn1n<_wmo8n|A7A<}hqxnD5GygJEpq1sKUjeBGf4R#;<^a4orVNeq!~z%Kh5~N zvs8&@D!Jj{fBfnB{u(!A1vXW3D3XR<{`?I{o!yLg73@NB;KompH!{n;cTs4|hr%4T()TBuuBwQ_CLi3OiERnEfLUPz5qEL8v z5TzGTAeahI>EKEQC={VE6pm116oF6^rLrjyRw$Z5>fzCxr7b}Cc&;Voz7^=eNQ}6TKvr7A zUbX=ljKWCO7Nd&oF!GPas8V~30%9<#+ySF19FcpY>NuA25?4>ILbbV$l-EwRP+&Yp zwW`I*eNaP^fKl+r7=<*c2Fjp$NN37u7n*cMNQ1gjQTl}O-JS8Bh){KuM3ZEi^q@&k zgaT17n(IxIPZ6qt`p~2=LN!qeLe)?zU9vRFN;=F1b6S>WuvCXorc$XVC{)j(Fats1 z1`aju$DxRR1T`^osHq8~X8kd0I{>40W{je=MW?zLqD5@n}1GFpDzgN`z1js zUven*D}vI7ff7jv&kt#bLoQ=D7>$7#m7uYZa%dc$1^MxKHl*L-InX)*&xLd%o`-GnO5A+B051fZ z^%h|&6^kKVg3(eqc~c?Ese`8BWssi^X&v+f{t@ysAgzOD;^mN^1!)~L8)985!YiR> z4#d25&|HXkYomFP))Kw1Ya#A_hG2+}$*)LO_dfwT^cw+`~lAgzOb#Oooy z9MU>y1>OMpm5|m!s~`#oas3J7O)!_$kaB1ZMB)Ukg;<=Rbr6kXv>sw`j5grykZ#0) zj5gsNkZ#61A>D#^!Mx>_(RPe><2_hj72N!t82t>vm+?7Bui*2L{*Es|`Uk!U=~etIq}T8zNdLsYL3$luhV%x$0_jcsJEXVp zACUfquR?koUxV}x{u9!>_&TKbaOMpxuZRA|+)eCIFAp*3mq+*(UCChdly;WL8=yCs zN{$3ABlT~?qLkpw5;U8BNWMhkqbGlKmk3Ae{URinW2@ znUujz1ka)jZXtL!WpEq8XD~XC(XSXiz~~7^FR;W(cMgmQzR6r5{1VRt!msdrAp9D? z!)Q62Yl5m&#dX1ls)0EkTmc*h?v5H);}O>xH{lVl!B!qg60wa((%v|qN5W)`hww-S z3Sl02lX6f4yJ#@A3&SLM_{X}pOj{@La zV=R+%_HeMy;DN{$FR?v5aM(g}CUDpya@L-{3mb#&=|5v*1A9D5Urf@M5W`ZUT}B`u zto@Nb(w7s%3Zh*}(pM3~YGPPJ?1qpy7$`r)7hL=XZq*H0weJ{&QWlbgg>V94q(Z|w zl1>ckp}m!+6Jf;-#IO;{k+ht$sg$x|Gf9B$(?sKxMI>Pn5CATV(zg)9R-)ZTiX3!4 zf__C#BCTnm43X_lA}Sd|c1X&w0P6GR;881P)yR>gS60sx8XENkAVW9s04{Z`90^{=D>n1 z0_iYG7AHP<@gZSNQYHs_>=HK-A^jI;%^4CmjbO1i)PlHh>W6eS+T+ zN)sxnAVDBGFk{16I&Z@{lF&qAjIbLb(mBIe(oTRS5DVi-JDIC6p0ty@3g3}-3RhtQ zY3Jc8OeF1;uEHeJ&eK(xOxk%l3dC@pwDT4Vc0)_8o;_Q@mf8)?d?5savc`g_q|sb9 zO1VH$H5aJRNH4;{;br(&(9=A+1eQU94ni0#LBGLqlAy~3NkLEY5MBDFN2InaG+ZGe zXN@h3WJIfnz4fIcRIAYNJ2Cu0)UQYdknq&z^^DEaLqV!QF4H44CVD3PC}N{R!&Tzz z1BW0={f59%N{P|LAe9%ZphqkH8qtgZk#8DoYzmz!P~i$rX{Tw2(wqBKg`*OY9%}fL zXbmiLoj`JfEDFk8sO<;n6*;0%Mp8LDLr-QIFS3wk2e;(A-MNs1P&%miR+y<+@Cd5a=^h48prXJwH(=5OLU{dPkPEmBF#MJW$i<{_xaKw|_DsY<|d*0qQ0 zQMx2N{Ui=g_YF^fM8eZ6gs0bo-(ILX`!Uu94IL)Ja6Wlo%(G)X7Bs zBQZ{|OmqZ8WIYR6RSi%}DSyL?bSyMHA%>@}+%rO# zY%eY>s(7iS#NGoLI|7y31Qy%_#-U~KBcQ4dUA}tu@RaA|AJ&gJ)-MjQgD?;fd9KT0 z4-ZxP3j)7}mn00&*S@5j)+10~2`YmyRHVNmn%8g)Uy(rVE7}|^;XFL8P7Pjbgs8!9 zV5)>cU8lbx>}=?OnSpZ|0+ZnLOgb`}l(>sd6!Sm7asePvs!hAowcz?P2S%H7g9`0d42cWi4l=+TJw z7q>MOV-F=IWk~OYP#8}KT4b~^0>Rx!Nf8gxU5SIhn|238Ba94jV z^lvQXJe0$!7CZ|iw06U%)Q^FOy%4%U4N&Dp-dVe$tNJun&%j0MnV43t#BhwxVY*cjQkY2$n@Prdq(dl$uIe+i zCv}OaBLv=#W3&%RHE~Tb1E$y%1L0b&tE5{uD?zQ99B;!v!~y}#?JQiW7yaQb{S^T z&7PtuDo)T4V^`qbUYNmWNHk|LZ`brkDfJTSp<(qxbh3KdZ@COvuyz!rhS6LQ3ek+= zh%?XnjOBP%){Ns&hKw;}Hw>T_?LHu;kLRXPoe$i#pqJ~?)YN*$R4BD{l!95Qm6Rxz z^-{j$QaI+6FxPSdmrj?ET2;EH2^`UU$I*F4@_=2@iTxe*LQ5Mz;-E1)y_;HAAsB2z zaOtAy(pmm0>1TUxan0#aXT*?YUg+nQS*C`+8E|k{R)KA2|_LhzLFGh8`k94-uiY6XA_G zyIyufFA<@ah|tD~uu`PZ+ivJBBJ>s!qMQh;M1)VNdXh8sq+ZQY6+|1?h>d-yrjm=A zYAwff9hm~EScbmm5RsvFgovlxkZ1rKMllODW~`F13_IP?9H0aM3iX^FA~V)aWZJvh zEw1))ZtXE(law{-5J|pFvm{X|rrXX+iB!G;t-}Xs!DZ0uiDUg3TUXsf~!8XrYtMw8mRAhgz3#iAzG#6{x~DjxXT$c<2M z;^x!fYCc0YgOT>COfj9fBpSbmBI$&r(_p)lXgX|{68*p-PKjo4^flc~4#Isji$hWw z>PQVV&BhO!-*zG1c(#bhQS#UqTqVfp}KvP6_D( zD_3Ms>PgTCXz@U!PUQ`ibUnHzIAa!jN@iWVcMwj;&nk8$dfbksWnFTa=9xg3R>y}sWUh!xPH`5;mVoX$m6CWabuO3w7q-XECD=HZ zqg!VW&*15mk50nwe5%VN9dX?zE#`512sa-V%}1$R08+sR;DsO-a)qKh!Ga8!Vsz#a z3@0HG_>@)$J{P%9(-VGsN!hGyz-5wiR=u6GS_rcejQLRYDSf3(+a_aY;{s@u8HYgK z6ii2aZ_eLCtRj==P;X!{r{f_OD30gSzbAne8q!#aGc4ht0Tx8Pmy27SwT;WgL#HkG0O(ZMj%h@Qx|V}E_z8=#>r~)ICa+F6 zd@&BJKy?|v6z8!^I;b(+#(A(J(=Z`2xtd}v#f25vg_jGzP_h)OJVpFM?QLw=?8H=B zrk6u{nH%XljPwtsq?eULdYK#PJ1){Q$|3!u8|k}@^vqJyKbAxKM>o=UU8HAW2`7^$ z(VYl5Bcjt&!Y{|2(x(zJcAZK>&Z>{lH7#Pp-Gd{x0;jLw3@f>4PDY?-AvFty2D+4v z?v$h1m;;VJrW`FVhoj|KrFL>eX)XrXf`gEs2lI%x;6-^V4k9y@r+wmLc+NjCyz+m< z@G>XED;dLc8N;0@!z;^ScqLXfpc&%|2e+F`x!vr>?Nb-G^UB$|Rc<@?kPSP(bmvx; zGwdq2oqOmay`UV@tKCRHVx$+Al3ra7>D6weAGt^`Du?tMH`0$8>BXg_*OWtgjT`C5 zF49ZNA-(nk(o0K8uPul4+Hy%RD~I$tH_}fS=^xn^x1(6=$|1cDt7wS$v74avUO~c&Pt~s1bx?))3)5vwjS(+Eut@K@3bjkIy-2%^8nun3wZ`R8Yg|cdH-Vv( z$z{}~71w1m<^io3i#F{{;IJhaqV`3w`d!^j+L=Q2anYht2PLIGD~jgKxCm1PRsm*L zs6HVg`GMJ+N+{Y1m(`m}7+F695J*^r5gn+A3Ny%S5tT9!q#`N-u*pSKp@7aPqNg7Y zco98MaAb<8VFTM$M6D1Qnj&g4z$z3`MNMU#>OU%fMX*vZxh+ioS~%p-uc1TiUShVk z{0fd0OR#n=M=z>fHSe%?CA~V?N*x2CHaG_B-t#eR@Y_o+dQbD{P}^|n<>7WrFXddf zKR;0_vid+}{fAWMVG6wz4o_?srL_aoOVRgykDc^;QmKpRE=rUKc~9BEi0;7@dU*hi z-IT^|hA#562l7(E)%9oERZ?LsgFpv@TDNb$Y z)PeHP145vQJsn2oh^PKtur@lzv*r+H! zna`xv|6HJx+S!z?5F%~mFdguBPW=bho~W;Kaoadj0HXUR7q^`beXiK>3-JMCmyF0CIPt& zNrtrIrjn8(;B-GIP>yd1`pAdC$>_u)ILh8lSPwO(tkKdfR|)zYfHmhOu6;U4_lZ+E zuc{@zJgXajp?h-(mtOiF#ynPdG=)9^%ndX0>|3@5~H*o&RFJUcjSJODKZa8RZf z4Hbei9c38s2?r1dr@Cw&_LR7UIAq+59hrUD&faR-4Tl`Q?J;1&!=qa2LF~MuD$D&r zO7b`sI1kU!t{08KDUCJggA*UT_;7~5&ed3s>3b8P!|)mRE7^SDvDz2p&xyFw*|@;j zL3E33?jw}#zt+{?z4pWY5_?L;AR0DH9INNxj7CTc7SQ^Hio9_@qfV~}>65l;#jT`4 z6npElSG?wFcTBwZ+2=ss$KH7|WR{gM2l@qqcMr4|f_D#eg2_N6H8e*s zDl?qxMUrDY_9hAFgRoHQ@DStJlT126v`b$*9i`kkWo`yk9K;T>qg{jThW%KxkE2p` ziggaBsvl_X=TI3UFr<^pYWF6-I;Z2s0W07hC`map;i5y%4=S3n_i5wIfPf?3*r!;>9 zH%}sEcWI|_A^m1Xyg-D4;RrWE1-dqH1ZcL)nD>y%#}alwArW5%(T@;@dAYtqa9&o> zj|k}5HiORK497U_adv;f8H+G|@SL&%V8k8ssx3Yk1YCioPWl*nW8B&~s`xa!--I@HgMFR84dSlAG;5!9_Nd5u-l+>e*(}D%lg=9-=cT54|h%EBK_fQP8x{OV*6IRu%Lodn5 zH8#gTVUBWi6-y=J+`tnG999+lq5vIlSC8a_B9gJakS?3JWM@+Xz+4p>u5(3(8ywiF z`ShJ9tGWqQw_H^T0ff>>shYtT8UErDsuCb;FD{}hTul3ewJD~Zz|<7O+^%6xCXtQ@ zkr5ZxOD9RGmQW21t9Di#f0zWEuiGrTvKJLoGF0XZodQq4t#^n!9M|_{y%KcFkT5w1 zw=B3a5Gl&wsg|MgyS$Q?Qf7+m>Th^(NkJ~O zcQ`1Ga+dDW(imsy9xaV?mhN-!k5X)Szy()(GZw?Lnn>(kTz^fPg^AsweY79_lH4uzD{^f5;ggaq4M&aIzGHlDB8@?%M{A zDmbT)IqegUW}b4|XDsua)4pJtmz?$$%e)4jNKvu&4OaxEx19DJSCn4D8HVxN;e1j0 z2;T5DuN}$LMsXUD5myT`H#qHbwrn>*QYF%EPJya<&be%7Q)(vEM6^N_1uE-m5O>h| zAm^-f21+MrsXQ^c$@U||NjT7ra9~0pq7@Fp#o!Rw3k?Ol>)<=8_tENa%2t;*)Gf@x zHhcuTP%EJp8deJywV0X+SI1!zkTiM>v^T(gr^2tj#o5{WS0D*z7SZpcWm5Us(y!a7 z(h-daCq8(`5vDCWCb& zYk1!MP_Z)PVLbZT+bQw0H<1kfe@lj9s^{@ctHq7wk$NVM`x6tbsH4^M-CAapX_u!%g>GZ1LL$8_rPktHU;!w!0-b~gn^G2RQa7Lw?7zyN6U}~&f^&t%Kp!ww!!eC$ z)#FHoTA&nYL?bc`nzc~WxeM4=T-Q0uTTzNKgO5|P;$ed1MGkC7^#WSIjxdUg_)?1V zToiAWQoLD~A{6sQiu1})g!)ukU(9TisyoX&J3&RXSm6OfF6Pxscp96=d9bE>j1yQ3 z8o4gz+hb75-|^VDG9A_o~ zPejj-qv)*yRRsd?D5_Mgpg7-&>_g8nh=8N$tEvjcy~I)Uqt}V_>(QVJ)O8N3Ad^?! z{jQ`!`?n>Zzb*OxZOO^EB{$gbBj+#ouB67hl61FUo7?ZOcO}rDy1CchFgLeIJ&7Gn z`XP2v5CVZNQco^Z1D*#=;11@&gUF^wPA8$hK@s%}G!DO@22^J74&6s>)e$aTw$Yc~ z&VXty_Jv6mf-p`7VN|lLh(m?qy)yL}R)`)$Aq;z$`MNVWr>|T3D!tU(1!d(>dg^p~ z2JPw%yhBql)veyhi|@fV@gt~r*hzfp&J+zezOO?Npzr{cH&?B=l2Z7 zf8{^_!m$rV*Y=JtUTTMmP^&dGSET-hpMSO}Wex$C(*WtuVA3Su?#&_e;LZm<^a?yphn%V2qf#^q`HtEof~@5OX}?EEEutJ+s8Oy5qwi zUcHxMOVs=LxRpGfuRg%fpI_k68zhivGm2I~-@P#WgFO90h|Di2aD0M9ITzQ!%mr5P zD=AT_-V2Xik#XJmWHDLA?;(=aJi@+c4Nn!uT2L6iXdNgF9<2w3;g4R^*BX3Vb`wyT z&TnMjTBY;f7M!KuTK$I`4m}CC1c3JO3-^cLcZGFY+#=ofWy~ECLP<|xzI};^+ z@xll8@;5G_mCH%#r@ku)$BAEx#UsZz&Gtg$Fk(-+frBCvZeVO2PQ`R3pf8m9BIc!{6w4|{QLwP5nv3WpNINQWdDu_My3A-dg%NAz&|4B-~J;6 b==_iWodI83J<=N literal 0 HcmV?d00001 diff --git a/js/tiny_mce/plugins/nonbreaking/editor_plugin.js b/js/tiny_mce/plugins/nonbreaking/editor_plugin.js index f2dbbff2bb..687f548669 100644 --- a/js/tiny_mce/plugins/nonbreaking/editor_plugin.js +++ b/js/tiny_mce/plugins/nonbreaking/editor_plugin.js @@ -1 +1 @@ -(function(){tinymce.create("tinymce.plugins.Nonbreaking",{init:function(a,b){var c=this;c.editor=a;a.addCommand("mceNonBreaking",function(){a.execCommand("mceInsertContent",false,(a.plugins.visualchars&&a.plugins.visualchars.state)?'·':" ")});a.addButton("nonbreaking",{title:"nonbreaking.nonbreaking_desc",cmd:"mceNonBreaking"});if(a.getParam("nonbreaking_force_tab")){a.onKeyDown.add(function(d,f){if(tinymce.isIE&&f.keyCode==9){d.execCommand("mceNonBreaking");d.execCommand("mceNonBreaking");d.execCommand("mceNonBreaking");tinymce.dom.Event.cancel(f)}})}},getInfo:function(){return{longname:"Nonbreaking space",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",infourl:"http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/nonbreaking",version:tinymce.majorVersion+"."+tinymce.minorVersion}}});tinymce.PluginManager.add("nonbreaking",tinymce.plugins.Nonbreaking)})(); \ No newline at end of file +(function(){tinymce.create("tinymce.plugins.Nonbreaking",{init:function(a,b){var c=this;c.editor=a;a.addCommand("mceNonBreaking",function(){a.execCommand("mceInsertContent",false,(a.plugins.visualchars&&a.plugins.visualchars.state)?' ':" ")});a.addButton("nonbreaking",{title:"nonbreaking.nonbreaking_desc",cmd:"mceNonBreaking"});if(a.getParam("nonbreaking_force_tab")){a.onKeyDown.add(function(d,f){if(f.keyCode==9){f.preventDefault();d.execCommand("mceNonBreaking");d.execCommand("mceNonBreaking");d.execCommand("mceNonBreaking")}})}},getInfo:function(){return{longname:"Nonbreaking space",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",infourl:"http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/nonbreaking",version:tinymce.majorVersion+"."+tinymce.minorVersion}}});tinymce.PluginManager.add("nonbreaking",tinymce.plugins.Nonbreaking)})(); \ No newline at end of file diff --git a/js/tiny_mce/plugins/nonbreaking/editor_plugin_src.js b/js/tiny_mce/plugins/nonbreaking/editor_plugin_src.js index e3b078bfae..d492fbefe4 100644 --- a/js/tiny_mce/plugins/nonbreaking/editor_plugin_src.js +++ b/js/tiny_mce/plugins/nonbreaking/editor_plugin_src.js @@ -17,7 +17,7 @@ // Register commands ed.addCommand('mceNonBreaking', function() { - ed.execCommand('mceInsertContent', false, (ed.plugins.visualchars && ed.plugins.visualchars.state) ? '·' : ' '); + ed.execCommand('mceInsertContent', false, (ed.plugins.visualchars && ed.plugins.visualchars.state) ? ' ' : ' '); }); // Register buttons @@ -25,11 +25,12 @@ if (ed.getParam('nonbreaking_force_tab')) { ed.onKeyDown.add(function(ed, e) { - if (tinymce.isIE && e.keyCode == 9) { + if (e.keyCode == 9) { + e.preventDefault(); + ed.execCommand('mceNonBreaking'); ed.execCommand('mceNonBreaking'); ed.execCommand('mceNonBreaking'); - tinymce.dom.Event.cancel(e); } }); } diff --git a/js/tiny_mce/plugins/noneditable/editor_plugin.js b/js/tiny_mce/plugins/noneditable/editor_plugin.js index 9945cd8580..2d60138eec 100644 --- a/js/tiny_mce/plugins/noneditable/editor_plugin.js +++ b/js/tiny_mce/plugins/noneditable/editor_plugin.js @@ -1 +1 @@ -(function(){var a=tinymce.dom.Event;tinymce.create("tinymce.plugins.NonEditablePlugin",{init:function(d,e){var f=this,c,b;f.editor=d;c=d.getParam("noneditable_editable_class","mceEditable");b=d.getParam("noneditable_noneditable_class","mceNonEditable");d.onNodeChange.addToTop(function(h,g,k){var j,i;j=h.dom.getParent(h.selection.getStart(),function(l){return h.dom.hasClass(l,b)});i=h.dom.getParent(h.selection.getEnd(),function(l){return h.dom.hasClass(l,b)});if(j||i){f._setDisabled(1);return false}else{f._setDisabled(0)}})},getInfo:function(){return{longname:"Non editable elements",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",infourl:"http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/noneditable",version:tinymce.majorVersion+"."+tinymce.minorVersion}},_block:function(c,d){var b=d.keyCode;if((b>32&&b<41)||(b>111&&b<124)){return}return a.cancel(d)},_setDisabled:function(d){var c=this,b=c.editor;tinymce.each(b.controlManager.controls,function(e){e.setDisabled(d)});if(d!==c.disabled){if(d){b.onKeyDown.addToTop(c._block);b.onKeyPress.addToTop(c._block);b.onKeyUp.addToTop(c._block);b.onPaste.addToTop(c._block)}else{b.onKeyDown.remove(c._block);b.onKeyPress.remove(c._block);b.onKeyUp.remove(c._block);b.onPaste.remove(c._block)}c.disabled=d}}});tinymce.PluginManager.add("noneditable",tinymce.plugins.NonEditablePlugin)})(); \ No newline at end of file +(function(){var a=tinymce.dom.Event;tinymce.create("tinymce.plugins.NonEditablePlugin",{init:function(d,e){var f=this,c,b,g;f.editor=d;c=d.getParam("noneditable_editable_class","mceEditable");b=d.getParam("noneditable_noneditable_class","mceNonEditable");d.onNodeChange.addToTop(function(i,h,l){var k,j;k=i.dom.getParent(i.selection.getStart(),function(m){return i.dom.hasClass(m,b)});j=i.dom.getParent(i.selection.getEnd(),function(m){return i.dom.hasClass(m,b)});if(k||j){g=1;f._setDisabled(1);return false}else{if(g==1){f._setDisabled(0);g=0}}})},getInfo:function(){return{longname:"Non editable elements",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",infourl:"http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/noneditable",version:tinymce.majorVersion+"."+tinymce.minorVersion}},_block:function(c,d){var b=d.keyCode;if((b>32&&b<41)||(b>111&&b<124)){return}return a.cancel(d)},_setDisabled:function(d){var c=this,b=c.editor;tinymce.each(b.controlManager.controls,function(e){e.setDisabled(d)});if(d!==c.disabled){if(d){b.onKeyDown.addToTop(c._block);b.onKeyPress.addToTop(c._block);b.onKeyUp.addToTop(c._block);b.onPaste.addToTop(c._block);b.onContextMenu.addToTop(c._block)}else{b.onKeyDown.remove(c._block);b.onKeyPress.remove(c._block);b.onKeyUp.remove(c._block);b.onPaste.remove(c._block);b.onContextMenu.remove(c._block)}c.disabled=d}}});tinymce.PluginManager.add("noneditable",tinymce.plugins.NonEditablePlugin)})(); \ No newline at end of file diff --git a/js/tiny_mce/plugins/noneditable/editor_plugin_src.js b/js/tiny_mce/plugins/noneditable/editor_plugin_src.js index 656c971b8c..916dce29cf 100644 --- a/js/tiny_mce/plugins/noneditable/editor_plugin_src.js +++ b/js/tiny_mce/plugins/noneditable/editor_plugin_src.js @@ -13,7 +13,7 @@ tinymce.create('tinymce.plugins.NonEditablePlugin', { init : function(ed, url) { - var t = this, editClass, nonEditClass; + var t = this, editClass, nonEditClass, state; t.editor = ed; editClass = ed.getParam("noneditable_editable_class", "mceEditable"); @@ -33,10 +33,13 @@ // Block or unblock if (sc || ec) { + state = 1; t._setDisabled(1); return false; - } else + } else if (state == 1) { t._setDisabled(0); + state = 0; + } }); }, @@ -73,11 +76,13 @@ ed.onKeyPress.addToTop(t._block); ed.onKeyUp.addToTop(t._block); ed.onPaste.addToTop(t._block); + ed.onContextMenu.addToTop(t._block); } else { ed.onKeyDown.remove(t._block); ed.onKeyPress.remove(t._block); ed.onKeyUp.remove(t._block); ed.onPaste.remove(t._block); + ed.onContextMenu.remove(t._block); } t.disabled = s; diff --git a/js/tiny_mce/plugins/pagebreak/editor_plugin.js b/js/tiny_mce/plugins/pagebreak/editor_plugin.js index a212f69633..35085e8adc 100644 --- a/js/tiny_mce/plugins/pagebreak/editor_plugin.js +++ b/js/tiny_mce/plugins/pagebreak/editor_plugin.js @@ -1 +1 @@ -(function(){tinymce.create("tinymce.plugins.PageBreakPlugin",{init:function(b,d){var f='',a="mcePageBreak",c=b.getParam("pagebreak_separator",""),e;e=new RegExp(c.replace(/[\?\.\*\[\]\(\)\{\}\+\^\$\:]/g,function(g){return"\\"+g}),"g");b.addCommand("mcePageBreak",function(){b.execCommand("mceInsertContent",0,f)});b.addButton("pagebreak",{title:"pagebreak.desc",cmd:a});b.onInit.add(function(){if(b.settings.content_css!==false){b.dom.loadCSS(d+"/css/content.css")}if(b.theme.onResolveName){b.theme.onResolveName.add(function(g,h){if(h.node.nodeName=="IMG"&&b.dom.hasClass(h.node,a)){h.name="pagebreak"}})}});b.onClick.add(function(g,h){h=h.target;if(h.nodeName==="IMG"&&g.dom.hasClass(h,a)){g.selection.select(h)}});b.onNodeChange.add(function(h,g,i){g.setActive("pagebreak",i.nodeName==="IMG"&&h.dom.hasClass(i,a))});b.onBeforeSetContent.add(function(g,h){h.content=h.content.replace(e,f)});b.onPostProcess.add(function(g,h){if(h.get){h.content=h.content.replace(/]+>/g,function(i){if(i.indexOf('class="mcePageBreak')!==-1){i=c}return i})}})},getInfo:function(){return{longname:"PageBreak",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",infourl:"http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/pagebreak",version:tinymce.majorVersion+"."+tinymce.minorVersion}}});tinymce.PluginManager.add("pagebreak",tinymce.plugins.PageBreakPlugin)})(); \ No newline at end of file +(function(){tinymce.create("tinymce.plugins.PageBreakPlugin",{init:function(b,d){var f='',a="mcePageBreak",c=b.getParam("pagebreak_separator",""),e;e=new RegExp(c.replace(/[\?\.\*\[\]\(\)\{\}\+\^\$\:]/g,function(g){return"\\"+g}),"g");b.addCommand("mcePageBreak",function(){b.execCommand("mceInsertContent",0,f)});b.addButton("pagebreak",{title:"pagebreak.desc",cmd:a});b.onInit.add(function(){if(b.theme.onResolveName){b.theme.onResolveName.add(function(g,h){if(h.node.nodeName=="IMG"&&b.dom.hasClass(h.node,a)){h.name="pagebreak"}})}});b.onClick.add(function(g,h){h=h.target;if(h.nodeName==="IMG"&&g.dom.hasClass(h,a)){g.selection.select(h)}});b.onNodeChange.add(function(h,g,i){g.setActive("pagebreak",i.nodeName==="IMG"&&h.dom.hasClass(i,a))});b.onBeforeSetContent.add(function(g,h){h.content=h.content.replace(e,f)});b.onPostProcess.add(function(g,h){if(h.get){h.content=h.content.replace(/]+>/g,function(i){if(i.indexOf('class="mcePageBreak')!==-1){i=c}return i})}})},getInfo:function(){return{longname:"PageBreak",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",infourl:"http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/pagebreak",version:tinymce.majorVersion+"."+tinymce.minorVersion}}});tinymce.PluginManager.add("pagebreak",tinymce.plugins.PageBreakPlugin)})(); \ No newline at end of file diff --git a/js/tiny_mce/plugins/pagebreak/editor_plugin_src.js b/js/tiny_mce/plugins/pagebreak/editor_plugin_src.js index 4e1eb0a7aa..a094c19162 100644 --- a/js/tiny_mce/plugins/pagebreak/editor_plugin_src.js +++ b/js/tiny_mce/plugins/pagebreak/editor_plugin_src.js @@ -11,7 +11,7 @@ (function() { tinymce.create('tinymce.plugins.PageBreakPlugin', { init : function(ed, url) { - var pb = '', cls = 'mcePageBreak', sep = ed.getParam('pagebreak_separator', ''), pbRE; + var pb = '', cls = 'mcePageBreak', sep = ed.getParam('pagebreak_separator', ''), pbRE; pbRE = new RegExp(sep.replace(/[\?\.\*\[\]\(\)\{\}\+\^\$\:]/g, function(a) {return '\\' + a;}), 'g'); @@ -24,9 +24,6 @@ ed.addButton('pagebreak', {title : 'pagebreak.desc', cmd : cls}); ed.onInit.add(function() { - if (ed.settings.content_css !== false) - ed.dom.loadCSS(url + "/css/content.css"); - if (ed.theme.onResolveName) { ed.theme.onResolveName.add(function(th, o) { if (o.node.nodeName == 'IMG' && ed.dom.hasClass(o.node, cls)) diff --git a/js/tiny_mce/plugins/paste/editor_plugin.js b/js/tiny_mce/plugins/paste/editor_plugin.js index 3e7b2504f1..e47a5c630a 100644 --- a/js/tiny_mce/plugins/paste/editor_plugin.js +++ b/js/tiny_mce/plugins/paste/editor_plugin.js @@ -1 +1 @@ -(function(){var c=tinymce.each,d=null,a={paste_auto_cleanup_on_paste:true,paste_block_drop:false,paste_retain_style_properties:"none",paste_strip_class_attributes:"mso",paste_remove_spans:false,paste_remove_styles:false,paste_remove_styles_if_webkit:true,paste_convert_middot_lists:true,paste_convert_headers_to_strong:false,paste_dialog_width:"450",paste_dialog_height:"400",paste_text_use_dialog:false,paste_text_sticky:false,paste_text_notifyalways:false,paste_text_linebreaktype:"p",paste_text_replacements:[[/\u2026/g,"..."],[/[\x93\x94\u201c\u201d]/g,'"'],[/[\x60\x91\x92\u2018\u2019]/g,"'"]]};function b(e,f){return e.getParam(f,a[f])}tinymce.create("tinymce.plugins.PastePlugin",{init:function(e,f){var g=this;g.editor=e;g.url=f;g.onPreProcess=new tinymce.util.Dispatcher(g);g.onPostProcess=new tinymce.util.Dispatcher(g);g.onPreProcess.add(g._preProcess);g.onPostProcess.add(g._postProcess);g.onPreProcess.add(function(j,k){e.execCallback("paste_preprocess",j,k)});g.onPostProcess.add(function(j,k){e.execCallback("paste_postprocess",j,k)});e.pasteAsPlainText=false;function i(l,j){var k=e.dom;g.onPreProcess.dispatch(g,l);l.node=k.create("div",0,l.content);g.onPostProcess.dispatch(g,l);l.content=e.serializer.serialize(l.node,{getInner:1});if((!j)&&(e.pasteAsPlainText)){g._insertPlainText(e,k,l.content);if(!b(e,"paste_text_sticky")){e.pasteAsPlainText=false;e.controlManager.setActive("pastetext",false)}}else{if(/<(p|h[1-6]|ul|ol)/.test(l.content)){g._insertBlockContent(e,k,l.content)}else{g._insert(l.content)}}}e.addCommand("mceInsertClipboardContent",function(j,k){i(k,true)});if(!b(e,"paste_text_use_dialog")){e.addCommand("mcePasteText",function(k,j){var l=tinymce.util.Cookie;e.pasteAsPlainText=!e.pasteAsPlainText;e.controlManager.setActive("pastetext",e.pasteAsPlainText);if((e.pasteAsPlainText)&&(!l.get("tinymcePasteText"))){if(b(e,"paste_text_sticky")){e.windowManager.alert(e.translate("paste.plaintext_mode_sticky"))}else{e.windowManager.alert(e.translate("paste.plaintext_mode_sticky"))}if(!b(e,"paste_text_notifyalways")){l.set("tinymcePasteText","1",new Date(new Date().getFullYear()+1,12,31))}}})}e.addButton("pastetext",{title:"paste.paste_text_desc",cmd:"mcePasteText"});e.addButton("selectall",{title:"paste.selectall_desc",cmd:"selectall"});function h(s){var m,q,k,l=e.selection,p=e.dom,r=e.getBody(),j;if(e.pasteAsPlainText&&(s.clipboardData||p.doc.dataTransfer)){s.preventDefault();i({content:(s.clipboardData||p.doc.dataTransfer).getData("Text")},true);return}if(p.get("_mcePaste")){return}m=p.add(r,"div",{id:"_mcePaste","class":"mcePaste"},"\uFEFF");if(r!=e.getDoc().body){j=p.getPos(e.selection.getStart(),r).y}else{j=r.scrollTop}p.setStyles(m,{position:"absolute",left:-10000,top:j,width:1,height:1,overflow:"hidden"});if(tinymce.isIE){k=p.doc.body.createTextRange();k.moveToElementText(m);k.execCommand("Paste");p.remove(m);if(m.innerHTML==="\uFEFF"){e.execCommand("mcePasteWord");s.preventDefault();return}i({content:m.innerHTML});return tinymce.dom.Event.cancel(s)}else{function o(n){n.preventDefault()}p.bind(e.getDoc(),"mousedown",o);p.bind(e.getDoc(),"keydown",o);q=e.selection.getRng();m=m.firstChild;k=e.getDoc().createRange();k.setStart(m,0);k.setEnd(m,1);l.setRng(k);window.setTimeout(function(){var t="",n=p.select("div.mcePaste");c(n,function(u){c(p.select("div.mcePaste",u),function(v){p.remove(v,1)});c(p.select("span.Apple-style-span",u),function(v){p.remove(v,1)});t+=u.innerHTML});c(n,function(u){p.remove(u)});if(q){l.setRng(q)}i({content:t});p.unbind(e.getDoc(),"mousedown",o);p.unbind(e.getDoc(),"keydown",o)},0)}}if(b(e,"paste_auto_cleanup_on_paste")){if(tinymce.isOpera||/Firefox\/2/.test(navigator.userAgent)){e.onKeyDown.add(function(j,k){if(((tinymce.isMac?k.metaKey:k.ctrlKey)&&k.keyCode==86)||(k.shiftKey&&k.keyCode==45)){h(k)}})}else{e.onPaste.addToTop(function(j,k){return h(k)})}}if(b(e,"paste_block_drop")){e.onInit.add(function(){e.dom.bind(e.getBody(),["dragend","dragover","draggesture","dragdrop","drop","drag"],function(j){j.preventDefault();j.stopPropagation();return false})})}g._legacySupport()},getInfo:function(){return{longname:"Paste text/word",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",infourl:"http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/paste",version:tinymce.majorVersion+"."+tinymce.minorVersion}},_preProcess:function(i,f){var l=this.editor,k=f.content,q=tinymce.grep,p=tinymce.explode,g=tinymce.trim,m,j;function e(h){c(h,function(o){if(o.constructor==RegExp){k=k.replace(o,"")}else{k=k.replace(o[0],o[1])}})}if(/class="?Mso|style="[^"]*\bmso-|w:WordDocument/i.test(k)||f.wordContent){f.wordContent=true;e([/^\s*( )+/gi,/( |]*>)+\s*$/gi]);if(b(l,"paste_convert_headers_to_strong")){k=k.replace(/

    ]*class="?MsoHeading"?[^>]*>(.*?)<\/p>/gi,"

    $1

    ")}if(b(l,"paste_convert_middot_lists")){e([[//gi,"$&__MCE_ITEM__"],[/(]+(?:mso-list:|:\s*symbol)[^>]+>)/gi,"$1__MCE_ITEM__"]])}e([//gi,/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi,[/<(\/?)s>/gi,"<$1strike>"],[/ /gi,"\u00a0"]]);do{m=k.length;k=k.replace(/(<[a-z][^>]*\s)(?:id|name|language|type|on\w+|\w+:\w+)=(?:"[^"]*"|\w+)\s?/gi,"$1")}while(m!=k.length);if(b(l,"paste_retain_style_properties").replace(/^none$/i,"").length==0){k=k.replace(/<\/?span[^>]*>/gi,"")}else{e([[/([\s\u00a0]*)<\/span>/gi,function(o,h){return(h.length>0)?h.replace(/./," ").slice(Math.floor(h.length/2)).split("").join("\u00a0"):""}],[/(<[a-z][^>]*)\sstyle="([^"]*)"/gi,function(u,h,t){var v=[],o=0,r=p(g(t).replace(/"/gi,"'"),";");c(r,function(s){var w,y,z=p(s,":");function x(A){return A+((A!=="0")&&(/\d$/.test(A)))?"px":""}if(z.length==2){w=z[0].toLowerCase();y=z[1].toLowerCase();switch(w){case"mso-padding-alt":case"mso-padding-top-alt":case"mso-padding-right-alt":case"mso-padding-bottom-alt":case"mso-padding-left-alt":case"mso-margin-alt":case"mso-margin-top-alt":case"mso-margin-right-alt":case"mso-margin-bottom-alt":case"mso-margin-left-alt":case"mso-table-layout-alt":case"mso-height":case"mso-width":case"mso-vertical-align-alt":v[o++]=w.replace(/^mso-|-alt$/g,"")+":"+x(y);return;case"horiz-align":v[o++]="text-align:"+y;return;case"vert-align":v[o++]="vertical-align:"+y;return;case"font-color":case"mso-foreground":v[o++]="color:"+y;return;case"mso-background":case"mso-highlight":v[o++]="background:"+y;return;case"mso-default-height":v[o++]="min-height:"+x(y);return;case"mso-default-width":v[o++]="min-width:"+x(y);return;case"mso-padding-between-alt":v[o++]="border-collapse:separate;border-spacing:"+x(y);return;case"text-line-through":if((y=="single")||(y=="double")){v[o++]="text-decoration:line-through"}return;case"mso-zero-height":if(y=="yes"){v[o++]="display:none"}return}if(/^(mso|column|font-emph|lang|layout|line-break|list-image|nav|panose|punct|row|ruby|sep|size|src|tab-|table-border|text-(?!align|decor|indent|trans)|top-bar|version|vnd|word-break)/.test(w)){return}v[o++]=w+":"+z[1]}});if(o>0){return h+' style="'+v.join(";")+'"'}else{return h}}]])}}if(b(l,"paste_convert_headers_to_strong")){e([[/]*>/gi,"

    "],[/<\/h[1-6][^>]*>/gi,"

    "]])}j=b(l,"paste_strip_class_attributes");if(j!=="none"){function n(r,o){if(j==="all"){return""}var h=q(p(o.replace(/^(["'])(.*)\1$/,"$2")," "),function(s){return(/^(?!mso)/i.test(s))});return h.length?' class="'+h.join(" ")+'"':""}k=k.replace(/ class="([^"]+)"/gi,n);k=k.replace(/ class=(\w+)/gi,n)}if(b(l,"paste_remove_spans")){k=k.replace(/<\/?span[^>]*>/gi,"")}f.content=k},_postProcess:function(h,j){var g=this,f=g.editor,i=f.dom,e;if(j.wordContent){c(i.select("a",j.node),function(k){if(!k.href||k.href.indexOf("#_Toc")!=-1){i.remove(k,1)}});if(b(f,"paste_convert_middot_lists")){g._convertLists(h,j)}e=b(f,"paste_retain_style_properties");if((tinymce.is(e,"string"))&&(e!=="all")&&(e!=="*")){e=tinymce.explode(e.replace(/^none$/i,""));c(i.select("*",j.node),function(n){var o={},l=0,m,p,k;if(e){for(m=0;m0){i.setStyles(n,o)}else{if(n.nodeName=="SPAN"&&!n.className){i.remove(n,true)}}})}}if(b(f,"paste_remove_styles")||(b(f,"paste_remove_styles_if_webkit")&&tinymce.isWebKit)){c(i.select("*[style]",j.node),function(k){k.removeAttribute("style");k.removeAttribute("_mce_style")})}else{if(tinymce.isWebKit){c(i.select("*",j.node),function(k){k.removeAttribute("_mce_style")})}}},_convertLists:function(h,f){var j=h.editor.dom,i,m,e=-1,g,n=[],l,k;c(j.select("p",f.node),function(u){var r,v="",t,s,o,q;for(r=u.firstChild;r&&r.nodeType==3;r=r.nextSibling){v+=r.nodeValue}v=u.innerHTML.replace(/<\/?\w+[^>]*>/gi,"").replace(/ /g,"\u00a0");if(/^(__MCE_ITEM__)+[\u2022\u00b7\u00a7\u00d8o]\s*\u00a0*/.test(v)){t="ul"}if(/^__MCE_ITEM__\s*\w+\.\s*\u00a0{2,}/.test(v)){t="ol"}if(t){g=parseFloat(u.style.marginLeft||0);if(g>e){n.push(g)}if(!i||t!=l){i=j.create(t);j.insertAfter(i,u)}else{if(g>e){i=m.appendChild(j.create(t))}else{if(g]*>/gi,"");if(t=="ul"&&/^[\u2022\u00b7\u00a7\u00d8o]/.test(p)){j.remove(w)}else{if(/^[\s\S]*\w+\.( |\u00a0)*\s*/.test(p)){j.remove(w)}}});s=u.innerHTML;if(t=="ul"){s=u.innerHTML.replace(/__MCE_ITEM__/g,"").replace(/^[\u2022\u00b7\u00a7\u00d8o]\s*( |\u00a0)+\s*/,"")}else{s=u.innerHTML.replace(/__MCE_ITEM__/g,"").replace(/^\s*\w+\.( |\u00a0)+\s*/,"")}m=i.appendChild(j.create("li",0,s));j.remove(u);e=g;l=t}else{i=e=0}});k=f.node.innerHTML;if(k.indexOf("__MCE_ITEM__")!=-1){f.node.innerHTML=k.replace(/__MCE_ITEM__/g,"")}},_insertBlockContent:function(l,h,m){var f,j,g=l.selection,q,n,e,o,i,k="mce_marker";function p(t){var s;if(tinymce.isIE){s=l.getDoc().body.createTextRange();s.moveToElementText(t);s.collapse(false);s.select()}else{g.select(t,1);g.collapse(false)}}this._insert(' ',1);j=h.get(k);f=h.getParent(j,"p,h1,h2,h3,h4,h5,h6,ul,ol,th,td");if(f&&!/TD|TH/.test(f.nodeName)){j=h.split(f,j);c(h.create("div",0,m).childNodes,function(r){q=j.parentNode.insertBefore(r.cloneNode(true),j)});p(q)}else{h.setOuterHTML(j,m);g.select(l.getBody(),1);g.collapse(0)}while(n=h.get(k)){h.remove(n)}n=g.getStart();e=h.getViewPort(l.getWin());o=l.dom.getPos(n).y;i=n.clientHeight;if(oe.y+e.h){l.getDoc().body.scrollTop=o0)){if(!d){d=("34,quot,38,amp,39,apos,60,lt,62,gt,"+j.serializer.settings.entities).split(",")}if(/<(?:p|br|h[1-6]|ul|ol|dl|table|t[rdh]|div|blockquote|fieldset|pre|address|center)[^>]*>/i.test(v)){q([/[\n\r]+/g])}else{q([/\r+/g])}q([[/<\/(?:p|h[1-6]|ul|ol|dl|table|div|blockquote|fieldset|pre|address|center)>/gi,"\n\n"],[/]*>|<\/tr>/gi,"\n"],[/<\/t[dh]>\s*]*>/gi,"\t"],/<[a-z!\/?][^>]*>/gi,[/ /gi," "],[/&(#\d+|[a-z0-9]{1,10});/gi,function(i,h){if(h.charAt(0)==="#"){return String.fromCharCode(h.slice(1))}else{return((i=y(d,h))>0)?String.fromCharCode(d[i-1]):" "}}],[/(?:(?!\n)\s)*(\n+)(?:(?!\n)\s)*/gi,"$1"],[/\n{3,}/g,"\n\n"],/^\s+|\s+$/g]);v=x.encode(v);if(!s.isCollapsed()){z.execCommand("Delete",false,null)}if(m(o,"array")||(m(o,"array"))){q(o)}else{if(m(o,"string")){q(new RegExp(o,"gi"))}}if(g=="none"){q([[/\n+/g," "]])}else{if(g=="br"){q([[/\n/g,"
    "]])}else{q([/^\s+|\s+$/g,[/\n\n/g,"

    "],[/\n/g,"
    "]])}}if((l=v.indexOf("

    "))!=-1){k=v.lastIndexOf("

    ");r=s.getNode();e=[];do{if(r.nodeType==1){if(r.nodeName=="TD"||r.nodeName=="BODY"){break}e[e.length]=r}}while(r=r.parentNode);if(e.length>0){p=v.substring(0,l);f="";for(t=0,u=e.length;t";f+="<"+e[e.length-t-1].nodeName.toLowerCase()+">"}if(l==k){v=p+f+v.substring(l+7)}else{v=p+v.substring(l+4,k+4)+f+v.substring(k+7)}}}j.execCommand("mceInsertRawHTML",false,v+' ');window.setTimeout(function(){var h=x.get("_plain_text_marker"),B,i,A,w;s.select(h,false);z.execCommand("Delete",false,null);h=null;B=s.getStart();i=x.getViewPort(n);A=x.getPos(B).y;w=B.clientHeight;if((Ai.y+i.h)){z.body.scrollTop=A")});return}}if(o.get("_mcePaste")){return}l=o.add(q,"div",{id:"_mcePaste","class":"mcePaste","data-mce-bogus":"1"},"\uFEFF\uFEFF");if(q!=d.getDoc().body){i=o.getPos(d.selection.getStart(),q).y}else{i=q.scrollTop+o.getViewPort(d.getWin()).y}o.setStyles(l,{position:"absolute",left:tinymce.isGecko?-40:0,top:i-25,width:1,height:1,overflow:"hidden"});if(tinymce.isIE){t=k.getRng();j=o.doc.body.createTextRange();j.moveToElementText(l);j.execCommand("Paste");o.remove(l);if(l.innerHTML==="\uFEFF\uFEFF"){d.execCommand("mcePasteWord");s.preventDefault();return}k.setRng(t);k.setContent("");setTimeout(function(){h({content:l.innerHTML})},0);return tinymce.dom.Event.cancel(s)}else{function m(n){n.preventDefault()}o.bind(d.getDoc(),"mousedown",m);o.bind(d.getDoc(),"keydown",m);p=d.selection.getRng();l=l.firstChild;j=d.getDoc().createRange();j.setStart(l,0);j.setEnd(l,2);k.setRng(j);window.setTimeout(function(){var u="",n;if(!o.select("div.mcePaste > div.mcePaste").length){n=o.select("div.mcePaste");c(n,function(w){var v=w.firstChild;if(v&&v.nodeName=="DIV"&&v.style.marginTop&&v.style.backgroundColor){o.remove(v,1)}c(o.select("span.Apple-style-span",w),function(x){o.remove(x,1)});c(o.select("br[data-mce-bogus]",w),function(x){o.remove(x)});if(w.parentNode.className!="mcePaste"){u+=w.innerHTML}})}else{u="

    "+o.encode(r).replace(/\r?\n\r?\n/g,"

    ").replace(/\r?\n/g,"
    ")+"

    "}c(o.select("div.mcePaste"),function(v){o.remove(v)});if(p){k.setRng(p)}h({content:u});o.unbind(d.getDoc(),"mousedown",m);o.unbind(d.getDoc(),"keydown",m)},0)}}if(b(d,"paste_auto_cleanup_on_paste")){if(tinymce.isOpera||/Firefox\/2/.test(navigator.userAgent)){d.onKeyDown.addToTop(function(i,j){if(((tinymce.isMac?j.metaKey:j.ctrlKey)&&j.keyCode==86)||(j.shiftKey&&j.keyCode==45)){g(j)}})}else{d.onPaste.addToTop(function(i,j){return g(j)})}}d.onInit.add(function(){d.controlManager.setActive("pastetext",d.pasteAsPlainText);if(b(d,"paste_block_drop")){d.dom.bind(d.getBody(),["dragend","dragover","draggesture","dragdrop","drop","drag"],function(i){i.preventDefault();i.stopPropagation();return false})}});f._legacySupport()},getInfo:function(){return{longname:"Paste text/word",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",infourl:"http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/paste",version:tinymce.majorVersion+"."+tinymce.minorVersion}},_preProcess:function(g,e){var k=this.editor,j=e.content,p=tinymce.grep,n=tinymce.explode,f=tinymce.trim,l,i;function d(h){c(h,function(o){if(o.constructor==RegExp){j=j.replace(o,"")}else{j=j.replace(o[0],o[1])}})}if(k.settings.paste_enable_default_filters==false){return}if(tinymce.isIE&&document.documentMode>=9){d([[/(?:
     [\s\r\n]+|
    )*(<\/?(h[1-6r]|p|div|address|pre|form|table|tbody|thead|tfoot|th|tr|td|li|ol|ul|caption|blockquote|center|dl|dt|dd|dir|fieldset)[^>]*>)(?:
     [\s\r\n]+|
    )*/g,"$1"]]);d([[/

    /g,"

    "],[/
    /g," "],[/

    /g,"
    "]])}if(/class="?Mso|style="[^"]*\bmso-|w:WordDocument/i.test(j)||e.wordContent){e.wordContent=true;d([/^\s*( )+/gi,/( |]*>)+\s*$/gi]);if(b(k,"paste_convert_headers_to_strong")){j=j.replace(/

    ]*class="?MsoHeading"?[^>]*>(.*?)<\/p>/gi,"

    $1

    ")}if(b(k,"paste_convert_middot_lists")){d([[//gi,"$&__MCE_ITEM__"],[/(]+(?:mso-list:|:\s*symbol)[^>]+>)/gi,"$1__MCE_ITEM__"],[/(]+(?:MsoListParagraph)[^>]+>)/gi,"$1__MCE_ITEM__"]])}d([//gi,/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi,[/<(\/?)s>/gi,"<$1strike>"],[/ /gi,"\u00a0"]]);do{l=j.length;j=j.replace(/(<[a-z][^>]*\s)(?:id|name|language|type|on\w+|\w+:\w+)=(?:"[^"]*"|\w+)\s?/gi,"$1")}while(l!=j.length);if(b(k,"paste_retain_style_properties").replace(/^none$/i,"").length==0){j=j.replace(/<\/?span[^>]*>/gi,"")}else{d([[/([\s\u00a0]*)<\/span>/gi,function(o,h){return(h.length>0)?h.replace(/./," ").slice(Math.floor(h.length/2)).split("").join("\u00a0"):""}],[/(<[a-z][^>]*)\sstyle="([^"]*)"/gi,function(t,h,r){var u=[],o=0,q=n(f(r).replace(/"/gi,"'"),";");c(q,function(s){var w,y,z=n(s,":");function x(A){return A+((A!=="0")&&(/\d$/.test(A)))?"px":""}if(z.length==2){w=z[0].toLowerCase();y=z[1].toLowerCase();switch(w){case"mso-padding-alt":case"mso-padding-top-alt":case"mso-padding-right-alt":case"mso-padding-bottom-alt":case"mso-padding-left-alt":case"mso-margin-alt":case"mso-margin-top-alt":case"mso-margin-right-alt":case"mso-margin-bottom-alt":case"mso-margin-left-alt":case"mso-table-layout-alt":case"mso-height":case"mso-width":case"mso-vertical-align-alt":u[o++]=w.replace(/^mso-|-alt$/g,"")+":"+x(y);return;case"horiz-align":u[o++]="text-align:"+y;return;case"vert-align":u[o++]="vertical-align:"+y;return;case"font-color":case"mso-foreground":u[o++]="color:"+y;return;case"mso-background":case"mso-highlight":u[o++]="background:"+y;return;case"mso-default-height":u[o++]="min-height:"+x(y);return;case"mso-default-width":u[o++]="min-width:"+x(y);return;case"mso-padding-between-alt":u[o++]="border-collapse:separate;border-spacing:"+x(y);return;case"text-line-through":if((y=="single")||(y=="double")){u[o++]="text-decoration:line-through"}return;case"mso-zero-height":if(y=="yes"){u[o++]="display:none"}return}if(/^(mso|column|font-emph|lang|layout|line-break|list-image|nav|panose|punct|row|ruby|sep|size|src|tab-|table-border|text-(?!align|decor|indent|trans)|top-bar|version|vnd|word-break)/.test(w)){return}u[o++]=w+":"+z[1]}});if(o>0){return h+' style="'+u.join(";")+'"'}else{return h}}]])}}if(b(k,"paste_convert_headers_to_strong")){d([[/]*>/gi,"

    "],[/<\/h[1-6][^>]*>/gi,"

    "]])}d([[/Version:[\d.]+\nStartHTML:\d+\nEndHTML:\d+\nStartFragment:\d+\nEndFragment:\d+/gi,""]]);i=b(k,"paste_strip_class_attributes");if(i!=="none"){function m(q,o){if(i==="all"){return""}var h=p(n(o.replace(/^(["'])(.*)\1$/,"$2")," "),function(r){return(/^(?!mso)/i.test(r))});return h.length?' class="'+h.join(" ")+'"':""}j=j.replace(/ class="([^"]+)"/gi,m);j=j.replace(/ class=([\-\w]+)/gi,m)}if(b(k,"paste_remove_spans")){j=j.replace(/<\/?span[^>]*>/gi,"")}e.content=j},_postProcess:function(g,i){var f=this,e=f.editor,h=e.dom,d;if(e.settings.paste_enable_default_filters==false){return}if(i.wordContent){c(h.select("a",i.node),function(j){if(!j.href||j.href.indexOf("#_Toc")!=-1){h.remove(j,1)}});if(b(e,"paste_convert_middot_lists")){f._convertLists(g,i)}d=b(e,"paste_retain_style_properties");if((tinymce.is(d,"string"))&&(d!=="all")&&(d!=="*")){d=tinymce.explode(d.replace(/^none$/i,""));c(h.select("*",i.node),function(m){var n={},k=0,l,o,j;if(d){for(l=0;l0){h.setStyles(m,n)}else{if(m.nodeName=="SPAN"&&!m.className){h.remove(m,true)}}})}}if(b(e,"paste_remove_styles")||(b(e,"paste_remove_styles_if_webkit")&&tinymce.isWebKit)){c(h.select("*[style]",i.node),function(j){j.removeAttribute("style");j.removeAttribute("data-mce-style")})}else{if(tinymce.isWebKit){c(h.select("*",i.node),function(j){j.removeAttribute("data-mce-style")})}}},_convertLists:function(g,e){var i=g.editor.dom,h,l,d=-1,f,m=[],k,j;c(i.select("p",e.node),function(t){var q,u="",s,r,n,o;for(q=t.firstChild;q&&q.nodeType==3;q=q.nextSibling){u+=q.nodeValue}u=t.innerHTML.replace(/<\/?\w+[^>]*>/gi,"").replace(/ /g,"\u00a0");if(/^(__MCE_ITEM__)+[\u2022\u00b7\u00a7\u00d8o\u25CF]\s*\u00a0*/.test(u)){s="ul"}if(/^__MCE_ITEM__\s*\w+\.\s*\u00a0+/.test(u)){s="ol"}if(s){f=parseFloat(t.style.marginLeft||0);if(f>d){m.push(f)}if(!h||s!=k){h=i.create(s);i.insertAfter(h,t)}else{if(f>d){h=l.appendChild(i.create(s))}else{if(f]*>/gi,"");if(s=="ul"&&/^__MCE_ITEM__[\u2022\u00b7\u00a7\u00d8o\u25CF]/.test(p)){i.remove(v)}else{if(/^__MCE_ITEM__[\s\S]*\w+\.( |\u00a0)*\s*/.test(p)){i.remove(v)}}});r=t.innerHTML;if(s=="ul"){r=t.innerHTML.replace(/__MCE_ITEM__/g,"").replace(/^[\u2022\u00b7\u00a7\u00d8o\u25CF]\s*( |\u00a0)+\s*/,"")}else{r=t.innerHTML.replace(/__MCE_ITEM__/g,"").replace(/^\s*\w+\.( |\u00a0)+\s*/,"")}l=h.appendChild(i.create("li",0,r));i.remove(t);d=f;k=s}else{h=d=0}});j=e.node.innerHTML;if(j.indexOf("__MCE_ITEM__")!=-1){e.node.innerHTML=j.replace(/__MCE_ITEM__/g,"")}},_insert:function(f,d){var e=this.editor,g=e.selection.getRng();if(!e.selection.isCollapsed()&&g.startContainer!=g.endContainer){e.getDoc().execCommand("Delete",false,null)}e.execCommand("mceInsertContent",false,f,{skip_undo:d})},_insertPlainText:function(g){var d=this.editor,e=b(d,"paste_text_linebreaktype"),i=b(d,"paste_text_replacements"),f=tinymce.is;function h(j){c(j,function(k){if(k.constructor==RegExp){g=g.replace(k,"")}else{g=g.replace(k[0],k[1])}})}if((typeof(g)==="string")&&(g.length>0)){if(/<(?:p|br|h[1-6]|ul|ol|dl|table|t[rdh]|div|blockquote|fieldset|pre|address|center)[^>]*>/i.test(g)){h([/[\n\r]+/g])}else{h([/\r+/g])}h([[/<\/(?:p|h[1-6]|ul|ol|dl|table|div|blockquote|fieldset|pre|address|center)>/gi,"\n\n"],[/]*>|<\/tr>/gi,"\n"],[/<\/t[dh]>\s*]*>/gi,"\t"],/<[a-z!\/?][^>]*>/gi,[/ /gi," "],[/(?:(?!\n)\s)*(\n+)(?:(?!\n)\s)*/gi,"$1"],[/\n{3,}/g,"\n\n"]]);g=d.dom.decode(tinymce.html.Entities.encodeRaw(g));if(f(i,"array")){h(i)}else{if(f(i,"string")){h(new RegExp(i,"gi"))}}if(e=="none"){h([[/\n+/g," "]])}else{if(e=="br"){h([[/\n/g,"
    "]])}else{if(e=="p"){h([[/\n+/g,"

    "],[/^(.*<\/p>)(

    )$/,"

    $1"]])}else{h([[/\n\n/g,"

    "],[/^(.*<\/p>)(

    )$/,"

    $1"],[/\n/g,"
    "]])}}}d.execCommand("mceInsertContent",false,g)}},_legacySupport:function(){var e=this,d=e.editor;d.addCommand("mcePasteWord",function(){d.windowManager.open({file:e.url+"/pasteword.htm",width:parseInt(b(d,"paste_dialog_width")),height:parseInt(b(d,"paste_dialog_height")),inline:1})});if(b(d,"paste_text_use_dialog")){d.addCommand("mcePasteText",function(){d.windowManager.open({file:e.url+"/pastetext.htm",width:parseInt(b(d,"paste_dialog_width")),height:parseInt(b(d,"paste_dialog_height")),inline:1})})}d.addButton("pasteword",{title:"paste.paste_word_desc",cmd:"mcePasteWord"})}});tinymce.PluginManager.add("paste",tinymce.plugins.PastePlugin)})(); \ No newline at end of file diff --git a/js/tiny_mce/plugins/paste/editor_plugin_src.js b/js/tiny_mce/plugins/paste/editor_plugin_src.js index 4c3bf6542e..73fe7fe9a4 100644 --- a/js/tiny_mce/plugins/paste/editor_plugin_src.js +++ b/js/tiny_mce/plugins/paste/editor_plugin_src.js @@ -10,9 +10,9 @@ (function() { var each = tinymce.each, - entities = null, defs = { paste_auto_cleanup_on_paste : true, + paste_enable_default_filters : true, paste_block_drop : false, paste_retain_style_properties : "none", paste_strip_class_attributes : "mso", @@ -25,8 +25,9 @@ paste_dialog_height : "400", paste_text_use_dialog : false, paste_text_sticky : false, + paste_text_sticky_default : false, paste_text_notifyalways : false, - paste_text_linebreaktype : "p", + paste_text_linebreaktype : "combined", paste_text_replacements : [ [/\u2026/g, "..."], [/[\x93\x94\u201c\u201d]/g, '"'], @@ -63,13 +64,19 @@ ed.execCallback('paste_postprocess', pl, o); }); + ed.onKeyDown.addToTop(function(ed, e) { + // Block ctrl+v from adding an undo level since the default logic in tinymce.Editor will add that + if (((tinymce.isMac ? e.metaKey : e.ctrlKey) && e.keyCode == 86) || (e.shiftKey && e.keyCode == 45)) + return false; // Stop other listeners + }); + // Initialize plain text flag - ed.pasteAsPlainText = false; + ed.pasteAsPlainText = getParam(ed, 'paste_text_sticky_default'); // This function executes the process handlers and inserts the contents // force_rich overrides plain text mode set by user, important for pasting with execCommand function process(o, force_rich) { - var dom = ed.dom; + var dom = ed.dom, rng; // Execute pre process handlers t.onPreProcess.dispatch(t, o); @@ -77,23 +84,31 @@ // Create DOM structure o.node = dom.create('div', 0, o.content); + // If pasting inside the same element and the contents is only one block + // remove the block and keep the text since Firefox will copy parts of pre and h1-h6 as a pre element + if (tinymce.isGecko) { + rng = ed.selection.getRng(true); + if (rng.startContainer == rng.endContainer && rng.startContainer.nodeType == 3) { + // Is only one block node and it doesn't contain word stuff + if (o.node.childNodes.length === 1 && /^(p|h[1-6]|pre)$/i.test(o.node.firstChild.nodeName) && o.content.indexOf('__MCE_ITEM__') === -1) + dom.remove(o.node.firstChild, true); + } + } + // Execute post process handlers t.onPostProcess.dispatch(t, o); // Serialize content - o.content = ed.serializer.serialize(o.node, {getInner : 1}); + o.content = ed.serializer.serialize(o.node, {getInner : 1, forced_root_block : ''}); // Plain text option active? if ((!force_rich) && (ed.pasteAsPlainText)) { - t._insertPlainText(ed, dom, o.content); + t._insertPlainText(o.content); if (!getParam(ed, "paste_text_sticky")) { ed.pasteAsPlainText = false; ed.controlManager.setActive("pastetext", false); } - } else if (/<(p|h[1-6]|ul|ol)/.test(o.content)) { - // Handle insertion of contents containing block elements separately - t._insertBlockContent(ed, dom, o.content); } else { t._insert(o.content); } @@ -115,7 +130,7 @@ if (getParam(ed, "paste_text_sticky")) { ed.windowManager.alert(ed.translate('paste.plaintext_mode_sticky')); } else { - ed.windowManager.alert(ed.translate('paste.plaintext_mode_sticky')); + ed.windowManager.alert(ed.translate('paste.plaintext_mode')); } if (!getParam(ed, "paste_text_notifyalways")) { @@ -132,38 +147,46 @@ // hidden div and placing the caret inside it and after the browser paste // is done it grabs that contents and processes that function grabContent(e) { - var n, or, rng, sel = ed.selection, dom = ed.dom, body = ed.getBody(), posY; + var n, or, rng, oldRng, sel = ed.selection, dom = ed.dom, body = ed.getBody(), posY, textContent; // Check if browser supports direct plaintext access - if (ed.pasteAsPlainText && (e.clipboardData || dom.doc.dataTransfer)) { - e.preventDefault(); - process({content : (e.clipboardData || dom.doc.dataTransfer).getData('Text')}, true); - return; + if (e.clipboardData || dom.doc.dataTransfer) { + textContent = (e.clipboardData || dom.doc.dataTransfer).getData('Text'); + + if (ed.pasteAsPlainText) { + e.preventDefault(); + process({content : dom.encode(textContent).replace(/\r?\n/g, '
    ')}); + return; + } } if (dom.get('_mcePaste')) return; // Create container to paste into - n = dom.add(body, 'div', {id : '_mcePaste', 'class' : 'mcePaste'}, '\uFEFF'); + n = dom.add(body, 'div', {id : '_mcePaste', 'class' : 'mcePaste', 'data-mce-bogus' : '1'}, '\uFEFF\uFEFF'); // If contentEditable mode we need to find out the position of the closest element if (body != ed.getDoc().body) posY = dom.getPos(ed.selection.getStart(), body).y; else - posY = body.scrollTop; + posY = body.scrollTop + dom.getViewPort(ed.getWin()).y; // Styles needs to be applied after the element is added to the document since WebKit will otherwise remove all styles + // If also needs to be in view on IE or the paste would fail dom.setStyles(n, { position : 'absolute', - left : -10000, - top : posY, + left : tinymce.isGecko ? -40 : 0, // Need to move it out of site on Gecko since it will othewise display a ghost resize rect for the div + top : posY - 25, width : 1, height : 1, overflow : 'hidden' }); if (tinymce.isIE) { + // Store away the old range + oldRng = sel.getRng(); + // Select the container rng = dom.doc.body.createTextRange(); rng.moveToElementText(n); @@ -174,14 +197,23 @@ // Check if the contents was changed, if it wasn't then clipboard extraction failed probably due // to IE security settings so we pass the junk though better than nothing right - if (n.innerHTML === '\uFEFF') { + if (n.innerHTML === '\uFEFF\uFEFF') { ed.execCommand('mcePasteWord'); e.preventDefault(); return; } - // Process contents - process({content : n.innerHTML}); + // Restore the old range and clear the contents before pasting + sel.setRng(oldRng); + sel.setContent(''); + + // For some odd reason we need to detach the the mceInsertContent call from the paste event + // It's like IE has a reference to the parent element that you paste in and the selection gets messed up + // when it tries to restore the selection + setTimeout(function() { + // Process contents + process({content : n.innerHTML}); + }, 0); // Block the real paste event return tinymce.dom.Event.cancel(e); @@ -196,34 +228,52 @@ or = ed.selection.getRng(); - // Move caret into hidden div + // Move select contents inside DIV n = n.firstChild; rng = ed.getDoc().createRange(); rng.setStart(n, 0); - rng.setEnd(n, 1); + rng.setEnd(n, 2); sel.setRng(rng); // Wait a while and grab the pasted contents window.setTimeout(function() { - var h = '', nl = dom.select('div.mcePaste'); + var h = '', nl; - // WebKit will split the div into multiple ones so this will loop through then all and join them to get the whole HTML string - each(nl, function(n) { - // WebKit duplicates the divs so we need to remove them - each(dom.select('div.mcePaste', n), function(n) { - dom.remove(n, 1); - }); + // Paste divs duplicated in paste divs seems to happen when you paste plain text so lets first look for that broken behavior in WebKit + if (!dom.select('div.mcePaste > div.mcePaste').length) { + nl = dom.select('div.mcePaste'); - // Remove apply style spans - each(dom.select('span.Apple-style-span', n), function(n) { - dom.remove(n, 1); - }); + // WebKit will split the div into multiple ones so this will loop through then all and join them to get the whole HTML string + each(nl, function(n) { + var child = n.firstChild; - h += n.innerHTML; - }); + // WebKit inserts a DIV container with lots of odd styles + if (child && child.nodeName == 'DIV' && child.style.marginTop && child.style.backgroundColor) { + dom.remove(child, 1); + } + + // Remove apply style spans + each(dom.select('span.Apple-style-span', n), function(n) { + dom.remove(n, 1); + }); + + // Remove bogus br elements + each(dom.select('br[data-mce-bogus]', n), function(n) { + dom.remove(n); + }); + + // WebKit will make a copy of the DIV for each line of plain text pasted and insert them into the DIV + if (n.parentNode.className != 'mcePaste') + h += n.innerHTML; + }); + } else { + // Found WebKit weirdness so force the content into paragraphs this seems to happen when you paste plain text from Nodepad etc + // So this logic will replace double enter with paragraphs and single enter with br so it kind of looks the same + h = '

    ' + dom.encode(textContent).replace(/\r?\n\r?\n/g, '

    ').replace(/\r?\n/g, '
    ') + '

    '; + } // Remove the nodes - each(nl, function(n) { + each(dom.select('div.mcePaste'), function(n) { dom.remove(n); }); @@ -244,7 +294,7 @@ if (getParam(ed, "paste_auto_cleanup_on_paste")) { // Is it's Opera or older FF use key handler if (tinymce.isOpera || /Firefox\/2/.test(navigator.userAgent)) { - ed.onKeyDown.add(function(ed, e) { + ed.onKeyDown.addToTop(function(ed, e) { if (((tinymce.isMac ? e.metaKey : e.ctrlKey) && e.keyCode == 86) || (e.shiftKey && e.keyCode == 45)) grabContent(e); }); @@ -256,17 +306,19 @@ } } - // Block all drag/drop events - if (getParam(ed, "paste_block_drop")) { - ed.onInit.add(function() { + ed.onInit.add(function() { + ed.controlManager.setActive("pastetext", ed.pasteAsPlainText); + + // Block all drag/drop events + if (getParam(ed, "paste_block_drop")) { ed.dom.bind(ed.getBody(), ['dragend', 'dragover', 'draggesture', 'dragdrop', 'drop', 'drag'], function(e) { e.preventDefault(); e.stopPropagation(); return false; }); - }); - } + } + }); // Add legacy support t._legacySupport(); @@ -283,8 +335,6 @@ }, _preProcess : function(pl, o) { - //console.log('Before preprocess:' + o.content); - var ed = this.editor, h = o.content, grep = tinymce.grep, @@ -292,6 +342,8 @@ trim = tinymce.trim, len, stripClass; + //console.log('Before preprocess:' + o.content); + function process(items) { each(items, function(v) { // Remove or replace @@ -301,6 +353,23 @@ h = h.replace(v[0], v[1]); }); } + + if (ed.settings.paste_enable_default_filters == false) { + return; + } + + // IE9 adds BRs before/after block elements when contents is pasted from word or for example another browser + if (tinymce.isIE && document.documentMode >= 9) { + // IE9 adds BRs before/after block elements when contents is pasted from word or for example another browser + process([[/(?:
     [\s\r\n]+|
    )*(<\/?(h[1-6r]|p|div|address|pre|form|table|tbody|thead|tfoot|th|tr|td|li|ol|ul|caption|blockquote|center|dl|dt|dd|dir|fieldset)[^>]*>)(?:
     [\s\r\n]+|
    )*/g, '$1']]); + + // IE9 also adds an extra BR element for each soft-linefeed and it also adds a BR for each word wrap break + process([ + [/

    /g, '

    '], // Replace multiple BR elements with uppercase BR to keep them intact + [/
    /g, ' '], // Replace single br elements with space since they are word wrap BR:s + [/

    /g, '
    '] // Replace back the double brs but into a single BR + ]); + } // Detect Word content and process it more aggressive if (/class="?Mso|style="[^"]*\bmso-|w:WordDocument/i.test(h) || o.wordContent) { @@ -320,7 +389,8 @@ if (getParam(ed, "paste_convert_middot_lists")) { process([ [//gi, '$&__MCE_ITEM__'], // Convert supportLists to a list item marker - [/(]+(?:mso-list:|:\s*symbol)[^>]+>)/gi, '$1__MCE_ITEM__'] // Convert mso-list and symbol spans to item markers + [/(]+(?:mso-list:|:\s*symbol)[^>]+>)/gi, '$1__MCE_ITEM__'], // Convert mso-list and symbol spans to item markers + [/(]+(?:MsoListParagraph)[^>]+>)/gi, '$1__MCE_ITEM__'] // Convert mso-list and symbol paragraphs to item markers (FF) ]); } @@ -472,6 +542,11 @@ ]); } + process([ + // Copy paste from Java like Open Office will produce this junk on FF + [/Version:[\d.]+\nStartHTML:\d+\nEndHTML:\d+\nStartFragment:\d+\nEndFragment:\d+/gi, ''] + ]); + // Class attribute options are: leave all as-is ("none"), remove all ("all"), or remove only those starting with mso ("mso"). // Note:- paste_strip_class_attributes: "none", verify_css_classes: true is also a good variation. stripClass = getParam(ed, "paste_strip_class_attributes"); @@ -491,7 +566,7 @@ }; h = h.replace(/ class="([^"]+)"/gi, removeClasses); - h = h.replace(/ class=(\w+)/gi, removeClasses); + h = h.replace(/ class=([\-\w]+)/gi, removeClasses); } // Remove spans option @@ -510,6 +585,10 @@ _postProcess : function(pl, o) { var t = this, ed = t.editor, dom = ed.dom, styleProps; + if (ed.settings.paste_enable_default_filters == false) { + return; + } + if (o.wordContent) { // Remove named anchors or TOC links each(dom.select('a', o.node), function(a) { @@ -561,14 +640,14 @@ if (getParam(ed, "paste_remove_styles") || (getParam(ed, "paste_remove_styles_if_webkit") && tinymce.isWebKit)) { each(dom.select('*[style]', o.node), function(el) { el.removeAttribute('style'); - el.removeAttribute('_mce_style'); + el.removeAttribute('data-mce-style'); }); } else { if (tinymce.isWebKit) { // We need to compress the styles on WebKit since if you paste it will become // Removing the mce_style that contains the real value will force the Serializer engine to compress the styles each(dom.select('*', o.node), function(el) { - el.removeAttribute('_mce_style'); + el.removeAttribute('data-mce-style'); }); } } @@ -591,11 +670,11 @@ val = p.innerHTML.replace(/<\/?\w+[^>]*>/gi, '').replace(/ /g, '\u00a0'); // Detect unordered lists look for bullets - if (/^(__MCE_ITEM__)+[\u2022\u00b7\u00a7\u00d8o]\s*\u00a0*/.test(val)) + if (/^(__MCE_ITEM__)+[\u2022\u00b7\u00a7\u00d8o\u25CF]\s*\u00a0*/.test(val)) type = 'ul'; // Detect ordered lists 1., a. or ixv. - if (/^__MCE_ITEM__\s*\w+\.\s*\u00a0{2,}/.test(val)) + if (/^__MCE_ITEM__\s*\w+\.\s*\u00a0+/.test(val)) type = 'ol'; // Check if node value matches the list pattern: o   @@ -625,9 +704,9 @@ var html = span.innerHTML.replace(/<\/?\w+[^>]*>/gi, ''); // Remove span with the middot or the number - if (type == 'ul' && /^[\u2022\u00b7\u00a7\u00d8o]/.test(html)) + if (type == 'ul' && /^__MCE_ITEM__[\u2022\u00b7\u00a7\u00d8o\u25CF]/.test(html)) dom.remove(span); - else if (/^[\s\S]*\w+\.( |\u00a0)*\s*/.test(html)) + else if (/^__MCE_ITEM__[\s\S]*\w+\.( |\u00a0)*\s*/.test(html)) dom.remove(span); }); @@ -635,7 +714,7 @@ // Remove middot/list items if (type == 'ul') - html = p.innerHTML.replace(/__MCE_ITEM__/g, '').replace(/^[\u2022\u00b7\u00a7\u00d8o]\s*( |\u00a0)+\s*/, ''); + html = p.innerHTML.replace(/__MCE_ITEM__/g, '').replace(/^[\u2022\u00b7\u00a7\u00d8o\u25CF]\s*( |\u00a0)+\s*/, ''); else html = p.innerHTML.replace(/__MCE_ITEM__/g, '').replace(/^\s*\w+\.( |\u00a0)+\s*/, ''); @@ -655,65 +734,6 @@ o.node.innerHTML = html.replace(/__MCE_ITEM__/g, ''); }, - /** - * This method will split the current block parent and insert the contents inside the split position. - * This logic can be improved so text nodes at the start/end remain in the start/end block elements - */ - _insertBlockContent : function(ed, dom, content) { - var parentBlock, marker, sel = ed.selection, last, elm, vp, y, elmHeight, markerId = 'mce_marker'; - - function select(n) { - var r; - - if (tinymce.isIE) { - r = ed.getDoc().body.createTextRange(); - r.moveToElementText(n); - r.collapse(false); - r.select(); - } else { - sel.select(n, 1); - sel.collapse(false); - } - } - - // Insert a marker for the caret position - this._insert(' ', 1); - marker = dom.get(markerId); - parentBlock = dom.getParent(marker, 'p,h1,h2,h3,h4,h5,h6,ul,ol,th,td'); - - // If it's a parent block but not a table cell - if (parentBlock && !/TD|TH/.test(parentBlock.nodeName)) { - // Split parent block - marker = dom.split(parentBlock, marker); - - // Insert nodes before the marker - each(dom.create('div', 0, content).childNodes, function(n) { - last = marker.parentNode.insertBefore(n.cloneNode(true), marker); - }); - - // Move caret after marker - select(last); - } else { - dom.setOuterHTML(marker, content); - sel.select(ed.getBody(), 1); - sel.collapse(0); - } - - // Remove marker if it's left - while (elm = dom.get(markerId)) - dom.remove(elm); - - // Get element, position and height - elm = sel.getStart(); - vp = dom.getViewPort(ed.getWin()); - y = ed.dom.getPos(elm).y; - elmHeight = elm.clientHeight; - - // Is element within viewport if not then scroll it into view - if (y < vp.y || y + elmHeight > vp.y + vp.h) - ed.getDoc().body.scrollTop = y < vp.y ? y : y - vp.h + 25; - }, - /** * Inserts the specified contents at the caret position. */ @@ -724,8 +744,7 @@ if (!ed.selection.isCollapsed() && r.startContainer != r.endContainer) ed.getDoc().execCommand('Delete', false, null); - // It's better to use the insertHTML method on Gecko since it will combine paragraphs correctly before inserting the contents - ed.execCommand(tinymce.isGecko ? 'insertHTML' : 'mceInsertContent', false, h, {skip_undo : skip_undo}); + ed.execCommand('mceInsertContent', false, h, {skip_undo : skip_undo}); }, /** @@ -737,31 +756,24 @@ * plugin, and requires minimal changes to add the new functionality. * Speednet - June 2009 */ - _insertPlainText : function(ed, dom, h) { - var i, len, pos, rpos, node, breakElms, before, after, - w = ed.getWin(), - d = ed.getDoc(), - sel = ed.selection, - is = tinymce.is, - inArray = tinymce.inArray, + _insertPlainText : function(content) { + var ed = this.editor, linebr = getParam(ed, "paste_text_linebreaktype"), - rl = getParam(ed, "paste_text_replacements"); + rl = getParam(ed, "paste_text_replacements"), + is = tinymce.is; function process(items) { each(items, function(v) { if (v.constructor == RegExp) - h = h.replace(v, ""); + content = content.replace(v, ""); else - h = h.replace(v[0], v[1]); + content = content.replace(v[0], v[1]); }); }; - if ((typeof(h) === "string") && (h.length > 0)) { - if (!entities) - entities = ("34,quot,38,amp,39,apos,60,lt,62,gt," + ed.serializer.settings.entities).split(","); - + if ((typeof(content) === "string") && (content.length > 0)) { // If HTML content with line-breaking tags, then remove all cr/lf chars because only tags will break a line - if (/<(?:p|br|h[1-6]|ul|ol|dl|table|t[rdh]|div|blockquote|fieldset|pre|address|center)[^>]*>/i.test(h)) { + if (/<(?:p|br|h[1-6]|ul|ol|dl|table|t[rdh]|div|blockquote|fieldset|pre|address|center)[^>]*>/i.test(content)) { process([ /[\n\r]+/g ]); @@ -778,128 +790,47 @@ [/<\/t[dh]>\s*]*>/gi, "\t"], // Table cells get tabs betweem them /<[a-z!\/?][^>]*>/gi, // Delete all remaining tags [/ /gi, " "], // Convert non-break spaces to regular spaces (remember, *plain text*) - [ - // HTML entity - /&(#\d+|[a-z0-9]{1,10});/gi, - - // Replace with actual character - function(e, s) { - if (s.charAt(0) === "#") { - return String.fromCharCode(s.slice(1)); - } - else { - return ((e = inArray(entities, s)) > 0)? String.fromCharCode(entities[e-1]) : " "; - } - } - ], - [/(?:(?!\n)\s)*(\n+)(?:(?!\n)\s)*/gi, "$1"], // Cool little RegExp deletes whitespace around linebreak chars. - [/\n{3,}/g, "\n\n"], // Max. 2 consecutive linebreaks - /^\s+|\s+$/g // Trim the front & back + [/(?:(?!\n)\s)*(\n+)(?:(?!\n)\s)*/gi, "$1"],// Cool little RegExp deletes whitespace around linebreak chars. + [/\n{3,}/g, "\n\n"] // Max. 2 consecutive linebreaks ]); - h = dom.encode(h); - - // Delete any highlighted text before pasting - if (!sel.isCollapsed()) { - d.execCommand("Delete", false, null); - } + content = ed.dom.decode(tinymce.html.Entities.encodeRaw(content)); // Perform default or custom replacements - if (is(rl, "array") || (is(rl, "array"))) { + if (is(rl, "array")) { process(rl); - } - else if (is(rl, "string")) { + } else if (is(rl, "string")) { process(new RegExp(rl, "gi")); } // Treat paragraphs as specified in the config if (linebr == "none") { + // Convert all line breaks to space process([ [/\n+/g, " "] ]); - } - else if (linebr == "br") { + } else if (linebr == "br") { + // Convert all line breaks to
    process([ [/\n/g, "
    "] ]); - } - else { + } else if (linebr == "p") { + // Convert all line breaks to

    ...

    + process([ + [/\n+/g, "

    "], + [/^(.*<\/p>)(

    )$/, '

    $1'] + ]); + } else { + // defaults to "combined" + // Convert single line breaks to
    and double line breaks to

    ...

    process([ - /^\s+|\s+$/g, [/\n\n/g, "

    "], + [/^(.*<\/p>)(

    )$/, '

    $1'], [/\n/g, "
    "] ]); } - // This next piece of code handles the situation where we're pasting more than one paragraph of plain - // text, and we are pasting the content into the middle of a block node in the editor. The block - // node gets split at the selection point into "Para A" and "Para B" (for the purposes of explaining). - // The first paragraph of the pasted text is appended to "Para A", and the last paragraph of the - // pasted text is prepended to "Para B". Any other paragraphs of pasted text are placed between - // "Para A" and "Para B". This code solves a host of problems with the original plain text plugin and - // now handles styles correctly. (Pasting plain text into a styled paragraph is supposed to make the - // plain text take the same style as the existing paragraph.) - if ((pos = h.indexOf("

    ")) != -1) { - rpos = h.lastIndexOf("

    "); - node = sel.getNode(); - breakElms = []; // Get list of elements to break - - do { - if (node.nodeType == 1) { - // Don't break tables and break at body - if (node.nodeName == "TD" || node.nodeName == "BODY") { - break; - } - - breakElms[breakElms.length] = node; - } - } while (node = node.parentNode); - - // Are we in the middle of a block node? - if (breakElms.length > 0) { - before = h.substring(0, pos); - after = ""; - - for (i=0, len=breakElms.length; i"; - after += "<" + breakElms[breakElms.length-i-1].nodeName.toLowerCase() + ">"; - } - - if (pos == rpos) { - h = before + after + h.substring(pos+7); - } - else { - h = before + h.substring(pos+4, rpos+4) + after + h.substring(rpos+7); - } - } - } - - // Insert content at the caret, plus add a marker for repositioning the caret - ed.execCommand("mceInsertRawHTML", false, h + ' '); - - // Reposition the caret to the marker, which was placed immediately after the inserted content. - // Needs to be done asynchronously (in window.setTimeout) or else it doesn't work in all browsers. - // The second part of the code scrolls the content up if the caret is positioned off-screen. - // This is only necessary for WebKit browsers, but it doesn't hurt to use for all. - window.setTimeout(function() { - var marker = dom.get('_plain_text_marker'), - elm, vp, y, elmHeight; - - sel.select(marker, false); - d.execCommand("Delete", false, null); - marker = null; - - // Get element, position and height - elm = sel.getStart(); - vp = dom.getViewPort(w); - y = dom.getPos(elm).y; - elmHeight = elm.clientHeight; - - // Is element within viewport if not then scroll it into view - if ((y < vp.y) || (y + elmHeight > vp.y + vp.h)) { - d.body.scrollTop = y < vp.y ? y : y - vp.h + 25; - } - }, 0); + ed.execCommand('mceInsertContent', false, content); } }, diff --git a/js/tiny_mce/plugins/paste/langs/en_dlg.js b/js/tiny_mce/plugins/paste/langs/en_dlg.js index eeac778960..bc74daf85c 100644 --- a/js/tiny_mce/plugins/paste/langs/en_dlg.js +++ b/js/tiny_mce/plugins/paste/langs/en_dlg.js @@ -1,5 +1 @@ -tinyMCE.addI18n('en.paste_dlg',{ -text_title:"Use CTRL+V on your keyboard to paste the text into the window.", -text_linebreaks:"Keep linebreaks", -word_title:"Use CTRL+V on your keyboard to paste the text into the window." -}); \ No newline at end of file +tinyMCE.addI18n('en.paste_dlg',{"word_title":"Use Ctrl+V on your keyboard to paste the text into the window.","text_linebreaks":"Keep Linebreaks","text_title":"Use Ctrl+V on your keyboard to paste the text into the window."}); \ No newline at end of file diff --git a/js/tiny_mce/plugins/searchreplace/editor_plugin.js b/js/tiny_mce/plugins/searchreplace/editor_plugin.js index cd9c985b7a..165bc12df5 100644 --- a/js/tiny_mce/plugins/searchreplace/editor_plugin.js +++ b/js/tiny_mce/plugins/searchreplace/editor_plugin.js @@ -1 +1 @@ -(function(){tinymce.create("tinymce.plugins.SearchReplacePlugin",{init:function(a,c){function b(d){a.windowManager.open({file:c+"/searchreplace.htm",width:420+parseInt(a.getLang("searchreplace.delta_width",0)),height:170+parseInt(a.getLang("searchreplace.delta_height",0)),inline:1,auto_focus:0},{mode:d,search_string:a.selection.getContent({format:"text"}),plugin_url:c})}a.addCommand("mceSearch",function(){b("search")});a.addCommand("mceReplace",function(){b("replace")});a.addButton("search",{title:"searchreplace.search_desc",cmd:"mceSearch"});a.addButton("replace",{title:"searchreplace.replace_desc",cmd:"mceReplace"});a.addShortcut("ctrl+f","searchreplace.search_desc","mceSearch")},getInfo:function(){return{longname:"Search/Replace",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",infourl:"http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/searchreplace",version:tinymce.majorVersion+"."+tinymce.minorVersion}}});tinymce.PluginManager.add("searchreplace",tinymce.plugins.SearchReplacePlugin)})(); \ No newline at end of file +(function(){tinymce.create("tinymce.plugins.SearchReplacePlugin",{init:function(a,c){function b(d){window.focus();a.windowManager.open({file:c+"/searchreplace.htm",width:420+parseInt(a.getLang("searchreplace.delta_width",0)),height:170+parseInt(a.getLang("searchreplace.delta_height",0)),inline:1,auto_focus:0},{mode:d,search_string:a.selection.getContent({format:"text"}),plugin_url:c})}a.addCommand("mceSearch",function(){b("search")});a.addCommand("mceReplace",function(){b("replace")});a.addButton("search",{title:"searchreplace.search_desc",cmd:"mceSearch"});a.addButton("replace",{title:"searchreplace.replace_desc",cmd:"mceReplace"});a.addShortcut("ctrl+f","searchreplace.search_desc","mceSearch")},getInfo:function(){return{longname:"Search/Replace",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",infourl:"http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/searchreplace",version:tinymce.majorVersion+"."+tinymce.minorVersion}}});tinymce.PluginManager.add("searchreplace",tinymce.plugins.SearchReplacePlugin)})(); \ No newline at end of file diff --git a/js/tiny_mce/plugins/searchreplace/editor_plugin_src.js b/js/tiny_mce/plugins/searchreplace/editor_plugin_src.js index 1433a06a4a..4c87e8fa79 100644 --- a/js/tiny_mce/plugins/searchreplace/editor_plugin_src.js +++ b/js/tiny_mce/plugins/searchreplace/editor_plugin_src.js @@ -12,6 +12,10 @@ tinymce.create('tinymce.plugins.SearchReplacePlugin', { init : function(ed, url) { function open(m) { + // Keep IE from writing out the f/r character to the editor + // instance while initializing a new dialog. See: #3131190 + window.focus(); + ed.windowManager.open({ file : url + '/searchreplace.htm', width : 420 + parseInt(ed.getLang('searchreplace.delta_width', 0)), diff --git a/js/tiny_mce/plugins/searchreplace/js/searchreplace.js b/js/tiny_mce/plugins/searchreplace/js/searchreplace.js index c0a6243297..80284b9f3f 100644 --- a/js/tiny_mce/plugins/searchreplace/js/searchreplace.js +++ b/js/tiny_mce/plugins/searchreplace/js/searchreplace.js @@ -2,14 +2,18 @@ tinyMCEPopup.requireLangPack(); var SearchReplaceDialog = { init : function(ed) { - var f = document.forms[0], m = tinyMCEPopup.getWindowArg("mode"); + var t = this, f = document.forms[0], m = tinyMCEPopup.getWindowArg("mode"); - this.switchMode(m); + t.switchMode(m); f[m + '_panel_searchstring'].value = tinyMCEPopup.getWindowArg("search_string"); // Focus input field f[m + '_panel_searchstring'].focus(); + + mcTabs.onChange.add(function(tab_id, panel_id) { + t.switchMode(tab_id.substring(0, tab_id.indexOf('_'))); + }); }, switchMode : function(m) { @@ -42,21 +46,23 @@ var SearchReplaceDialog = { ca = f[m + '_panel_casesensitivebox'].checked; rs = f['replace_panel_replacestring'].value; + if (tinymce.isIE) { + r = ed.getDoc().selection.createRange(); + } + if (s == '') return; function fix() { // Correct Firefox graphics glitches + // TODO: Verify if this is actually needed any more, maybe it was for very old FF versions? r = se.getRng().cloneRange(); ed.getDoc().execCommand('SelectAll', false, null); se.setRng(r); }; function replace() { - if (tinymce.isIE) - ed.selection.getRng().duplicate().pasteHTML(rs); // Needs to be duplicated due to selection bug in IE - else - ed.getDoc().execCommand('InsertHTML', false, rs); + ed.selection.setContent(rs); // Needs to be duplicated due to selection bug in IE }; // IE flags @@ -70,6 +76,9 @@ var SearchReplaceDialog = { ed.selection.collapse(true); if (tinymce.isIE) { + ed.focus(); + r = ed.getDoc().selection.createRange(); + while (r.findText(s, b ? -1 : 1, fl)) { r.scrollIntoView(); r.select(); @@ -111,6 +120,9 @@ var SearchReplaceDialog = { return; if (tinymce.isIE) { + ed.focus(); + r = ed.getDoc().selection.createRange(); + if (r.findText(s, b ? -1 : 1, fl)) { r.scrollIntoView(); r.select(); diff --git a/js/tiny_mce/plugins/searchreplace/langs/en_dlg.js b/js/tiny_mce/plugins/searchreplace/langs/en_dlg.js index 370959afa3..8a65900977 100644 --- a/js/tiny_mce/plugins/searchreplace/langs/en_dlg.js +++ b/js/tiny_mce/plugins/searchreplace/langs/en_dlg.js @@ -1,16 +1 @@ -tinyMCE.addI18n('en.searchreplace_dlg',{ -searchnext_desc:"Find again", -notfound:"The search has been completed. The search string could not be found.", -search_title:"Find", -replace_title:"Find/Replace", -allreplaced:"All occurrences of the search string were replaced.", -findwhat:"Find what", -replacewith:"Replace with", -direction:"Direction", -up:"Up", -down:"Down", -mcase:"Match case", -findnext:"Find next", -replace:"Replace", -replaceall:"Replace all" -}); \ No newline at end of file +tinyMCE.addI18n('en.searchreplace_dlg',{findwhat:"Find What",replacewith:"Replace with",direction:"Direction",up:"Up",down:"Down",mcase:"Match Case",findnext:"Find Next",allreplaced:"All occurrences of the search string were replaced.","searchnext_desc":"Find Again",notfound:"The search has been completed. The search string could not be found.","search_title":"Find","replace_title":"Find/Replace",replaceall:"Replace All",replace:"Replace"}); \ No newline at end of file diff --git a/js/tiny_mce/plugins/searchreplace/searchreplace.htm b/js/tiny_mce/plugins/searchreplace/searchreplace.htm index d0424cfc9b..5a22d8aa4d 100644 --- a/js/tiny_mce/plugins/searchreplace/searchreplace.htm +++ b/js/tiny_mce/plugins/searchreplace/searchreplace.htm @@ -8,27 +8,28 @@ - + +

    - +
    - +
    - - - +
    + + @@ -39,7 +40,7 @@ - - - - - -
    {#style_dlg.padding} - +
    @@ -288,11 +330,14 @@ @@ -300,11 +345,14 @@ @@ -312,11 +360,14 @@ @@ -324,11 +375,14 @@ @@ -341,7 +395,7 @@
    {#style_dlg.margin} -
     
    - +
    - +
      + + +
    - +
    - +
      + + +
    - +
    - +
      + + +
    - +
    - +
      + + +
    +
    @@ -349,11 +403,14 @@ @@ -361,11 +418,14 @@ @@ -373,11 +433,14 @@ @@ -385,11 +448,14 @@ @@ -401,131 +467,148 @@
    -
     
    - +
    - +
      + + +
    - +
    - +
      + + +
    - +
    - +
      + + +
    - +
    - +
      + + +
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      {#style_dlg.style} {#style_dlg.width} {#style_dlg.color}
          
    {#style_dlg.top}   - - - - - - -
     
    -
      - - - - - -
     
    -
    {#style_dlg.right}   - - - - - - -
     
    -
      - - - - - -
     
    -
    {#style_dlg.bottom}   - - - - - - -
     
    -
      - - - - - -
     
    -
    {#style_dlg.left}   - - - - - - -
     
    -
      - - - - - +
    + {#style_dlg.border} +
     
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      {#style_dlg.style} {#style_dlg.width} {#style_dlg.color}
          
    {#style_dlg.top}   + + + + + + +
      + + +
    +
      + + + + + +
     
    +
    {#style_dlg.right}   + + + + + + +
      + + +
    +
      + + + + + +
     
    +
    {#style_dlg.bottom}   + + + + + + +
      + + +
    +
      + + + + + +
     
    +
    {#style_dlg.left}   + + + + + + +
      + + +
    +
      + + + + + +
     
    +
    -
    +
    - +
    + {#style_dlg.list} +
    @@ -541,10 +624,13 @@
    +
    - +
    + {#style_dlg.position} +
    @@ -555,11 +641,14 @@ @@ -570,11 +659,14 @@ @@ -582,12 +674,13 @@
    - +
    - +
      + + +
    - +
    - +
      + + +
    +
    {#style_dlg.placement} - +
    @@ -595,11 +688,14 @@ @@ -607,11 +703,14 @@ @@ -619,11 +718,14 @@ @@ -631,11 +733,14 @@ @@ -648,7 +753,7 @@
    {#style_dlg.clip} -
     
    {#style_dlg.top} - +
    - +
      + + +
    {#style_dlg.right} - +
    - +
      + + +
    {#style_dlg.bottom} - +
    - +
      + + +
    {#style_dlg.left} - +
    - +
      + + +
    +
    @@ -656,11 +761,14 @@ @@ -668,11 +776,14 @@ @@ -680,11 +791,14 @@ @@ -692,11 +806,14 @@ diff --git a/js/tiny_mce/plugins/tabfocus/editor_plugin.js b/js/tiny_mce/plugins/tabfocus/editor_plugin.js index 27d2440222..42a82d112c 100644 --- a/js/tiny_mce/plugins/tabfocus/editor_plugin.js +++ b/js/tiny_mce/plugins/tabfocus/editor_plugin.js @@ -1 +1 @@ -(function(){var c=tinymce.DOM,a=tinymce.dom.Event,d=tinymce.each,b=tinymce.explode;tinymce.create("tinymce.plugins.TabFocusPlugin",{init:function(f,g){function e(i,j){if(j.keyCode===9){return a.cancel(j)}}function h(l,p){var j,m,o,n,k;function q(i){o=c.getParent(l.id,"form");n=o.elements;if(o){d(n,function(s,r){if(s.id==l.id){j=r;return false}});if(i>0){for(m=j+1;m=0;m--){if(n[m].type!="hidden"){return n[m]}}}}return null}if(p.keyCode===9){k=b(l.getParam("tab_focus",l.getParam("tabfocus_elements",":prev,:next")));if(k.length==1){k[1]=k[0];k[0]=":prev"}if(p.shiftKey){if(k[0]==":prev"){n=q(-1)}else{n=c.get(k[0])}}else{if(k[1]==":next"){n=q(1)}else{n=c.get(k[1])}}if(n){if(l=tinymce.get(n.id||n.name)){l.focus()}else{window.setTimeout(function(){window.focus();n.focus()},10)}return a.cancel(p)}}}f.onKeyUp.add(e);if(tinymce.isGecko){f.onKeyPress.add(h);f.onKeyDown.add(e)}else{f.onKeyDown.add(h)}f.onInit.add(function(){d(c.select("a:first,a:last",f.getContainer()),function(i){a.add(i,"focus",function(){f.focus()})})})},getInfo:function(){return{longname:"Tabfocus",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",infourl:"http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/tabfocus",version:tinymce.majorVersion+"."+tinymce.minorVersion}}});tinymce.PluginManager.add("tabfocus",tinymce.plugins.TabFocusPlugin)})(); \ No newline at end of file +(function(){var c=tinymce.DOM,a=tinymce.dom.Event,d=tinymce.each,b=tinymce.explode;tinymce.create("tinymce.plugins.TabFocusPlugin",{init:function(f,g){function e(i,j){if(j.keyCode===9){return a.cancel(j)}}function h(l,p){var j,m,o,n,k;function q(t){n=c.select(":input:enabled,*[tabindex]");function s(v){return v.nodeName==="BODY"||(v.type!="hidden"&&!(v.style.display=="none")&&!(v.style.visibility=="hidden")&&s(v.parentNode))}function i(v){return v.attributes.tabIndex.specified||v.nodeName=="INPUT"||v.nodeName=="TEXTAREA"}function u(){return tinymce.isIE6||tinymce.isIE7}function r(v){return((!u()||i(v)))&&v.getAttribute("tabindex")!="-1"&&s(v)}d(n,function(w,v){if(w.id==l.id){j=v;return false}});if(t>0){for(m=j+1;m=0;m--){if(r(n[m])){return n[m]}}}return null}if(p.keyCode===9){k=b(l.getParam("tab_focus",l.getParam("tabfocus_elements",":prev,:next")));if(k.length==1){k[1]=k[0];k[0]=":prev"}if(p.shiftKey){if(k[0]==":prev"){n=q(-1)}else{n=c.get(k[0])}}else{if(k[1]==":next"){n=q(1)}else{n=c.get(k[1])}}if(n){if(n.id&&(l=tinymce.get(n.id||n.name))){l.focus()}else{window.setTimeout(function(){if(!tinymce.isWebKit){window.focus()}n.focus()},10)}return a.cancel(p)}}}f.onKeyUp.add(e);if(tinymce.isGecko){f.onKeyPress.add(h);f.onKeyDown.add(e)}else{f.onKeyDown.add(h)}},getInfo:function(){return{longname:"Tabfocus",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",infourl:"http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/tabfocus",version:tinymce.majorVersion+"."+tinymce.minorVersion}}});tinymce.PluginManager.add("tabfocus",tinymce.plugins.TabFocusPlugin)})(); \ No newline at end of file diff --git a/js/tiny_mce/plugins/tabfocus/editor_plugin_src.js b/js/tiny_mce/plugins/tabfocus/editor_plugin_src.js index c2be2f40a6..a1579c85f2 100644 --- a/js/tiny_mce/plugins/tabfocus/editor_plugin_src.js +++ b/js/tiny_mce/plugins/tabfocus/editor_plugin_src.js @@ -1,112 +1,122 @@ -/** - * editor_plugin_src.js - * - * Copyright 2009, Moxiecode Systems AB - * Released under LGPL License. - * - * License: http://tinymce.moxiecode.com/license - * Contributing: http://tinymce.moxiecode.com/contributing - */ - -(function() { - var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, explode = tinymce.explode; - - tinymce.create('tinymce.plugins.TabFocusPlugin', { - init : function(ed, url) { - function tabCancel(ed, e) { - if (e.keyCode === 9) - return Event.cancel(e); - }; - - function tabHandler(ed, e) { - var x, i, f, el, v; - - function find(d) { - f = DOM.getParent(ed.id, 'form'); - el = f.elements; - - if (f) { - each(el, function(e, i) { - if (e.id == ed.id) { - x = i; - return false; - } - }); - - if (d > 0) { - for (i = x + 1; i < el.length; i++) { - if (el[i].type != 'hidden') - return el[i]; - } - } else { - for (i = x - 1; i >= 0; i--) { - if (el[i].type != 'hidden') - return el[i]; - } - } - } - - return null; - }; - - if (e.keyCode === 9) { - v = explode(ed.getParam('tab_focus', ed.getParam('tabfocus_elements', ':prev,:next'))); - - if (v.length == 1) { - v[1] = v[0]; - v[0] = ':prev'; - } - - // Find element to focus - if (e.shiftKey) { - if (v[0] == ':prev') - el = find(-1); - else - el = DOM.get(v[0]); - } else { - if (v[1] == ':next') - el = find(1); - else - el = DOM.get(v[1]); - } - - if (el) { - if (ed = tinymce.get(el.id || el.name)) - ed.focus(); - else - window.setTimeout(function() {window.focus();el.focus();}, 10); - - return Event.cancel(e); - } - } - }; - - ed.onKeyUp.add(tabCancel); - - if (tinymce.isGecko) { - ed.onKeyPress.add(tabHandler); - ed.onKeyDown.add(tabCancel); - } else - ed.onKeyDown.add(tabHandler); - - ed.onInit.add(function() { - each(DOM.select('a:first,a:last', ed.getContainer()), function(n) { - Event.add(n, 'focus', function() {ed.focus();}); - }); - }); - }, - - getInfo : function() { - return { - longname : 'Tabfocus', - author : 'Moxiecode Systems AB', - authorurl : 'http://tinymce.moxiecode.com', - infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/tabfocus', - version : tinymce.majorVersion + "." + tinymce.minorVersion - }; - } - }); - - // Register plugin - tinymce.PluginManager.add('tabfocus', tinymce.plugins.TabFocusPlugin); -})(); \ No newline at end of file +/** + * editor_plugin_src.js + * + * Copyright 2009, Moxiecode Systems AB + * Released under LGPL License. + * + * License: http://tinymce.moxiecode.com/license + * Contributing: http://tinymce.moxiecode.com/contributing + */ + +(function() { + var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, explode = tinymce.explode; + + tinymce.create('tinymce.plugins.TabFocusPlugin', { + init : function(ed, url) { + function tabCancel(ed, e) { + if (e.keyCode === 9) + return Event.cancel(e); + } + + function tabHandler(ed, e) { + var x, i, f, el, v; + + function find(d) { + el = DOM.select(':input:enabled,*[tabindex]'); + + function canSelectRecursive(e) { + return e.nodeName==="BODY" || (e.type != 'hidden' && + !(e.style.display == "none") && + !(e.style.visibility == "hidden") && canSelectRecursive(e.parentNode)); + } + function canSelectInOldIe(el) { + return el.attributes["tabIndex"].specified || el.nodeName == "INPUT" || el.nodeName == "TEXTAREA"; + } + function isOldIe() { + return tinymce.isIE6 || tinymce.isIE7; + } + function canSelect(el) { + return ((!isOldIe() || canSelectInOldIe(el))) && el.getAttribute("tabindex") != '-1' && canSelectRecursive(el); + } + + each(el, function(e, i) { + if (e.id == ed.id) { + x = i; + return false; + } + }); + if (d > 0) { + for (i = x + 1; i < el.length; i++) { + if (canSelect(el[i])) + return el[i]; + } + } else { + for (i = x - 1; i >= 0; i--) { + if (canSelect(el[i])) + return el[i]; + } + } + + return null; + } + + if (e.keyCode === 9) { + v = explode(ed.getParam('tab_focus', ed.getParam('tabfocus_elements', ':prev,:next'))); + + if (v.length == 1) { + v[1] = v[0]; + v[0] = ':prev'; + } + + // Find element to focus + if (e.shiftKey) { + if (v[0] == ':prev') + el = find(-1); + else + el = DOM.get(v[0]); + } else { + if (v[1] == ':next') + el = find(1); + else + el = DOM.get(v[1]); + } + + if (el) { + if (el.id && (ed = tinymce.get(el.id || el.name))) + ed.focus(); + else + window.setTimeout(function() { + if (!tinymce.isWebKit) + window.focus(); + el.focus(); + }, 10); + + return Event.cancel(e); + } + } + } + + ed.onKeyUp.add(tabCancel); + + if (tinymce.isGecko) { + ed.onKeyPress.add(tabHandler); + ed.onKeyDown.add(tabCancel); + } else + ed.onKeyDown.add(tabHandler); + + }, + + getInfo : function() { + return { + longname : 'Tabfocus', + author : 'Moxiecode Systems AB', + authorurl : 'http://tinymce.moxiecode.com', + infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/tabfocus', + version : tinymce.majorVersion + "." + tinymce.minorVersion + }; + } + }); + + // Register plugin + tinymce.PluginManager.add('tabfocus', tinymce.plugins.TabFocusPlugin); +})(); diff --git a/js/tiny_mce/plugins/table/cell.htm b/js/tiny_mce/plugins/table/cell.htm index d243e1d833..a72a8d6973 100644 --- a/js/tiny_mce/plugins/table/cell.htm +++ b/js/tiny_mce/plugins/table/cell.htm @@ -5,16 +5,17 @@ + - + @@ -23,7 +24,7 @@
    {#table_dlg.general_props} -
     
    {#style_dlg.top} - +
    - +
      + + +
    {#style_dlg.right} - +
    - +
      + + +
    {#style_dlg.bottom} - +
    - +
      + + +
    {#style_dlg.left} - +
    - +
      + + +
    +
    - + - + @@ -92,7 +93,7 @@
    {#table_dlg.advanced_props} -
    @@ -70,10 +71,10 @@
    +
    @@ -124,7 +125,7 @@
    - +
    @@ -133,10 +134,10 @@ - - + +
     
    - +
    @@ -145,10 +146,10 @@ - - + + '; if (s.image) - h1 = DOM.createHTML('img ', {src : s.image, 'class' : 'mceAction ' + s['class']}); + h1 = DOM.createHTML('img ', {src : s.image, role: 'presentation', 'class' : 'mceAction ' + s['class']}); else h1 = DOM.createHTML('span', {'class' : 'mceAction ' + s['class']}, ''); - h += ''; + h1 += DOM.createHTML('span', {'class': 'mceVoiceLabel mceIconOnly', id: t.id + '_voice', style: 'display:none;'}, s.title); + h += ''; - h1 = DOM.createHTML('span', {'class' : 'mceOpen ' + s['class']}); - h += ''; + h1 = DOM.createHTML('span', {'class' : 'mceOpen ' + s['class']}, ''); + h += ''; h += ''; - - return DOM.createHTML('table', {id : t.id, 'class' : 'mceSplitButton mceSplitButtonEnabled ' + s['class'], cellpadding : '0', cellspacing : '0', onmousedown : 'return false;', title : s.title}, h); + h = DOM.createHTML('table', { role: 'presentation', 'class' : 'mceSplitButton mceSplitButtonEnabled ' + s['class'], cellpadding : '0', cellspacing : '0', title : s.title}, h); + return DOM.createHTML('div', {id : t.id, role: 'button', tabindex: '0', 'aria-labelledby': t.id + '_voice', 'aria-haspopup': 'true'}, h); }, postRender : function() { - var t = this, s = t.settings; + var t = this, s = t.settings, activate; if (s.onclick) { - Event.add(t.id + '_action', 'click', function() { - if (!t.isDisabled()) + activate = function(evt) { + if (!t.isDisabled()) { s.onclick(t.value); + Event.cancel(evt); + } + }; + Event.add(t.id + '_action', 'click', activate); + Event.add(t.id, ['click', 'keydown'], function(evt) { + var DOM_VK_SPACE = 32, DOM_VK_ENTER = 14, DOM_VK_RETURN = 13, DOM_VK_UP = 38, DOM_VK_DOWN = 40; + if ((evt.keyCode === 32 || evt.keyCode === 13 || evt.keyCode === 14) && !evt.altKey && !evt.ctrlKey && !evt.metaKey) { + activate(); + Event.cancel(evt); + } else if (evt.type === 'click' || evt.keyCode === DOM_VK_DOWN) { + t.showMenu(); + Event.cancel(evt); + } }); } - Event.add(t.id + '_open', 'click', t.showMenu, t); - Event.add(t.id + '_open', 'focus', function() {t._focused = 1;}); - Event.add(t.id + '_open', 'blur', function() {t._focused = 0;}); + Event.add(t.id + '_open', 'click', function (evt) { + t.showMenu(); + Event.cancel(evt); + }); + Event.add([t.id, t.id + '_open'], 'focus', function() {t._focused = 1;}); + Event.add([t.id, t.id + '_open'], 'blur', function() {t._focused = 0;}); // Old IE doesn't have hover on all elements if (tinymce.isIE6 || !DOM.boxModel) { @@ -8562,6 +10393,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { Event.clear(this.id + '_action'); Event.clear(this.id + '_open'); + Event.clear(this.id); } }); })(tinymce); @@ -8570,10 +10402,10 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { var DOM = tinymce.DOM, Event = tinymce.dom.Event, is = tinymce.is, each = tinymce.each; tinymce.create('tinymce.ui.ColorSplitButton:tinymce.ui.SplitButton', { - ColorSplitButton : function(id, s) { + ColorSplitButton : function(id, s, ed) { var t = this; - t.parent(id, s); + t.parent(id, s, ed); t.settings = s = tinymce.extend({ colors : '000000,993300,333300,003300,003366,000080,333399,333333,800000,FF6600,808000,008000,008080,0000FF,666699,808080,FF0000,FF9900,99CC00,339966,33CCCC,3366FF,800080,999999,FF00FF,FFCC00,FFFF00,00FF00,00FFFF,00CCFF,993366,C0C0C0,FF99CC,FFCC99,FFFF99,CCFFCC,CCFFFF,99CCFF,CC99FF,FFFFFF', @@ -8631,30 +10463,31 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { hideMenu : function(e) { var t = this; - // Prevent double toogles by canceling the mouse click event to the button - if (e && e.type == "mousedown" && DOM.getParent(e.target, function(e) {return e.id === t.id + '_open';})) - return; - - if (!e || !DOM.getParent(e.target, '.mceSplitButtonMenu')) { - DOM.removeClass(t.id, 'mceSplitButtonSelected'); - Event.remove(DOM.doc, 'mousedown', t.hideMenu, t); - Event.remove(t.id + '_menu', 'keydown', t._keyHandler); - DOM.hide(t.id + '_menu'); - } + if (t.isMenuVisible) { + // Prevent double toogles by canceling the mouse click event to the button + if (e && e.type == "mousedown" && DOM.getParent(e.target, function(e) {return e.id === t.id + '_open';})) + return; - t.onHideMenu.dispatch(t); + if (!e || !DOM.getParent(e.target, '.mceSplitButtonMenu')) { + DOM.removeClass(t.id, 'mceSplitButtonSelected'); + Event.remove(DOM.doc, 'mousedown', t.hideMenu, t); + Event.remove(t.id + '_menu', 'keydown', t._keyHandler); + DOM.hide(t.id + '_menu'); + } - t.isMenuVisible = 0; + t.isMenuVisible = 0; + t.onHideMenu.dispatch(); + } }, renderMenu : function() { - var t = this, m, i = 0, s = t.settings, n, tb, tr, w; + var t = this, m, i = 0, s = t.settings, n, tb, tr, w, context; - w = DOM.add(s.menu_container, 'div', {id : t.id + '_menu', 'class' : s['menu_class'] + ' ' + s['class'], style : 'position:absolute;left:0;top:-1000px;'}); + w = DOM.add(s.menu_container, 'div', {role: 'listbox', id : t.id + '_menu', 'class' : s['menu_class'] + ' ' + s['class'], style : 'position:absolute;left:0;top:-1000px;'}); m = DOM.add(w, 'div', {'class' : s['class'] + ' mceSplitButtonMenu'}); DOM.add(m, 'span', {'class' : 'mceMenuLine'}); - n = DOM.add(m, 'table', {'class' : 'mceColorSplitMenu'}); + n = DOM.add(m, 'table', {role: 'presentation', 'class' : 'mceColorSplitMenu'}); tb = DOM.add(n, 'tbody'); // Generate color grid @@ -8668,20 +10501,32 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { } n = DOM.add(tr, 'td'); - n = DOM.add(n, 'a', { + role : 'option', href : 'javascript:;', style : { backgroundColor : '#' + c }, - _mce_color : '#' + c + 'title': t.editor.getLang('colors.' + c, c), + 'data-mce-color' : '#' + c }); + + if (t.editor.forcedHighContrastMode) { + n = DOM.add(n, 'canvas', { width: 16, height: 16, 'aria-hidden': 'true' }); + if (n.getContext && (context = n.getContext("2d"))) { + context.fillStyle = '#' + c; + context.fillRect(0, 0, 16, 16); + } else { + // No point leaving a canvas element around if it's not supported for drawing on anyway. + DOM.remove(n); + } + } }); if (s.more_colors_func) { n = DOM.add(tb, 'tr'); n = DOM.add(n, 'td', {colspan : s.grid_width, 'class' : 'mceMoreColors'}); - n = DOM.add(n, 'a', {id : t.id + '_more', href : 'javascript:;', onclick : 'return false;', 'class' : 'mceMoreColors'}, s.more_colors_title); + n = DOM.add(n, 'a', {role: 'option', id : t.id + '_more', href : 'javascript:;', onclick : 'return false;', 'class' : 'mceMoreColors'}, s.more_colors_title); Event.add(n, 'click', function(e) { s.more_colors_func.call(s.more_colors_scope || this); @@ -8690,13 +10535,25 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { } DOM.addClass(m, 'mceColorSplitMenu'); + + new tinymce.ui.KeyboardNavigation({ + root: t.id + '_menu', + items: DOM.select('a', t.id + '_menu'), + onCancel: function() { + t.hideMenu(); + t.focus(); + } + }); + + // Prevent IE from scrolling and hindering click to occur #4019 + Event.add(t.id + '_menu', 'mousedown', function(e) {return Event.cancel(e);}); Event.add(t.id + '_menu', 'click', function(e) { var c; - e = e.target; + e = DOM.getParent(e.target, 'a', tb); - if (e.nodeName == 'A' && (c = e.getAttribute('_mce_color'))) + if (e && e.nodeName.toLowerCase() == 'a' && (c = e.getAttribute('data-mce-color'))) t.setColor(c); return Event.cancel(e); // Prevent IE auto save warning @@ -8706,13 +10563,17 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { }, setColor : function(c) { + this.displayColor(c); + this.hideMenu(); + this.settings.onselect(c); + }, + + displayColor : function(c) { var t = this; DOM.setStyle(t.id + '_preview', 'backgroundColor', c); t.value = c; - t.hideMenu(); - t.settings.onselect(c); }, postRender : function() { @@ -8733,9 +10594,72 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { }); })(tinymce); +(function(tinymce) { +// Shorten class names +var dom = tinymce.DOM, each = tinymce.each, Event = tinymce.dom.Event; +tinymce.create('tinymce.ui.ToolbarGroup:tinymce.ui.Container', { + renderHTML : function() { + var t = this, h = [], controls = t.controls, each = tinymce.each, settings = t.settings; + + h.push('
    '); + //TODO: ACC test this out - adding a role = application for getting the landmarks working well. + h.push(""); + h.push(''); + each(controls, function(toolbar) { + h.push(toolbar.renderHTML()); + }); + h.push(""); + h.push('
    '); + + return h.join(''); + }, + + focus : function() { + var t = this; + dom.get(t.id).focus(); + }, + + postRender : function() { + var t = this, items = []; + + each(t.controls, function(toolbar) { + each (toolbar.controls, function(control) { + if (control.id) { + items.push(control); + } + }); + }); + + t.keyNav = new tinymce.ui.KeyboardNavigation({ + root: t.id, + items: items, + onCancel: function() { + //Move focus if webkit so that navigation back will read the item. + if (tinymce.isWebKit) { + dom.get(t.editor.id+"_ifr").focus(); + } + t.editor.focus(); + }, + excludeFromTabOrder: !t.settings.tab_focus_toolbar + }); + }, + + destroy : function() { + var self = this; + + self.parent(); + self.keyNav.destroy(); + Event.clear(self.id); + } +}); +})(tinymce); + +(function(tinymce) { +// Shorten class names +var dom = tinymce.DOM, each = tinymce.each; tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { renderHTML : function() { - var t = this, h = '', c, co, dom = tinymce.DOM, s = t.settings, i, pr, nx, cl; + var t = this, h = '', c, co, s = t.settings, i, pr, nx, cl; cl = t.controls; for (i=0; i')); - return dom.createHTML('table', {id : t.id, 'class' : 'mceToolbar' + (s['class'] ? ' ' + s['class'] : ''), cellpadding : '0', cellspacing : '0', align : t.settings.align || ''}, '' + h + ''); + return dom.createHTML('table', {id : t.id, 'class' : 'mceToolbar' + (s['class'] ? ' ' + s['class'] : ''), cellpadding : '0', cellspacing : '0', align : t.settings.align || '', role: 'presentation', tabindex: '-1'}, '' + h + ''); } }); +})(tinymce); (function(tinymce) { var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each; tinymce.create('tinymce.AddOnManager', { - items : [], - urls : {}, - lookup : {}, + AddOnManager : function() { + var self = this; - onAdd : new Dispatcher(this), + self.items = []; + self.urls = {}; + self.lookup = {}; + self.onAdd = new Dispatcher(self); + }, get : function(n) { - return this.lookup[n]; + if (this.lookup[n]) { + return this.lookup[n].instance; + } else { + return undefined; + } + }, + + dependencies : function(n) { + var result; + if (this.lookup[n]) { + result = this.lookup[n].dependencies; + } + return result || []; }, requireLangPack : function(n) { var s = tinymce.settings; - if (s && s.language) + if (s && s.language && s.language_load !== false) tinymce.ScriptLoader.add(this.urls[n] + '/langs/' + s.language + '.js'); }, - add : function(id, o) { + add : function(id, o, dependencies) { this.items.push(o); - this.lookup[id] = o; + this.lookup[id] = {instance:o, dependencies:dependencies}; this.onAdd.dispatch(this, id, o); return o; }, + createUrl: function(baseUrl, dep) { + if (typeof dep === "object") { + return dep + } else { + return {prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix}; + } + }, + + addComponents: function(pluginName, scripts) { + var pluginUrl = this.urls[pluginName]; + tinymce.each(scripts, function(script){ + tinymce.ScriptLoader.add(pluginUrl+"/"+script); + }); + }, load : function(n, u, cb, s) { - var t = this; + var t = this, url = u; + + function loadDependencies() { + var dependencies = t.dependencies(n); + tinymce.each(dependencies, function(dep) { + var newUrl = t.createUrl(u, dep); + t.load(newUrl.resource, newUrl, undefined, undefined); + }); + if (cb) { + if (s) { + cb.call(s); + } else { + cb.call(tinymce.ScriptLoader); + } + } + } if (t.urls[n]) return; + if (typeof u === "object") + url = u.prefix + u.resource + u.suffix; - if (u.indexOf('/') != 0 && u.indexOf('://') == -1) - u = tinymce.baseURL + '/' + u; + if (url.indexOf('/') != 0 && url.indexOf('://') == -1) + url = tinymce.baseURL + '/' + url; - t.urls[n] = u.substring(0, u.lastIndexOf('/')); - tinymce.ScriptLoader.add(u, cb, s); + t.urls[n] = url.substring(0, url.lastIndexOf('/')); + + if (t.lookup[n]) { + loadDependencies(); + } else { + tinymce.ScriptLoader.add(url, loadDependencies, s); + } } }); @@ -9289,10 +11265,10 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { visual_table_class : 'mceItemTable', visual : 1, font_size_style_values : 'xx-small,x-small,small,medium,large,x-large,xx-large', + font_size_legacy_values : 'xx-small,small,medium,large,x-large,xx-large,300%', // See: http://www.w3.org/TR/CSS2/fonts.html#propdef-font-size apply_source_formatting : 1, directionality : 'ltr', forced_root_block : 'p', - valid_elements : '@[id|class|style|title|dir
     
    - +
    @@ -166,6 +167,7 @@ diff --git a/js/tiny_mce/plugins/table/editor_plugin.js b/js/tiny_mce/plugins/table/editor_plugin.js index 266d7d5371..2f3b0e2d7a 100644 --- a/js/tiny_mce/plugins/table/editor_plugin.js +++ b/js/tiny_mce/plugins/table/editor_plugin.js @@ -1 +1 @@ -(function(b){var c=b.each;function a(F,E,I){var e,J,B,n;r();n=E.getParent(I.getStart(),"th,td");if(n){J=D(n);B=G();n=v(J.x,J.y)}function w(L,K){L=L.cloneNode(K);L.removeAttribute("id");return L}function r(){var K=0;e=[];c(["thead","tbody","tfoot"],function(L){var M=E.select(L+" tr",F);c(M,function(N,O){O+=K;c(E.select("td,th",N),function(U,P){var Q,R,S,T;if(e[O]){while(e[O][P]){P++}}S=g(U,"rowspan");T=g(U,"colspan");for(R=O;R'}return false}},"childNodes");K=w(K,false);K.rowSpan=K.colSpan=1;if(L){K.appendChild(L)}else{if(!b.isIE){K.innerHTML='
    '}}return K}function p(){var K=E.createRng();c(E.select("tr",F),function(L){if(L.cells.length==0){E.remove(L)}});if(E.select("tr",F).length==0){K.setStartAfter(F);K.setEndAfter(F);I.setRng(K);E.remove(F);return}c(E.select("thead,tbody,tfoot",F),function(L){if(L.rows.length==0){E.remove(L)}});r();row=e[Math.min(e.length-1,J.y)];if(row){I.select(row[Math.min(row.length-1,J.x)].elm,true);I.collapse(true)}}function s(Q,O,S,P){var N,L,K,M,R;N=e[O][Q].elm.parentNode;for(K=1;K<=S;K++){N=E.getNext(N,"tr");if(N){for(L=Q;L>=0;L--){R=e[O+K][L].elm;if(R.parentNode==N){for(M=1;M<=P;M++){E.insertAfter(d(R),R)}break}}if(L==-1){for(M=1;M<=P;M++){N.insertBefore(d(N.cells[0]),N.cells[0])}}}}}function A(){c(e,function(K,L){c(K,function(N,M){var Q,P,R,O;if(h(N)){N=N.elm;Q=g(N,"colspan");P=g(N,"rowspan");if(Q>1||P>1){N.colSpan=N.rowSpan=1;for(O=0;O1){P.rowSpan=rowSpan+1;continue}}else{if(K>0&&e[K-1][O]){S=e[K-1][O].elm;rowSpan=g(S,"rowspan");if(rowSpan>1){S.rowSpan=rowSpan+1;continue}}}L=d(P);L.colSpan=P.colSpan;R.appendChild(L);M=P}}if(R.hasChildNodes()){if(!N){E.insertAfter(R,Q)}else{Q.parentNode.insertBefore(R,Q)}}}function f(L){var M,K;c(e,function(N,O){c(N,function(Q,P){if(h(Q)){M=P;if(L){return false}}});if(L){return !M}});c(e,function(Q,R){var N=Q[M].elm,O,P;if(N!=K){P=g(N,"colspan");O=g(N,"rowspan");if(P==1){if(!L){E.insertAfter(d(N),N);s(M,R,O-1,P)}else{N.parentNode.insertBefore(d(N),N);s(M,R,O-1,P)}}else{N.colSpan++}K=N}})}function m(){var K=[];c(e,function(L,M){c(L,function(O,N){if(h(O)&&b.inArray(K,N)===-1){c(e,function(R){var P=R[N].elm,Q;Q=g(P,"colspan");if(Q>1){P.colSpan=Q-1}else{E.remove(P)}});K.push(N)}})});p()}function l(){var L;function K(O){var N,P,M;N=E.getNext(O,"tr");c(O.cells,function(Q){var R=g(Q,"rowspan");if(R>1){Q.rowSpan=R-1;P=D(Q);s(P.x,P.y,1,1)}});P=D(O.cells[0]);c(e[P.y],function(Q){var R;Q=Q.elm;if(Q!=M){R=g(Q,"rowspan");if(R<=1){E.remove(Q)}else{Q.rowSpan=R-1}M=Q}})}L=j();c(L.reverse(),function(M){K(M)});p()}function C(){var K=j();E.remove(K);p();return K}function H(){var K=j();c(K,function(M,L){K[L]=w(M,true)});return K}function z(M,L){var N=j(),K=N[L?0:N.length-1],O=K.cells.length;c(e,function(Q){var P;O=0;c(Q,function(S,R){if(S.real){O+=S.colspan}if(S.elm.parentNode==K){P=1}});if(P){return false}});if(!L){M.reverse()}c(M,function(R){var Q=R.cells.length,P;for(i=0;iL){L=P}if(O>K){K=O}if(Q.real){S=Q.colspan-1;R=Q.rowspan-1;if(S){if(P+S>L){L=P+S}}if(R){if(O+R>K){K=O+R}}}}})});return{x:L,y:K}}function t(Q){var N,M,S,R,L,K,O,P;B=D(Q);if(J&&B){N=Math.min(J.x,B.x);M=Math.min(J.y,B.y);S=Math.max(J.x,B.x);R=Math.max(J.y,B.y);L=S;K=R;for(y=M;y<=K;y++){Q=e[y][N];if(!Q.real){if(N-(Q.colspan-1)L){L=x+O}}if(P){if(y+P>K){K=y+P}}}}}E.removeClass(E.select("td.mceSelected,th.mceSelected"),"mceSelected");for(y=M;y<=K;y++){for(x=N;x<=L;x++){E.addClass(e[y][x].elm,"mceSelected")}}}}b.extend(this,{deleteTable:q,split:A,merge:o,insertRow:k,insertCol:f,deleteCols:m,deleteRows:l,cutRows:C,copyRows:H,pasteRows:z,getPos:D,setStartCell:u,setEndCell:t})}b.create("tinymce.plugins.TablePlugin",{init:function(e,f){var d,j;function h(m){var l=e.selection,k=e.dom.getParent(m||l.getNode(),"table");if(k){return new a(k,e.dom,l)}}function g(){e.getBody().style.webkitUserSelect="";e.dom.removeClass(e.dom.select("td.mceSelected,th.mceSelected"),"mceSelected")}c([["table","table.desc","mceInsertTable",true],["delete_table","table.del","mceTableDelete"],["delete_col","table.delete_col_desc","mceTableDeleteCol"],["delete_row","table.delete_row_desc","mceTableDeleteRow"],["col_after","table.col_after_desc","mceTableInsertColAfter"],["col_before","table.col_before_desc","mceTableInsertColBefore"],["row_after","table.row_after_desc","mceTableInsertRowAfter"],["row_before","table.row_before_desc","mceTableInsertRowBefore"],["row_props","table.row_desc","mceTableRowProps",true],["cell_props","table.cell_desc","mceTableCellProps",true],["split_cells","table.split_cells_desc","mceTableSplitCells",true],["merge_cells","table.merge_cells_desc","mceTableMergeCells",true]],function(k){e.addButton(k[0],{title:k[1],cmd:k[2],ui:k[3]})});if(!b.isIE){e.onClick.add(function(k,l){l=l.target;if(l.nodeName==="TABLE"){k.selection.select(l)}})}e.onNodeChange.add(function(l,k,o){var m;o=l.selection.getStart();m=l.dom.getParent(o,"td,th,caption");k.setActive("table",o.nodeName==="TABLE"||!!m);if(m&&m.nodeName==="CAPTION"){m=0}k.setDisabled("delete_table",!m);k.setDisabled("delete_col",!m);k.setDisabled("delete_table",!m);k.setDisabled("delete_row",!m);k.setDisabled("col_after",!m);k.setDisabled("col_before",!m);k.setDisabled("row_after",!m);k.setDisabled("row_before",!m);k.setDisabled("row_props",!m);k.setDisabled("cell_props",!m);k.setDisabled("split_cells",!m);k.setDisabled("merge_cells",!m)});e.onInit.add(function(l){var k,o,p=l.dom,m;d=l.windowManager;l.onMouseDown.add(function(q,r){if(r.button!=2){g();o=p.getParent(r.target,"td,th");k=p.getParent(o,"table")}});p.bind(l.getDoc(),"mouseover",function(t){var r,q,s=t.target;if(o&&(m||s!=o)&&(s.nodeName=="TD"||s.nodeName=="TH")){q=p.getParent(s,"table");if(q==k){if(!m){m=h(q);m.setStartCell(o);l.getBody().style.webkitUserSelect="none"}m.setEndCell(s)}r=l.selection.getSel();if(r.removeAllRanges){r.removeAllRanges()}else{r.empty()}t.preventDefault()}});l.onMouseUp.add(function(z,A){var r,t=z.selection,B,C=t.getSel(),q,u,s,w;if(o){if(m){z.getBody().style.webkitUserSelect=""}function v(D,F){var E=new b.dom.TreeWalker(D,D);do{if(D.nodeType==3&&b.trim(D.nodeValue).length!=0){if(F){r.setStart(D,0)}else{r.setEnd(D,D.nodeValue.length)}return}if(D.nodeName=="BR"){if(F){r.setStartBefore(D)}else{r.setEndBefore(D)}return}}while(D=(F?E.next():E.prev()))}B=p.select("td.mceSelected,th.mceSelected");if(B.length>0){r=p.createRng();u=B[0];w=B[B.length-1];v(u,1);q=new b.dom.TreeWalker(u,p.getParent(B[0],"table"));do{if(u.nodeName=="TD"||u.nodeName=="TH"){if(!p.hasClass(u,"mceSelected")){break}s=u}}while(u=q.next());v(s);t.setRng(r)}z.nodeChanged();o=m=k=null}});l.onKeyUp.add(function(q,r){g()});if(l&&l.plugins.contextmenu){l.plugins.contextmenu.onContextMenu.add(function(s,q,u){var v,t=l.selection,r=t.getNode()||l.getBody();if(l.dom.getParent(u,"td")||l.dom.getParent(u,"th")||l.dom.select("td.mceSelected,th.mceSelected").length){q.removeAll();if(r.nodeName=="A"&&!l.dom.getAttrib(r,"name")){q.add({title:"advanced.link_desc",icon:"link",cmd:l.plugins.advlink?"mceAdvLink":"mceLink",ui:true});q.add({title:"advanced.unlink_desc",icon:"unlink",cmd:"UnLink"});q.addSeparator()}if(r.nodeName=="IMG"&&r.className.indexOf("mceItem")==-1){q.add({title:"advanced.image_desc",icon:"image",cmd:l.plugins.advimage?"mceAdvImage":"mceImage",ui:true});q.addSeparator()}q.add({title:"table.desc",icon:"table",cmd:"mceInsertTable",value:{action:"insert"}});q.add({title:"table.props_desc",icon:"table_props",cmd:"mceInsertTable"});q.add({title:"table.del",icon:"delete_table",cmd:"mceTableDelete"});q.addSeparator();v=q.addMenu({title:"table.cell"});v.add({title:"table.cell_desc",icon:"cell_props",cmd:"mceTableCellProps"});v.add({title:"table.split_cells_desc",icon:"split_cells",cmd:"mceTableSplitCells"});v.add({title:"table.merge_cells_desc",icon:"merge_cells",cmd:"mceTableMergeCells"});v=q.addMenu({title:"table.row"});v.add({title:"table.row_desc",icon:"row_props",cmd:"mceTableRowProps"});v.add({title:"table.row_before_desc",icon:"row_before",cmd:"mceTableInsertRowBefore"});v.add({title:"table.row_after_desc",icon:"row_after",cmd:"mceTableInsertRowAfter"});v.add({title:"table.delete_row_desc",icon:"delete_row",cmd:"mceTableDeleteRow"});v.addSeparator();v.add({title:"table.cut_row_desc",icon:"cut",cmd:"mceTableCutRow"});v.add({title:"table.copy_row_desc",icon:"copy",cmd:"mceTableCopyRow"});v.add({title:"table.paste_row_before_desc",icon:"paste",cmd:"mceTablePasteRowBefore"}).setDisabled(!j);v.add({title:"table.paste_row_after_desc",icon:"paste",cmd:"mceTablePasteRowAfter"}).setDisabled(!j);v=q.addMenu({title:"table.col"});v.add({title:"table.col_before_desc",icon:"col_before",cmd:"mceTableInsertColBefore"});v.add({title:"table.col_after_desc",icon:"col_after",cmd:"mceTableInsertColAfter"});v.add({title:"table.delete_col_desc",icon:"delete_col",cmd:"mceTableDeleteCol"})}else{q.add({title:"table.desc",icon:"table",cmd:"mceInsertTable"})}})}if(!b.isIE){function n(){var q;for(q=l.getBody().lastChild;q&&q.nodeType==3&&!q.nodeValue.length;q=q.previousSibling){}if(q&&q.nodeName=="TABLE"){l.dom.add(l.getBody(),"p",null,'
    ')}}if(b.isGecko){l.onKeyDown.add(function(r,t){var q,s,u=r.dom;if(t.keyCode==37||t.keyCode==38){q=r.selection.getRng();s=u.getParent(q.startContainer,"table");if(s&&r.getBody().firstChild==s){if(isAtStart(q,s)){q=u.createRng();q.setStartBefore(s);q.setEndBefore(s);r.selection.setRng(q);t.preventDefault()}}}})}l.onKeyUp.add(n);l.onSetContent.add(n);l.onVisualAid.add(n);l.onPreProcess.add(function(q,s){var r=s.node.lastChild;if(r&&r.childNodes.length==1&&r.firstChild.nodeName=="BR"){q.dom.remove(r)}});n()}});c({mceTableSplitCells:function(k){k.split()},mceTableMergeCells:function(l){var m,n,k;k=e.dom.getParent(e.selection.getNode(),"th,td");if(k){m=k.rowSpan;n=k.colSpan}if(!e.dom.select("td.mceSelected,th.mceSelected").length){d.open({url:f+"/merge_cells.htm",width:240+parseInt(e.getLang("table.merge_cells_delta_width",0)),height:110+parseInt(e.getLang("table.merge_cells_delta_height",0)),inline:1},{rows:m,cols:n,onaction:function(o){l.merge(k,o.cols,o.rows)},plugin_url:f})}else{l.merge()}},mceTableInsertRowBefore:function(k){k.insertRow(true)},mceTableInsertRowAfter:function(k){k.insertRow()},mceTableInsertColBefore:function(k){k.insertCol(true)},mceTableInsertColAfter:function(k){k.insertCol()},mceTableDeleteCol:function(k){k.deleteCols()},mceTableDeleteRow:function(k){k.deleteRows()},mceTableCutRow:function(k){j=k.cutRows()},mceTableCopyRow:function(k){j=k.copyRows()},mceTablePasteRowBefore:function(k){k.pasteRows(j,true)},mceTablePasteRowAfter:function(k){k.pasteRows(j)},mceTableDelete:function(k){k.deleteTable()}},function(l,k){e.addCommand(k,function(){var m=h();if(m){l(m);e.execCommand("mceRepaint");g()}})});c({mceInsertTable:function(k){d.open({url:f+"/table.htm",width:400+parseInt(e.getLang("table.table_delta_width",0)),height:320+parseInt(e.getLang("table.table_delta_height",0)),inline:1},{plugin_url:f,action:k?k.action:0})},mceTableRowProps:function(){d.open({url:f+"/row.htm",width:400+parseInt(e.getLang("table.rowprops_delta_width",0)),height:295+parseInt(e.getLang("table.rowprops_delta_height",0)),inline:1},{plugin_url:f})},mceTableCellProps:function(){d.open({url:f+"/cell.htm",width:400+parseInt(e.getLang("table.cellprops_delta_width",0)),height:295+parseInt(e.getLang("table.cellprops_delta_height",0)),inline:1},{plugin_url:f})}},function(l,k){e.addCommand(k,function(m,n){l(n)})})}});b.PluginManager.add("table",b.plugins.TablePlugin)})(tinymce); \ No newline at end of file +(function(d){var e=d.each;function c(g,h){var j=h.ownerDocument,f=j.createRange(),k;f.setStartBefore(h);f.setEnd(g.endContainer,g.endOffset);k=j.createElement("body");k.appendChild(f.cloneContents());return k.innerHTML.replace(/<(br|img|object|embed|input|textarea)[^>]*>/gi,"-").replace(/<[^>]+>/g,"").length==0}function a(g,f){return parseInt(g.getAttribute(f)||1)}function b(H,G,K){var g,L,D,o;t();o=G.getParent(K.getStart(),"th,td");if(o){L=F(o);D=I();o=z(L.x,L.y)}function A(N,M){N=N.cloneNode(M);N.removeAttribute("id");return N}function t(){var M=0;g=[];e(["thead","tbody","tfoot"],function(N){var O=G.select("> "+N+" tr",H);e(O,function(P,Q){Q+=M;e(G.select("> td, > th",P),function(W,R){var S,T,U,V;if(g[Q]){while(g[Q][R]){R++}}U=a(W,"rowspan");V=a(W,"colspan");for(T=Q;T'}return false}},"childNodes");M=A(M,false);s(M,"rowSpan",1);s(M,"colSpan",1);if(N){M.appendChild(N)}else{if(!d.isIE){M.innerHTML='
    '}}return M}function q(){var M=G.createRng();e(G.select("tr",H),function(N){if(N.cells.length==0){G.remove(N)}});if(G.select("tr",H).length==0){M.setStartAfter(H);M.setEndAfter(H);K.setRng(M);G.remove(H);return}e(G.select("thead,tbody,tfoot",H),function(N){if(N.rows.length==0){G.remove(N)}});t();row=g[Math.min(g.length-1,L.y)];if(row){K.select(row[Math.min(row.length-1,L.x)].elm,true);K.collapse(true)}}function u(S,Q,U,R){var P,N,M,O,T;P=g[Q][S].elm.parentNode;for(M=1;M<=U;M++){P=G.getNext(P,"tr");if(P){for(N=S;N>=0;N--){T=g[Q+M][N].elm;if(T.parentNode==P){for(O=1;O<=R;O++){G.insertAfter(f(T),T)}break}}if(N==-1){for(O=1;O<=R;O++){P.insertBefore(f(P.cells[0]),P.cells[0])}}}}}function C(){e(g,function(M,N){e(M,function(P,O){var S,R,T,Q;if(j(P)){P=P.elm;S=a(P,"colspan");R=a(P,"rowspan");if(S>1||R>1){s(P,"rowSpan",1);s(P,"colSpan",1);for(Q=0;Q1){s(S,"rowSpan",O+1);continue}}else{if(M>0&&g[M-1][R]){V=g[M-1][R].elm;O=a(V,"rowSpan");if(O>1){s(V,"rowSpan",O+1);continue}}}N=f(S);s(N,"colSpan",S.colSpan);U.appendChild(N);P=S}}if(U.hasChildNodes()){if(!Q){G.insertAfter(U,T)}else{T.parentNode.insertBefore(U,T)}}}function h(N){var O,M;e(g,function(P,Q){e(P,function(S,R){if(j(S)){O=R;if(N){return false}}});if(N){return !O}});e(g,function(S,T){var P,Q,R;if(!S[O]){return}P=S[O].elm;if(P!=M){R=a(P,"colspan");Q=a(P,"rowspan");if(R==1){if(!N){G.insertAfter(f(P),P);u(O,T,Q-1,R)}else{P.parentNode.insertBefore(f(P),P);u(O,T,Q-1,R)}}else{s(P,"colSpan",P.colSpan+1)}M=P}})}function n(){var M=[];e(g,function(N,O){e(N,function(Q,P){if(j(Q)&&d.inArray(M,P)===-1){e(g,function(T){var R=T[P].elm,S;S=a(R,"colSpan");if(S>1){s(R,"colSpan",S-1)}else{G.remove(R)}});M.push(P)}})});q()}function m(){var N;function M(Q){var P,R,O;P=G.getNext(Q,"tr");e(Q.cells,function(S){var T=a(S,"rowSpan");if(T>1){s(S,"rowSpan",T-1);R=F(S);u(R.x,R.y,1,1)}});R=F(Q.cells[0]);e(g[R.y],function(S){var T;S=S.elm;if(S!=O){T=a(S,"rowSpan");if(T<=1){G.remove(S)}else{s(S,"rowSpan",T-1)}O=S}})}N=k();e(N.reverse(),function(O){M(O)});q()}function E(){var M=k();G.remove(M);q();return M}function J(){var M=k();e(M,function(O,N){M[N]=A(O,true)});return M}function B(O,N){var P=k(),M=P[N?0:P.length-1],Q=M.cells.length;e(g,function(S){var R;Q=0;e(S,function(U,T){if(U.real){Q+=U.colspan}if(U.elm.parentNode==M){R=1}});if(R){return false}});if(!N){O.reverse()}e(O,function(T){var S=T.cells.length,R;for(i=0;iN){N=R}if(Q>M){M=Q}if(S.real){U=S.colspan-1;T=S.rowspan-1;if(U){if(R+U>N){N=R+U}}if(T){if(Q+T>M){M=Q+T}}}}})});return{x:N,y:M}}function v(S){var P,O,U,T,N,M,Q,R;D=F(S);if(L&&D){P=Math.min(L.x,D.x);O=Math.min(L.y,D.y);U=Math.max(L.x,D.x);T=Math.max(L.y,D.y);N=U;M=T;for(y=O;y<=M;y++){S=g[y][P];if(!S.real){if(P-(S.colspan-1)N){N=x+Q}}if(R){if(y+R>M){M=y+R}}}}}G.removeClass(G.select("td.mceSelected,th.mceSelected"),"mceSelected");for(y=O;y<=M;y++){for(x=P;x<=N;x++){if(g[y][x]){G.addClass(g[y][x].elm,"mceSelected")}}}}}d.extend(this,{deleteTable:r,split:C,merge:p,insertRow:l,insertCol:h,deleteCols:n,deleteRows:m,cutRows:E,copyRows:J,pasteRows:B,getPos:F,setStartCell:w,setEndCell:v})}d.create("tinymce.plugins.TablePlugin",{init:function(g,h){var f,m,j=true;function l(p){var o=g.selection,n=g.dom.getParent(p||o.getNode(),"table");if(n){return new b(n,g.dom,o)}}function k(){g.getBody().style.webkitUserSelect="";if(j){g.dom.removeClass(g.dom.select("td.mceSelected,th.mceSelected"),"mceSelected");j=false}}e([["table","table.desc","mceInsertTable",true],["delete_table","table.del","mceTableDelete"],["delete_col","table.delete_col_desc","mceTableDeleteCol"],["delete_row","table.delete_row_desc","mceTableDeleteRow"],["col_after","table.col_after_desc","mceTableInsertColAfter"],["col_before","table.col_before_desc","mceTableInsertColBefore"],["row_after","table.row_after_desc","mceTableInsertRowAfter"],["row_before","table.row_before_desc","mceTableInsertRowBefore"],["row_props","table.row_desc","mceTableRowProps",true],["cell_props","table.cell_desc","mceTableCellProps",true],["split_cells","table.split_cells_desc","mceTableSplitCells",true],["merge_cells","table.merge_cells_desc","mceTableMergeCells",true]],function(n){g.addButton(n[0],{title:n[1],cmd:n[2],ui:n[3]})});if(!d.isIE){g.onClick.add(function(n,o){o=o.target;if(o.nodeName==="TABLE"){n.selection.select(o);n.nodeChanged()}})}g.onPreProcess.add(function(o,p){var n,q,r,t=o.dom,s;n=t.select("table",p.node);q=n.length;while(q--){r=n[q];t.setAttrib(r,"data-mce-style","");if((s=t.getAttrib(r,"width"))){t.setStyle(r,"width",s);t.setAttrib(r,"width","")}if((s=t.getAttrib(r,"height"))){t.setStyle(r,"height",s);t.setAttrib(r,"height","")}}});g.onNodeChange.add(function(q,o,s){var r;s=q.selection.getStart();r=q.dom.getParent(s,"td,th,caption");o.setActive("table",s.nodeName==="TABLE"||!!r);if(r&&r.nodeName==="CAPTION"){r=0}o.setDisabled("delete_table",!r);o.setDisabled("delete_col",!r);o.setDisabled("delete_table",!r);o.setDisabled("delete_row",!r);o.setDisabled("col_after",!r);o.setDisabled("col_before",!r);o.setDisabled("row_after",!r);o.setDisabled("row_before",!r);o.setDisabled("row_props",!r);o.setDisabled("cell_props",!r);o.setDisabled("split_cells",!r);o.setDisabled("merge_cells",!r)});g.onInit.add(function(r){var p,t,q=r.dom,u;f=r.windowManager;r.onMouseDown.add(function(w,z){if(z.button!=2){k();t=q.getParent(z.target,"td,th");p=q.getParent(t,"table")}});q.bind(r.getDoc(),"mouseover",function(C){var A,z,B=C.target;if(t&&(u||B!=t)&&(B.nodeName=="TD"||B.nodeName=="TH")){z=q.getParent(B,"table");if(z==p){if(!u){u=l(z);u.setStartCell(t);r.getBody().style.webkitUserSelect="none"}u.setEndCell(B);j=true}A=r.selection.getSel();try{if(A.removeAllRanges){A.removeAllRanges()}else{A.empty()}}catch(w){}C.preventDefault()}});r.onMouseUp.add(function(F,G){var z,B=F.selection,H,I=B.getSel(),w,C,A,E;if(t){if(u){F.getBody().style.webkitUserSelect=""}function D(J,L){var K=new d.dom.TreeWalker(J,J);do{if(J.nodeType==3&&d.trim(J.nodeValue).length!=0){if(L){z.setStart(J,0)}else{z.setEnd(J,J.nodeValue.length)}return}if(J.nodeName=="BR"){if(L){z.setStartBefore(J)}else{z.setEndBefore(J)}return}}while(J=(L?K.next():K.prev()))}H=q.select("td.mceSelected,th.mceSelected");if(H.length>0){z=q.createRng();C=H[0];E=H[H.length-1];z.setStartBefore(C);z.setEndAfter(C);D(C,1);w=new d.dom.TreeWalker(C,q.getParent(H[0],"table"));do{if(C.nodeName=="TD"||C.nodeName=="TH"){if(!q.hasClass(C,"mceSelected")){break}A=C}}while(C=w.next());D(A);B.setRng(z)}F.nodeChanged();t=u=p=null}});r.onKeyUp.add(function(w,z){k()});r.onKeyDown.add(function(w,z){n(w)});r.onMouseDown.add(function(w,z){if(z.button!=2){n(w)}});function o(D,z,A,F){var B=3,G=D.dom.getParent(z.startContainer,"TABLE"),C,w,E;if(G){C=G.parentNode}w=z.startContainer.nodeType==B&&z.startOffset==0&&z.endOffset==0&&F&&(A.nodeName=="TR"||A==C);E=(A.nodeName=="TD"||A.nodeName=="TH")&&!F;return w||E}function n(A){if(!d.isWebKit){return}var z=A.selection.getRng();var C=A.selection.getNode();var B=A.dom.getParent(z.startContainer,"TD,TH");if(!o(A,z,C,B)){return}if(!B){B=C}var w=B.lastChild;while(w.lastChild){w=w.lastChild}z.setEnd(w,w.nodeValue.length);A.selection.setRng(z)}r.plugins.table.fixTableCellSelection=n;if(r&&r.plugins.contextmenu){r.plugins.contextmenu.onContextMenu.add(function(A,w,C){var D,B=r.selection,z=B.getNode()||r.getBody();if(r.dom.getParent(C,"td")||r.dom.getParent(C,"th")||r.dom.select("td.mceSelected,th.mceSelected").length){w.removeAll();if(z.nodeName=="A"&&!r.dom.getAttrib(z,"name")){w.add({title:"advanced.link_desc",icon:"link",cmd:r.plugins.advlink?"mceAdvLink":"mceLink",ui:true});w.add({title:"advanced.unlink_desc",icon:"unlink",cmd:"UnLink"});w.addSeparator()}if(z.nodeName=="IMG"&&z.className.indexOf("mceItem")==-1){w.add({title:"advanced.image_desc",icon:"image",cmd:r.plugins.advimage?"mceAdvImage":"mceImage",ui:true});w.addSeparator()}w.add({title:"table.desc",icon:"table",cmd:"mceInsertTable",value:{action:"insert"}});w.add({title:"table.props_desc",icon:"table_props",cmd:"mceInsertTable"});w.add({title:"table.del",icon:"delete_table",cmd:"mceTableDelete"});w.addSeparator();D=w.addMenu({title:"table.cell"});D.add({title:"table.cell_desc",icon:"cell_props",cmd:"mceTableCellProps"});D.add({title:"table.split_cells_desc",icon:"split_cells",cmd:"mceTableSplitCells"});D.add({title:"table.merge_cells_desc",icon:"merge_cells",cmd:"mceTableMergeCells"});D=w.addMenu({title:"table.row"});D.add({title:"table.row_desc",icon:"row_props",cmd:"mceTableRowProps"});D.add({title:"table.row_before_desc",icon:"row_before",cmd:"mceTableInsertRowBefore"});D.add({title:"table.row_after_desc",icon:"row_after",cmd:"mceTableInsertRowAfter"});D.add({title:"table.delete_row_desc",icon:"delete_row",cmd:"mceTableDeleteRow"});D.addSeparator();D.add({title:"table.cut_row_desc",icon:"cut",cmd:"mceTableCutRow"});D.add({title:"table.copy_row_desc",icon:"copy",cmd:"mceTableCopyRow"});D.add({title:"table.paste_row_before_desc",icon:"paste",cmd:"mceTablePasteRowBefore"}).setDisabled(!m);D.add({title:"table.paste_row_after_desc",icon:"paste",cmd:"mceTablePasteRowAfter"}).setDisabled(!m);D=w.addMenu({title:"table.col"});D.add({title:"table.col_before_desc",icon:"col_before",cmd:"mceTableInsertColBefore"});D.add({title:"table.col_after_desc",icon:"col_after",cmd:"mceTableInsertColAfter"});D.add({title:"table.delete_col_desc",icon:"delete_col",cmd:"mceTableDeleteCol"})}else{w.add({title:"table.desc",icon:"table",cmd:"mceInsertTable"})}})}if(d.isWebKit){function v(C,N){var L=d.VK;var Q=N.keyCode;function O(Y,U,S){var T=Y?"previousSibling":"nextSibling";var Z=C.dom.getParent(U,"tr");var X=Z[T];if(X){z(C,U,X,Y);d.dom.Event.cancel(S);return true}else{var aa=C.dom.getParent(Z,"table");var W=Z.parentNode;var R=W.nodeName.toLowerCase();if(R==="tbody"||R===(Y?"tfoot":"thead")){var V=w(Y,aa,W,"tbody");if(V!==null){return K(Y,V,U,S)}}return M(Y,Z,T,aa,S)}}function w(V,T,U,X){var S=C.dom.select(">"+X,T);var R=S.indexOf(U);if(V&&R===0||!V&&R===S.length-1){return B(V,T)}else{if(R===-1){var W=U.tagName.toLowerCase()==="thead"?0:S.length-1;return S[W]}else{return S[R+(V?-1:1)]}}}function B(U,T){var S=U?"thead":"tfoot";var R=C.dom.select(">"+S,T);return R.length!==0?R[0]:null}function K(V,T,S,U){var R=J(T,V);R&&z(C,S,R,V);d.dom.Event.cancel(U);return true}function M(Y,U,R,X,W){var S=X[R];if(S){F(S);return true}else{var V=C.dom.getParent(X,"td,th");if(V){return O(Y,V,W)}else{var T=J(U,!Y);F(T);return d.dom.Event.cancel(W)}}}function J(S,R){return S&&S[R?"lastChild":"firstChild"]}function F(R){C.selection.setCursorLocation(R,0)}function A(){return Q==L.UP||Q==L.DOWN}function D(R){var T=R.selection.getNode();var S=R.dom.getParent(T,"tr");return S!==null}function P(S){var R=0;var T=S;while(T.previousSibling){T=T.previousSibling;R=R+a(T,"colspan")}return R}function E(T,R){var U=0;var S=0;e(T.children,function(V,W){U=U+a(V,"colspan");S=W;if(U>R){return false}});return S}function z(T,W,Y,V){var X=P(T.dom.getParent(W,"td,th"));var S=E(Y,X);var R=Y.childNodes[S];var U=J(R,V);F(U||R)}function H(R){var T=C.selection.getNode();var U=C.dom.getParent(T,"td,th");var S=C.dom.getParent(R,"td,th");return U&&U!==S&&I(U,S)}function I(S,R){return C.dom.getParent(S,"TABLE")===C.dom.getParent(R,"TABLE")}if(A()&&D(C)){var G=C.selection.getNode();setTimeout(function(){if(H(G)){O(!N.shiftKey&&Q===L.UP,G,N)}},0)}}r.onKeyDown.add(v)}if(!d.isIE){function s(){var w;for(w=r.getBody().lastChild;w&&w.nodeType==3&&!w.nodeValue.length;w=w.previousSibling){}if(w&&w.nodeName=="TABLE"){r.dom.add(r.getBody(),"p",null,'
    ')}}if(d.isGecko){r.onKeyDown.add(function(z,B){var w,A,C=z.dom;if(B.keyCode==37||B.keyCode==38){w=z.selection.getRng();A=C.getParent(w.startContainer,"table");if(A&&z.getBody().firstChild==A){if(c(w,A)){w=C.createRng();w.setStartBefore(A);w.setEndBefore(A);z.selection.setRng(w);B.preventDefault()}}}})}r.onKeyUp.add(s);r.onSetContent.add(s);r.onVisualAid.add(s);r.onPreProcess.add(function(w,A){var z=A.node.lastChild;if(z&&z.childNodes.length==1&&z.firstChild.nodeName=="BR"){w.dom.remove(z)}});s();r.startContent=r.getContent({format:"raw"})}});e({mceTableSplitCells:function(n){n.split()},mceTableMergeCells:function(o){var p,q,n;n=g.dom.getParent(g.selection.getNode(),"th,td");if(n){p=n.rowSpan;q=n.colSpan}if(!g.dom.select("td.mceSelected,th.mceSelected").length){f.open({url:h+"/merge_cells.htm",width:240+parseInt(g.getLang("table.merge_cells_delta_width",0)),height:110+parseInt(g.getLang("table.merge_cells_delta_height",0)),inline:1},{rows:p,cols:q,onaction:function(r){o.merge(n,r.cols,r.rows)},plugin_url:h})}else{o.merge()}},mceTableInsertRowBefore:function(n){n.insertRow(true)},mceTableInsertRowAfter:function(n){n.insertRow()},mceTableInsertColBefore:function(n){n.insertCol(true)},mceTableInsertColAfter:function(n){n.insertCol()},mceTableDeleteCol:function(n){n.deleteCols()},mceTableDeleteRow:function(n){n.deleteRows()},mceTableCutRow:function(n){m=n.cutRows()},mceTableCopyRow:function(n){m=n.copyRows()},mceTablePasteRowBefore:function(n){n.pasteRows(m,true)},mceTablePasteRowAfter:function(n){n.pasteRows(m)},mceTableDelete:function(n){n.deleteTable()}},function(o,n){g.addCommand(n,function(){var p=l();if(p){o(p);g.execCommand("mceRepaint");k()}})});e({mceInsertTable:function(n){f.open({url:h+"/table.htm",width:400+parseInt(g.getLang("table.table_delta_width",0)),height:320+parseInt(g.getLang("table.table_delta_height",0)),inline:1},{plugin_url:h,action:n?n.action:0})},mceTableRowProps:function(){f.open({url:h+"/row.htm",width:400+parseInt(g.getLang("table.rowprops_delta_width",0)),height:295+parseInt(g.getLang("table.rowprops_delta_height",0)),inline:1},{plugin_url:h})},mceTableCellProps:function(){f.open({url:h+"/cell.htm",width:400+parseInt(g.getLang("table.cellprops_delta_width",0)),height:295+parseInt(g.getLang("table.cellprops_delta_height",0)),inline:1},{plugin_url:h})}},function(o,n){g.addCommand(n,function(p,q){o(q)})})}});d.PluginManager.add("table",d.plugins.TablePlugin)})(tinymce); \ No newline at end of file diff --git a/js/tiny_mce/plugins/table/editor_plugin_src.js b/js/tiny_mce/plugins/table/editor_plugin_src.js index c2f307f045..8170e4ed44 100644 --- a/js/tiny_mce/plugins/table/editor_plugin_src.js +++ b/js/tiny_mce/plugins/table/editor_plugin_src.js @@ -1,1125 +1,1408 @@ -/** - * editor_plugin_src.js - * - * Copyright 2009, Moxiecode Systems AB - * Released under LGPL License. - * - * License: http://tinymce.moxiecode.com/license - * Contributing: http://tinymce.moxiecode.com/contributing - */ - -(function(tinymce) { - var each = tinymce.each; - - /** - * Table Grid class. - */ - function TableGrid(table, dom, selection) { - var grid, startPos, endPos, selectedCell; - - buildGrid(); - selectedCell = dom.getParent(selection.getStart(), 'th,td'); - if (selectedCell) { - startPos = getPos(selectedCell); - endPos = findEndPos(); - selectedCell = getCell(startPos.x, startPos.y); - } - - function cloneNode(node, children) { - node = node.cloneNode(children); - node.removeAttribute('id'); - - return node; - } - - function buildGrid() { - var startY = 0; - - grid = []; - - each(['thead', 'tbody', 'tfoot'], function(part) { - var rows = dom.select(part + ' tr', table); - - each(rows, function(tr, y) { - y += startY; - - each(dom.select('td,th', tr), function(td, x) { - var x2, y2, rowspan, colspan; - - // Skip over existing cells produced by rowspan - if (grid[y]) { - while (grid[y][x]) - x++; - } - - // Get col/rowspan from cell - rowspan = getSpanVal(td, 'rowspan'); - colspan = getSpanVal(td, 'colspan'); - - // Fill out rowspan/colspan right and down - for (y2 = y; y2 < y + rowspan; y2++) { - if (!grid[y2]) - grid[y2] = []; - - for (x2 = x; x2 < x + colspan; x2++) { - grid[y2][x2] = { - part : part, - real : y2 == y && x2 == x, - elm : td, - rowspan : rowspan, - colspan : colspan - }; - } - } - }); - }); - - startY += rows.length; - }); - }; - - function getCell(x, y) { - var row; - - row = grid[y]; - if (row) - return row[x]; - }; - - function getSpanVal(td, name) { - return parseInt(td.getAttribute(name) || 1); - }; - - function isCellSelected(cell) { - return dom.hasClass(cell.elm, 'mceSelected') || cell == selectedCell; - }; - - function getSelectedRows() { - var rows = []; - - each(table.rows, function(row) { - each(row.cells, function(cell) { - if (dom.hasClass(cell, 'mceSelected') || cell == selectedCell.elm) { - rows.push(row); - return false; - } - }); - }); - - return rows; - }; - - function deleteTable() { - var rng = dom.createRng(); - - rng.setStartAfter(table); - rng.setEndAfter(table); - - selection.setRng(rng); - - dom.remove(table); - }; - - function cloneCell(cell) { - var formatNode; - - // Clone formats - tinymce.walk(cell, function(node) { - var curNode; - - if (node.nodeType == 3) { - each(dom.getParents(node.parentNode, null, cell).reverse(), function(node) { - node = cloneNode(node, false); - - if (!formatNode) - formatNode = curNode = node; - else if (curNode) - curNode.appendChild(node); - - curNode = node; - }); - - // Add something to the inner node - if (curNode) - curNode.innerHTML = tinymce.isIE ? ' ' : '
    '; - - return false; - } - }, 'childNodes'); - - cell = cloneNode(cell, false); - cell.rowSpan = cell.colSpan = 1; - - if (formatNode) { - cell.appendChild(formatNode); - } else { - if (!tinymce.isIE) - cell.innerHTML = '
    '; - } - - return cell; - }; - - function cleanup() { - var rng = dom.createRng(); - - // Empty rows - each(dom.select('tr', table), function(tr) { - if (tr.cells.length == 0) - dom.remove(tr); - }); - - // Empty table - if (dom.select('tr', table).length == 0) { - rng.setStartAfter(table); - rng.setEndAfter(table); - selection.setRng(rng); - dom.remove(table); - return; - } - - // Empty header/body/footer - each(dom.select('thead,tbody,tfoot', table), function(part) { - if (part.rows.length == 0) - dom.remove(part); - }); - - // Restore selection to start position if it still exists - buildGrid(); - - // Restore the selection to the closest table position - row = grid[Math.min(grid.length - 1, startPos.y)]; - if (row) { - selection.select(row[Math.min(row.length - 1, startPos.x)].elm, true); - selection.collapse(true); - } - }; - - function fillLeftDown(x, y, rows, cols) { - var tr, x2, r, c, cell; - - tr = grid[y][x].elm.parentNode; - for (r = 1; r <= rows; r++) { - tr = dom.getNext(tr, 'tr'); - - if (tr) { - // Loop left to find real cell - for (x2 = x; x2 >= 0; x2--) { - cell = grid[y + r][x2].elm; - - if (cell.parentNode == tr) { - // Append clones after - for (c = 1; c <= cols; c++) - dom.insertAfter(cloneCell(cell), cell); - - break; - } - } - - if (x2 == -1) { - // Insert nodes before first cell - for (c = 1; c <= cols; c++) - tr.insertBefore(cloneCell(tr.cells[0]), tr.cells[0]); - } - } - } - }; - - function split() { - each(grid, function(row, y) { - each(row, function(cell, x) { - var colSpan, rowSpan, newCell, i; - - if (isCellSelected(cell)) { - cell = cell.elm; - colSpan = getSpanVal(cell, 'colspan'); - rowSpan = getSpanVal(cell, 'rowspan'); - - if (colSpan > 1 || rowSpan > 1) { - cell.colSpan = cell.rowSpan = 1; - - // Insert cells right - for (i = 0; i < colSpan - 1; i++) - dom.insertAfter(cloneCell(cell), cell); - - fillLeftDown(x, y, rowSpan - 1, colSpan); - } - } - }); - }); - }; - - function merge(cell, cols, rows) { - var startX, startY, endX, endY, x, y, startCell, endCell, cell, children; - - // Use specified cell and cols/rows - if (cell) { - pos = getPos(cell); - startX = pos.x; - startY = pos.y; - endX = startX + (cols - 1); - endY = startY + (rows - 1); - } else { - // Use selection - startX = startPos.x; - startY = startPos.y; - endX = endPos.x; - endY = endPos.y; - } - - // Find start/end cells - startCell = getCell(startX, startY); - endCell = getCell(endX, endY); - - // Check if the cells exists and if they are of the same part for example tbody = tbody - if (startCell && endCell && startCell.part == endCell.part) { - // Split and rebuild grid - split(); - buildGrid(); - - // Set row/col span to start cell - startCell = getCell(startX, startY).elm; - startCell.colSpan = (endX - startX) + 1; - startCell.rowSpan = (endY - startY) + 1; - - // Remove other cells and add it's contents to the start cell - for (y = startY; y <= endY; y++) { - for (x = startX; x <= endX; x++) { - cell = grid[y][x].elm; - - if (cell != startCell) { - // Move children to startCell - children = tinymce.grep(cell.childNodes); - each(children, function(node, i) { - // Jump over last BR element - if (node.nodeName != 'BR' || i != children.length - 1) - startCell.appendChild(node); - }); - - // Remove cell - dom.remove(cell); - } - } - } - - // Remove empty rows etc and restore caret location - cleanup(); - } - }; - - function insertRow(before) { - var posY, cell, lastCell, x, rowElm, newRow, newCell, otherCell; - - // Find first/last row - each(grid, function(row, y) { - each(row, function(cell, x) { - if (isCellSelected(cell)) { - cell = cell.elm; - rowElm = cell.parentNode; - newRow = cloneNode(rowElm, false); - posY = y; - - if (before) - return false; - } - }); - - if (before) - return !posY; - }); - - for (x = 0; x < grid[0].length; x++) { - cell = grid[posY][x].elm; - - if (cell != lastCell) { - if (!before) { - rowSpan = getSpanVal(cell, 'rowspan'); - if (rowSpan > 1) { - cell.rowSpan = rowSpan + 1; - continue; - } - } else { - // Check if cell above can be expanded - if (posY > 0 && grid[posY - 1][x]) { - otherCell = grid[posY - 1][x].elm; - rowSpan = getSpanVal(otherCell, 'rowspan'); - if (rowSpan > 1) { - otherCell.rowSpan = rowSpan + 1; - continue; - } - } - } - - // Insert new cell into new row - newCell = cloneCell(cell) - newCell.colSpan = cell.colSpan; - newRow.appendChild(newCell); - - lastCell = cell; - } - } - - if (newRow.hasChildNodes()) { - if (!before) - dom.insertAfter(newRow, rowElm); - else - rowElm.parentNode.insertBefore(newRow, rowElm); - } - }; - - function insertCol(before) { - var posX, lastCell; - - // Find first/last column - each(grid, function(row, y) { - each(row, function(cell, x) { - if (isCellSelected(cell)) { - posX = x; - - if (before) - return false; - } - }); - - if (before) - return !posX; - }); - - each(grid, function(row, y) { - var cell = row[posX].elm, rowSpan, colSpan; - - if (cell != lastCell) { - colSpan = getSpanVal(cell, 'colspan'); - rowSpan = getSpanVal(cell, 'rowspan'); - - if (colSpan == 1) { - if (!before) { - dom.insertAfter(cloneCell(cell), cell); - fillLeftDown(posX, y, rowSpan - 1, colSpan); - } else { - cell.parentNode.insertBefore(cloneCell(cell), cell); - fillLeftDown(posX, y, rowSpan - 1, colSpan); - } - } else - cell.colSpan++; - - lastCell = cell; - } - }); - }; - - function deleteCols() { - var cols = []; - - // Get selected column indexes - each(grid, function(row, y) { - each(row, function(cell, x) { - if (isCellSelected(cell) && tinymce.inArray(cols, x) === -1) { - each(grid, function(row) { - var cell = row[x].elm, colSpan; - - colSpan = getSpanVal(cell, 'colspan'); - - if (colSpan > 1) - cell.colSpan = colSpan - 1; - else - dom.remove(cell); - }); - - cols.push(x); - } - }); - }); - - cleanup(); - }; - - function deleteRows() { - var rows; - - function deleteRow(tr) { - var nextTr, pos, lastCell; - - nextTr = dom.getNext(tr, 'tr'); - - // Move down row spanned cells - each(tr.cells, function(cell) { - var rowSpan = getSpanVal(cell, 'rowspan'); - - if (rowSpan > 1) { - cell.rowSpan = rowSpan - 1; - pos = getPos(cell); - fillLeftDown(pos.x, pos.y, 1, 1); - } - }); - - // Delete cells - pos = getPos(tr.cells[0]); - each(grid[pos.y], function(cell) { - var rowSpan; - - cell = cell.elm; - - if (cell != lastCell) { - rowSpan = getSpanVal(cell, 'rowspan'); - - if (rowSpan <= 1) - dom.remove(cell); - else - cell.rowSpan = rowSpan - 1; - - lastCell = cell; - } - }); - }; - - // Get selected rows and move selection out of scope - rows = getSelectedRows(); - - // Delete all selected rows - each(rows.reverse(), function(tr) { - deleteRow(tr); - }); - - cleanup(); - }; - - function cutRows() { - var rows = getSelectedRows(); - - dom.remove(rows); - cleanup(); - - return rows; - }; - - function copyRows() { - var rows = getSelectedRows(); - - each(rows, function(row, i) { - rows[i] = cloneNode(row, true); - }); - - return rows; - }; - - function pasteRows(rows, before) { - var selectedRows = getSelectedRows(), - targetRow = selectedRows[before ? 0 : selectedRows.length - 1], - targetCellCount = targetRow.cells.length; - - // Calc target cell count - each(grid, function(row) { - var match; - - targetCellCount = 0; - each(row, function(cell, x) { - if (cell.real) - targetCellCount += cell.colspan; - - if (cell.elm.parentNode == targetRow) - match = 1; - }); - - if (match) - return false; - }); - - if (!before) - rows.reverse(); - - each(rows, function(row) { - var cellCount = row.cells.length, cell; - - // Remove col/rowspans - for (i = 0; i < cellCount; i++) { - cell = row.cells[i]; - cell.colSpan = cell.rowSpan = 1; - } - - // Needs more cells - for (i = cellCount; i < targetCellCount; i++) - row.appendChild(cloneCell(row.cells[cellCount - 1])); - - // Needs less cells - for (i = targetCellCount; i < cellCount; i++) - dom.remove(row.cells[i]); - - // Add before/after - if (before) - targetRow.parentNode.insertBefore(row, targetRow); - else - dom.insertAfter(row, targetRow); - }); - }; - - function getPos(target) { - var pos; - - each(grid, function(row, y) { - each(row, function(cell, x) { - if (cell.elm == target) { - pos = {x : x, y : y}; - return false; - } - }); - - return !pos; - }); - - return pos; - }; - - function setStartCell(cell) { - startPos = getPos(cell); - }; - - function findEndPos() { - var pos, maxX, maxY; - - maxX = maxY = 0; - - each(grid, function(row, y) { - each(row, function(cell, x) { - var colSpan, rowSpan; - - if (isCellSelected(cell)) { - cell = grid[y][x]; - - if (x > maxX) - maxX = x; - - if (y > maxY) - maxY = y; - - if (cell.real) { - colSpan = cell.colspan - 1; - rowSpan = cell.rowspan - 1; - - if (colSpan) { - if (x + colSpan > maxX) - maxX = x + colSpan; - } - - if (rowSpan) { - if (y + rowSpan > maxY) - maxY = y + rowSpan; - } - } - } - }); - }); - - return {x : maxX, y : maxY}; - }; - - function setEndCell(cell) { - var startX, startY, endX, endY, maxX, maxY, colSpan, rowSpan; - - endPos = getPos(cell); - - if (startPos && endPos) { - // Get start/end positions - startX = Math.min(startPos.x, endPos.x); - startY = Math.min(startPos.y, endPos.y); - endX = Math.max(startPos.x, endPos.x); - endY = Math.max(startPos.y, endPos.y); - - // Expand end positon to include spans - maxX = endX; - maxY = endY; - - // Expand startX - for (y = startY; y <= maxY; y++) { - cell = grid[y][startX]; - - if (!cell.real) { - if (startX - (cell.colspan - 1) < startX) - startX -= cell.colspan - 1; - } - } - - // Expand startY - for (x = startX; x <= maxX; x++) { - cell = grid[startY][x]; - - if (!cell.real) { - if (startY - (cell.rowspan - 1) < startY) - startY -= cell.rowspan - 1; - } - } - - // Find max X, Y - for (y = startY; y <= endY; y++) { - for (x = startX; x <= endX; x++) { - cell = grid[y][x]; - - if (cell.real) { - colSpan = cell.colspan - 1; - rowSpan = cell.rowspan - 1; - - if (colSpan) { - if (x + colSpan > maxX) - maxX = x + colSpan; - } - - if (rowSpan) { - if (y + rowSpan > maxY) - maxY = y + rowSpan; - } - } - } - } - - // Remove current selection - dom.removeClass(dom.select('td.mceSelected,th.mceSelected'), 'mceSelected'); - - // Add new selection - for (y = startY; y <= maxY; y++) { - for (x = startX; x <= maxX; x++) - dom.addClass(grid[y][x].elm, 'mceSelected'); - } - } - }; - - // Expose to public - tinymce.extend(this, { - deleteTable : deleteTable, - split : split, - merge : merge, - insertRow : insertRow, - insertCol : insertCol, - deleteCols : deleteCols, - deleteRows : deleteRows, - cutRows : cutRows, - copyRows : copyRows, - pasteRows : pasteRows, - getPos : getPos, - setStartCell : setStartCell, - setEndCell : setEndCell - }); - }; - - tinymce.create('tinymce.plugins.TablePlugin', { - init : function(ed, url) { - var winMan, clipboardRows; - - function createTableGrid(node) { - var selection = ed.selection, tblElm = ed.dom.getParent(node || selection.getNode(), 'table'); - - if (tblElm) - return new TableGrid(tblElm, ed.dom, selection); - }; - - function cleanup() { - // Restore selection possibilities - ed.getBody().style.webkitUserSelect = ''; - ed.dom.removeClass(ed.dom.select('td.mceSelected,th.mceSelected'), 'mceSelected'); - }; - - // Register buttons - each([ - ['table', 'table.desc', 'mceInsertTable', true], - ['delete_table', 'table.del', 'mceTableDelete'], - ['delete_col', 'table.delete_col_desc', 'mceTableDeleteCol'], - ['delete_row', 'table.delete_row_desc', 'mceTableDeleteRow'], - ['col_after', 'table.col_after_desc', 'mceTableInsertColAfter'], - ['col_before', 'table.col_before_desc', 'mceTableInsertColBefore'], - ['row_after', 'table.row_after_desc', 'mceTableInsertRowAfter'], - ['row_before', 'table.row_before_desc', 'mceTableInsertRowBefore'], - ['row_props', 'table.row_desc', 'mceTableRowProps', true], - ['cell_props', 'table.cell_desc', 'mceTableCellProps', true], - ['split_cells', 'table.split_cells_desc', 'mceTableSplitCells', true], - ['merge_cells', 'table.merge_cells_desc', 'mceTableMergeCells', true] - ], function(c) { - ed.addButton(c[0], {title : c[1], cmd : c[2], ui : c[3]}); - }); - - // Select whole table is a table border is clicked - if (!tinymce.isIE) { - ed.onClick.add(function(ed, e) { - e = e.target; - - if (e.nodeName === 'TABLE') - ed.selection.select(e); - }); - } - - // Handle node change updates - ed.onNodeChange.add(function(ed, cm, n) { - var p; - - n = ed.selection.getStart(); - p = ed.dom.getParent(n, 'td,th,caption'); - cm.setActive('table', n.nodeName === 'TABLE' || !!p); - - // Disable table tools if we are in caption - if (p && p.nodeName === 'CAPTION') - p = 0; - - cm.setDisabled('delete_table', !p); - cm.setDisabled('delete_col', !p); - cm.setDisabled('delete_table', !p); - cm.setDisabled('delete_row', !p); - cm.setDisabled('col_after', !p); - cm.setDisabled('col_before', !p); - cm.setDisabled('row_after', !p); - cm.setDisabled('row_before', !p); - cm.setDisabled('row_props', !p); - cm.setDisabled('cell_props', !p); - cm.setDisabled('split_cells', !p); - cm.setDisabled('merge_cells', !p); - }); - - ed.onInit.add(function(ed) { - var startTable, startCell, dom = ed.dom, tableGrid; - - winMan = ed.windowManager; - - // Add cell selection logic - ed.onMouseDown.add(function(ed, e) { - if (e.button != 2) { - cleanup(); - - startCell = dom.getParent(e.target, 'td,th'); - startTable = dom.getParent(startCell, 'table'); - } - }); - - dom.bind(ed.getDoc(), 'mouseover', function(e) { - var sel, table, target = e.target; - - if (startCell && (tableGrid || target != startCell) && (target.nodeName == 'TD' || target.nodeName == 'TH')) { - table = dom.getParent(target, 'table'); - if (table == startTable) { - if (!tableGrid) { - tableGrid = createTableGrid(table); - tableGrid.setStartCell(startCell); - - ed.getBody().style.webkitUserSelect = 'none'; - } - - tableGrid.setEndCell(target); - } - - // Remove current selection - sel = ed.selection.getSel(); - - if (sel.removeAllRanges) - sel.removeAllRanges(); - else - sel.empty(); - - e.preventDefault(); - } - }); - - ed.onMouseUp.add(function(ed, e) { - var rng, sel = ed.selection, selectedCells, nativeSel = sel.getSel(), walker, node, lastNode, endNode; - - // Move selection to startCell - if (startCell) { - if (tableGrid) - ed.getBody().style.webkitUserSelect = ''; - - function setPoint(node, start) { - var walker = new tinymce.dom.TreeWalker(node, node); - - do { - // Text node - if (node.nodeType == 3 && tinymce.trim(node.nodeValue).length != 0) { - if (start) - rng.setStart(node, 0); - else - rng.setEnd(node, node.nodeValue.length); - - return; - } - - // BR element - if (node.nodeName == 'BR') { - if (start) - rng.setStartBefore(node); - else - rng.setEndBefore(node); - - return; - } - } while (node = (start ? walker.next() : walker.prev())); - }; - - // Try to expand text selection as much as we can only Gecko supports cell selection - selectedCells = dom.select('td.mceSelected,th.mceSelected'); - if (selectedCells.length > 0) { - rng = dom.createRng(); - node = selectedCells[0]; - endNode = selectedCells[selectedCells.length - 1]; - - setPoint(node, 1); - walker = new tinymce.dom.TreeWalker(node, dom.getParent(selectedCells[0], 'table')); - - do { - if (node.nodeName == 'TD' || node.nodeName == 'TH') { - if (!dom.hasClass(node, 'mceSelected')) - break; - - lastNode = node; - } - } while (node = walker.next()); - - setPoint(lastNode); - - sel.setRng(rng); - } - - ed.nodeChanged(); - startCell = tableGrid = startTable = null; - } - }); - - ed.onKeyUp.add(function(ed, e) { - cleanup(); - }); - - // Add context menu - if (ed && ed.plugins.contextmenu) { - ed.plugins.contextmenu.onContextMenu.add(function(th, m, e) { - var sm, se = ed.selection, el = se.getNode() || ed.getBody(); - - if (ed.dom.getParent(e, 'td') || ed.dom.getParent(e, 'th') || ed.dom.select('td.mceSelected,th.mceSelected').length) { - m.removeAll(); - - if (el.nodeName == 'A' && !ed.dom.getAttrib(el, 'name')) { - m.add({title : 'advanced.link_desc', icon : 'link', cmd : ed.plugins.advlink ? 'mceAdvLink' : 'mceLink', ui : true}); - m.add({title : 'advanced.unlink_desc', icon : 'unlink', cmd : 'UnLink'}); - m.addSeparator(); - } - - if (el.nodeName == 'IMG' && el.className.indexOf('mceItem') == -1) { - m.add({title : 'advanced.image_desc', icon : 'image', cmd : ed.plugins.advimage ? 'mceAdvImage' : 'mceImage', ui : true}); - m.addSeparator(); - } - - m.add({title : 'table.desc', icon : 'table', cmd : 'mceInsertTable', value : {action : 'insert'}}); - m.add({title : 'table.props_desc', icon : 'table_props', cmd : 'mceInsertTable'}); - m.add({title : 'table.del', icon : 'delete_table', cmd : 'mceTableDelete'}); - m.addSeparator(); - - // Cell menu - sm = m.addMenu({title : 'table.cell'}); - sm.add({title : 'table.cell_desc', icon : 'cell_props', cmd : 'mceTableCellProps'}); - sm.add({title : 'table.split_cells_desc', icon : 'split_cells', cmd : 'mceTableSplitCells'}); - sm.add({title : 'table.merge_cells_desc', icon : 'merge_cells', cmd : 'mceTableMergeCells'}); - - // Row menu - sm = m.addMenu({title : 'table.row'}); - sm.add({title : 'table.row_desc', icon : 'row_props', cmd : 'mceTableRowProps'}); - sm.add({title : 'table.row_before_desc', icon : 'row_before', cmd : 'mceTableInsertRowBefore'}); - sm.add({title : 'table.row_after_desc', icon : 'row_after', cmd : 'mceTableInsertRowAfter'}); - sm.add({title : 'table.delete_row_desc', icon : 'delete_row', cmd : 'mceTableDeleteRow'}); - sm.addSeparator(); - sm.add({title : 'table.cut_row_desc', icon : 'cut', cmd : 'mceTableCutRow'}); - sm.add({title : 'table.copy_row_desc', icon : 'copy', cmd : 'mceTableCopyRow'}); - sm.add({title : 'table.paste_row_before_desc', icon : 'paste', cmd : 'mceTablePasteRowBefore'}).setDisabled(!clipboardRows); - sm.add({title : 'table.paste_row_after_desc', icon : 'paste', cmd : 'mceTablePasteRowAfter'}).setDisabled(!clipboardRows); - - // Column menu - sm = m.addMenu({title : 'table.col'}); - sm.add({title : 'table.col_before_desc', icon : 'col_before', cmd : 'mceTableInsertColBefore'}); - sm.add({title : 'table.col_after_desc', icon : 'col_after', cmd : 'mceTableInsertColAfter'}); - sm.add({title : 'table.delete_col_desc', icon : 'delete_col', cmd : 'mceTableDeleteCol'}); - } else - m.add({title : 'table.desc', icon : 'table', cmd : 'mceInsertTable'}); - }); - } - - // Fixes an issue on Gecko where it's impossible to place the caret behind a table - // This fix will force a paragraph element after the table but only when the forced_root_block setting is enabled - if (!tinymce.isIE) { - function fixTableCaretPos() { - var last; - - // Skip empty text nodes form the end - for (last = ed.getBody().lastChild; last && last.nodeType == 3 && !last.nodeValue.length; last = last.previousSibling) ; - - if (last && last.nodeName == 'TABLE') - ed.dom.add(ed.getBody(), 'p', null, '
    '); - }; - - // Fixes an bug where it's impossible to place the caret before a table in Gecko - // this fix solves it by detecting when the caret is at the beginning of such a table - // and then manually moves the caret infront of the table - if (tinymce.isGecko) { - ed.onKeyDown.add(function(ed, e) { - var rng, table, dom = ed.dom; - - // On gecko it's not possible to place the caret before a table - if (e.keyCode == 37 || e.keyCode == 38) { - rng = ed.selection.getRng(); - table = dom.getParent(rng.startContainer, 'table'); - - if (table && ed.getBody().firstChild == table) { - if (isAtStart(rng, table)) { - rng = dom.createRng(); - - rng.setStartBefore(table); - rng.setEndBefore(table); - - ed.selection.setRng(rng); - - e.preventDefault(); - } - } - } - }); - } - - ed.onKeyUp.add(fixTableCaretPos); - ed.onSetContent.add(fixTableCaretPos); - ed.onVisualAid.add(fixTableCaretPos); - - ed.onPreProcess.add(function(ed, o) { - var last = o.node.lastChild; - - if (last && last.childNodes.length == 1 && last.firstChild.nodeName == 'BR') - ed.dom.remove(last); - }); - - fixTableCaretPos(); - } - }); - - // Register action commands - each({ - mceTableSplitCells : function(grid) { - grid.split(); - }, - - mceTableMergeCells : function(grid) { - var rowSpan, colSpan, cell; - - cell = ed.dom.getParent(ed.selection.getNode(), 'th,td'); - if (cell) { - rowSpan = cell.rowSpan; - colSpan = cell.colSpan; - } - - if (!ed.dom.select('td.mceSelected,th.mceSelected').length) { - winMan.open({ - url : url + '/merge_cells.htm', - width : 240 + parseInt(ed.getLang('table.merge_cells_delta_width', 0)), - height : 110 + parseInt(ed.getLang('table.merge_cells_delta_height', 0)), - inline : 1 - }, { - rows : rowSpan, - cols : colSpan, - onaction : function(data) { - grid.merge(cell, data.cols, data.rows); - }, - plugin_url : url - }); - } else - grid.merge(); - }, - - mceTableInsertRowBefore : function(grid) { - grid.insertRow(true); - }, - - mceTableInsertRowAfter : function(grid) { - grid.insertRow(); - }, - - mceTableInsertColBefore : function(grid) { - grid.insertCol(true); - }, - - mceTableInsertColAfter : function(grid) { - grid.insertCol(); - }, - - mceTableDeleteCol : function(grid) { - grid.deleteCols(); - }, - - mceTableDeleteRow : function(grid) { - grid.deleteRows(); - }, - - mceTableCutRow : function(grid) { - clipboardRows = grid.cutRows(); - }, - - mceTableCopyRow : function(grid) { - clipboardRows = grid.copyRows(); - }, - - mceTablePasteRowBefore : function(grid) { - grid.pasteRows(clipboardRows, true); - }, - - mceTablePasteRowAfter : function(grid) { - grid.pasteRows(clipboardRows); - }, - - mceTableDelete : function(grid) { - grid.deleteTable(); - } - }, function(func, name) { - ed.addCommand(name, function() { - var grid = createTableGrid(); - - if (grid) { - func(grid); - ed.execCommand('mceRepaint'); - cleanup(); - } - }); - }); - - // Register dialog commands - each({ - mceInsertTable : function(val) { - winMan.open({ - url : url + '/table.htm', - width : 400 + parseInt(ed.getLang('table.table_delta_width', 0)), - height : 320 + parseInt(ed.getLang('table.table_delta_height', 0)), - inline : 1 - }, { - plugin_url : url, - action : val ? val.action : 0 - }); - }, - - mceTableRowProps : function() { - winMan.open({ - url : url + '/row.htm', - width : 400 + parseInt(ed.getLang('table.rowprops_delta_width', 0)), - height : 295 + parseInt(ed.getLang('table.rowprops_delta_height', 0)), - inline : 1 - }, { - plugin_url : url - }); - }, - - mceTableCellProps : function() { - winMan.open({ - url : url + '/cell.htm', - width : 400 + parseInt(ed.getLang('table.cellprops_delta_width', 0)), - height : 295 + parseInt(ed.getLang('table.cellprops_delta_height', 0)), - inline : 1 - }, { - plugin_url : url - }); - } - }, function(func, name) { - ed.addCommand(name, function(ui, val) { - func(val); - }); - }); - } - }); - - // Register plugin - tinymce.PluginManager.add('table', tinymce.plugins.TablePlugin); -})(tinymce); \ No newline at end of file +/** + * editor_plugin_src.js + * + * Copyright 2009, Moxiecode Systems AB + * Released under LGPL License. + * + * License: http://tinymce.moxiecode.com/license + * Contributing: http://tinymce.moxiecode.com/contributing + */ + +(function(tinymce) { + var each = tinymce.each; + + // Checks if the selection/caret is at the start of the specified block element + function isAtStart(rng, par) { + var doc = par.ownerDocument, rng2 = doc.createRange(), elm; + + rng2.setStartBefore(par); + rng2.setEnd(rng.endContainer, rng.endOffset); + + elm = doc.createElement('body'); + elm.appendChild(rng2.cloneContents()); + + // Check for text characters of other elements that should be treated as content + return elm.innerHTML.replace(/<(br|img|object|embed|input|textarea)[^>]*>/gi, '-').replace(/<[^>]+>/g, '').length == 0; + }; + + function getSpanVal(td, name) { + return parseInt(td.getAttribute(name) || 1); + } + + /** + * Table Grid class. + */ + function TableGrid(table, dom, selection) { + var grid, startPos, endPos, selectedCell; + + buildGrid(); + selectedCell = dom.getParent(selection.getStart(), 'th,td'); + if (selectedCell) { + startPos = getPos(selectedCell); + endPos = findEndPos(); + selectedCell = getCell(startPos.x, startPos.y); + } + + function cloneNode(node, children) { + node = node.cloneNode(children); + node.removeAttribute('id'); + + return node; + } + + function buildGrid() { + var startY = 0; + + grid = []; + + each(['thead', 'tbody', 'tfoot'], function(part) { + var rows = dom.select('> ' + part + ' tr', table); + + each(rows, function(tr, y) { + y += startY; + + each(dom.select('> td, > th', tr), function(td, x) { + var x2, y2, rowspan, colspan; + + // Skip over existing cells produced by rowspan + if (grid[y]) { + while (grid[y][x]) + x++; + } + + // Get col/rowspan from cell + rowspan = getSpanVal(td, 'rowspan'); + colspan = getSpanVal(td, 'colspan'); + + // Fill out rowspan/colspan right and down + for (y2 = y; y2 < y + rowspan; y2++) { + if (!grid[y2]) + grid[y2] = []; + + for (x2 = x; x2 < x + colspan; x2++) { + grid[y2][x2] = { + part : part, + real : y2 == y && x2 == x, + elm : td, + rowspan : rowspan, + colspan : colspan + }; + } + } + }); + }); + + startY += rows.length; + }); + }; + + function getCell(x, y) { + var row; + + row = grid[y]; + if (row) + return row[x]; + }; + + function setSpanVal(td, name, val) { + if (td) { + val = parseInt(val); + + if (val === 1) + td.removeAttribute(name, 1); + else + td.setAttribute(name, val, 1); + } + } + + function isCellSelected(cell) { + return cell && (dom.hasClass(cell.elm, 'mceSelected') || cell == selectedCell); + }; + + function getSelectedRows() { + var rows = []; + + each(table.rows, function(row) { + each(row.cells, function(cell) { + if (dom.hasClass(cell, 'mceSelected') || cell == selectedCell.elm) { + rows.push(row); + return false; + } + }); + }); + + return rows; + }; + + function deleteTable() { + var rng = dom.createRng(); + + rng.setStartAfter(table); + rng.setEndAfter(table); + + selection.setRng(rng); + + dom.remove(table); + }; + + function cloneCell(cell) { + var formatNode; + + // Clone formats + tinymce.walk(cell, function(node) { + var curNode; + + if (node.nodeType == 3) { + each(dom.getParents(node.parentNode, null, cell).reverse(), function(node) { + node = cloneNode(node, false); + + if (!formatNode) + formatNode = curNode = node; + else if (curNode) + curNode.appendChild(node); + + curNode = node; + }); + + // Add something to the inner node + if (curNode) + curNode.innerHTML = tinymce.isIE ? ' ' : '
    '; + + return false; + } + }, 'childNodes'); + + cell = cloneNode(cell, false); + setSpanVal(cell, 'rowSpan', 1); + setSpanVal(cell, 'colSpan', 1); + + if (formatNode) { + cell.appendChild(formatNode); + } else { + if (!tinymce.isIE) + cell.innerHTML = '
    '; + } + + return cell; + }; + + function cleanup() { + var rng = dom.createRng(); + + // Empty rows + each(dom.select('tr', table), function(tr) { + if (tr.cells.length == 0) + dom.remove(tr); + }); + + // Empty table + if (dom.select('tr', table).length == 0) { + rng.setStartAfter(table); + rng.setEndAfter(table); + selection.setRng(rng); + dom.remove(table); + return; + } + + // Empty header/body/footer + each(dom.select('thead,tbody,tfoot', table), function(part) { + if (part.rows.length == 0) + dom.remove(part); + }); + + // Restore selection to start position if it still exists + buildGrid(); + + // Restore the selection to the closest table position + row = grid[Math.min(grid.length - 1, startPos.y)]; + if (row) { + selection.select(row[Math.min(row.length - 1, startPos.x)].elm, true); + selection.collapse(true); + } + }; + + function fillLeftDown(x, y, rows, cols) { + var tr, x2, r, c, cell; + + tr = grid[y][x].elm.parentNode; + for (r = 1; r <= rows; r++) { + tr = dom.getNext(tr, 'tr'); + + if (tr) { + // Loop left to find real cell + for (x2 = x; x2 >= 0; x2--) { + cell = grid[y + r][x2].elm; + + if (cell.parentNode == tr) { + // Append clones after + for (c = 1; c <= cols; c++) + dom.insertAfter(cloneCell(cell), cell); + + break; + } + } + + if (x2 == -1) { + // Insert nodes before first cell + for (c = 1; c <= cols; c++) + tr.insertBefore(cloneCell(tr.cells[0]), tr.cells[0]); + } + } + } + }; + + function split() { + each(grid, function(row, y) { + each(row, function(cell, x) { + var colSpan, rowSpan, newCell, i; + + if (isCellSelected(cell)) { + cell = cell.elm; + colSpan = getSpanVal(cell, 'colspan'); + rowSpan = getSpanVal(cell, 'rowspan'); + + if (colSpan > 1 || rowSpan > 1) { + setSpanVal(cell, 'rowSpan', 1); + setSpanVal(cell, 'colSpan', 1); + + // Insert cells right + for (i = 0; i < colSpan - 1; i++) + dom.insertAfter(cloneCell(cell), cell); + + fillLeftDown(x, y, rowSpan - 1, colSpan); + } + } + }); + }); + }; + + function merge(cell, cols, rows) { + var startX, startY, endX, endY, x, y, startCell, endCell, cell, children, count; + + // Use specified cell and cols/rows + if (cell) { + pos = getPos(cell); + startX = pos.x; + startY = pos.y; + endX = startX + (cols - 1); + endY = startY + (rows - 1); + } else { + // Use selection + startX = startPos.x; + startY = startPos.y; + endX = endPos.x; + endY = endPos.y; + } + + // Find start/end cells + startCell = getCell(startX, startY); + endCell = getCell(endX, endY); + + // Check if the cells exists and if they are of the same part for example tbody = tbody + if (startCell && endCell && startCell.part == endCell.part) { + // Split and rebuild grid + split(); + buildGrid(); + + // Set row/col span to start cell + startCell = getCell(startX, startY).elm; + setSpanVal(startCell, 'colSpan', (endX - startX) + 1); + setSpanVal(startCell, 'rowSpan', (endY - startY) + 1); + + // Remove other cells and add it's contents to the start cell + for (y = startY; y <= endY; y++) { + for (x = startX; x <= endX; x++) { + if (!grid[y] || !grid[y][x]) + continue; + + cell = grid[y][x].elm; + + if (cell != startCell) { + // Move children to startCell + children = tinymce.grep(cell.childNodes); + each(children, function(node) { + startCell.appendChild(node); + }); + + // Remove bogus nodes if there is children in the target cell + if (children.length) { + children = tinymce.grep(startCell.childNodes); + count = 0; + each(children, function(node) { + if (node.nodeName == 'BR' && dom.getAttrib(node, 'data-mce-bogus') && count++ < children.length - 1) + startCell.removeChild(node); + }); + } + + // Remove cell + dom.remove(cell); + } + } + } + + // Remove empty rows etc and restore caret location + cleanup(); + } + }; + + function insertRow(before) { + var posY, cell, lastCell, x, rowElm, newRow, newCell, otherCell, rowSpan; + + // Find first/last row + each(grid, function(row, y) { + each(row, function(cell, x) { + if (isCellSelected(cell)) { + cell = cell.elm; + rowElm = cell.parentNode; + newRow = cloneNode(rowElm, false); + posY = y; + + if (before) + return false; + } + }); + + if (before) + return !posY; + }); + + for (x = 0; x < grid[0].length; x++) { + // Cell not found could be because of an invalid table structure + if (!grid[posY][x]) + continue; + + cell = grid[posY][x].elm; + + if (cell != lastCell) { + if (!before) { + rowSpan = getSpanVal(cell, 'rowspan'); + if (rowSpan > 1) { + setSpanVal(cell, 'rowSpan', rowSpan + 1); + continue; + } + } else { + // Check if cell above can be expanded + if (posY > 0 && grid[posY - 1][x]) { + otherCell = grid[posY - 1][x].elm; + rowSpan = getSpanVal(otherCell, 'rowSpan'); + if (rowSpan > 1) { + setSpanVal(otherCell, 'rowSpan', rowSpan + 1); + continue; + } + } + } + + // Insert new cell into new row + newCell = cloneCell(cell); + setSpanVal(newCell, 'colSpan', cell.colSpan); + + newRow.appendChild(newCell); + + lastCell = cell; + } + } + + if (newRow.hasChildNodes()) { + if (!before) + dom.insertAfter(newRow, rowElm); + else + rowElm.parentNode.insertBefore(newRow, rowElm); + } + }; + + function insertCol(before) { + var posX, lastCell; + + // Find first/last column + each(grid, function(row, y) { + each(row, function(cell, x) { + if (isCellSelected(cell)) { + posX = x; + + if (before) + return false; + } + }); + + if (before) + return !posX; + }); + + each(grid, function(row, y) { + var cell, rowSpan, colSpan; + + if (!row[posX]) + return; + + cell = row[posX].elm; + if (cell != lastCell) { + colSpan = getSpanVal(cell, 'colspan'); + rowSpan = getSpanVal(cell, 'rowspan'); + + if (colSpan == 1) { + if (!before) { + dom.insertAfter(cloneCell(cell), cell); + fillLeftDown(posX, y, rowSpan - 1, colSpan); + } else { + cell.parentNode.insertBefore(cloneCell(cell), cell); + fillLeftDown(posX, y, rowSpan - 1, colSpan); + } + } else + setSpanVal(cell, 'colSpan', cell.colSpan + 1); + + lastCell = cell; + } + }); + }; + + function deleteCols() { + var cols = []; + + // Get selected column indexes + each(grid, function(row, y) { + each(row, function(cell, x) { + if (isCellSelected(cell) && tinymce.inArray(cols, x) === -1) { + each(grid, function(row) { + var cell = row[x].elm, colSpan; + + colSpan = getSpanVal(cell, 'colSpan'); + + if (colSpan > 1) + setSpanVal(cell, 'colSpan', colSpan - 1); + else + dom.remove(cell); + }); + + cols.push(x); + } + }); + }); + + cleanup(); + }; + + function deleteRows() { + var rows; + + function deleteRow(tr) { + var nextTr, pos, lastCell; + + nextTr = dom.getNext(tr, 'tr'); + + // Move down row spanned cells + each(tr.cells, function(cell) { + var rowSpan = getSpanVal(cell, 'rowSpan'); + + if (rowSpan > 1) { + setSpanVal(cell, 'rowSpan', rowSpan - 1); + pos = getPos(cell); + fillLeftDown(pos.x, pos.y, 1, 1); + } + }); + + // Delete cells + pos = getPos(tr.cells[0]); + each(grid[pos.y], function(cell) { + var rowSpan; + + cell = cell.elm; + + if (cell != lastCell) { + rowSpan = getSpanVal(cell, 'rowSpan'); + + if (rowSpan <= 1) + dom.remove(cell); + else + setSpanVal(cell, 'rowSpan', rowSpan - 1); + + lastCell = cell; + } + }); + }; + + // Get selected rows and move selection out of scope + rows = getSelectedRows(); + + // Delete all selected rows + each(rows.reverse(), function(tr) { + deleteRow(tr); + }); + + cleanup(); + }; + + function cutRows() { + var rows = getSelectedRows(); + + dom.remove(rows); + cleanup(); + + return rows; + }; + + function copyRows() { + var rows = getSelectedRows(); + + each(rows, function(row, i) { + rows[i] = cloneNode(row, true); + }); + + return rows; + }; + + function pasteRows(rows, before) { + var selectedRows = getSelectedRows(), + targetRow = selectedRows[before ? 0 : selectedRows.length - 1], + targetCellCount = targetRow.cells.length; + + // Calc target cell count + each(grid, function(row) { + var match; + + targetCellCount = 0; + each(row, function(cell, x) { + if (cell.real) + targetCellCount += cell.colspan; + + if (cell.elm.parentNode == targetRow) + match = 1; + }); + + if (match) + return false; + }); + + if (!before) + rows.reverse(); + + each(rows, function(row) { + var cellCount = row.cells.length, cell; + + // Remove col/rowspans + for (i = 0; i < cellCount; i++) { + cell = row.cells[i]; + setSpanVal(cell, 'colSpan', 1); + setSpanVal(cell, 'rowSpan', 1); + } + + // Needs more cells + for (i = cellCount; i < targetCellCount; i++) + row.appendChild(cloneCell(row.cells[cellCount - 1])); + + // Needs less cells + for (i = targetCellCount; i < cellCount; i++) + dom.remove(row.cells[i]); + + // Add before/after + if (before) + targetRow.parentNode.insertBefore(row, targetRow); + else + dom.insertAfter(row, targetRow); + }); + }; + + function getPos(target) { + var pos; + + each(grid, function(row, y) { + each(row, function(cell, x) { + if (cell.elm == target) { + pos = {x : x, y : y}; + return false; + } + }); + + return !pos; + }); + + return pos; + }; + + function setStartCell(cell) { + startPos = getPos(cell); + }; + + function findEndPos() { + var pos, maxX, maxY; + + maxX = maxY = 0; + + each(grid, function(row, y) { + each(row, function(cell, x) { + var colSpan, rowSpan; + + if (isCellSelected(cell)) { + cell = grid[y][x]; + + if (x > maxX) + maxX = x; + + if (y > maxY) + maxY = y; + + if (cell.real) { + colSpan = cell.colspan - 1; + rowSpan = cell.rowspan - 1; + + if (colSpan) { + if (x + colSpan > maxX) + maxX = x + colSpan; + } + + if (rowSpan) { + if (y + rowSpan > maxY) + maxY = y + rowSpan; + } + } + } + }); + }); + + return {x : maxX, y : maxY}; + }; + + function setEndCell(cell) { + var startX, startY, endX, endY, maxX, maxY, colSpan, rowSpan; + + endPos = getPos(cell); + + if (startPos && endPos) { + // Get start/end positions + startX = Math.min(startPos.x, endPos.x); + startY = Math.min(startPos.y, endPos.y); + endX = Math.max(startPos.x, endPos.x); + endY = Math.max(startPos.y, endPos.y); + + // Expand end positon to include spans + maxX = endX; + maxY = endY; + + // Expand startX + for (y = startY; y <= maxY; y++) { + cell = grid[y][startX]; + + if (!cell.real) { + if (startX - (cell.colspan - 1) < startX) + startX -= cell.colspan - 1; + } + } + + // Expand startY + for (x = startX; x <= maxX; x++) { + cell = grid[startY][x]; + + if (!cell.real) { + if (startY - (cell.rowspan - 1) < startY) + startY -= cell.rowspan - 1; + } + } + + // Find max X, Y + for (y = startY; y <= endY; y++) { + for (x = startX; x <= endX; x++) { + cell = grid[y][x]; + + if (cell.real) { + colSpan = cell.colspan - 1; + rowSpan = cell.rowspan - 1; + + if (colSpan) { + if (x + colSpan > maxX) + maxX = x + colSpan; + } + + if (rowSpan) { + if (y + rowSpan > maxY) + maxY = y + rowSpan; + } + } + } + } + + // Remove current selection + dom.removeClass(dom.select('td.mceSelected,th.mceSelected'), 'mceSelected'); + + // Add new selection + for (y = startY; y <= maxY; y++) { + for (x = startX; x <= maxX; x++) { + if (grid[y][x]) + dom.addClass(grid[y][x].elm, 'mceSelected'); + } + } + } + }; + + // Expose to public + tinymce.extend(this, { + deleteTable : deleteTable, + split : split, + merge : merge, + insertRow : insertRow, + insertCol : insertCol, + deleteCols : deleteCols, + deleteRows : deleteRows, + cutRows : cutRows, + copyRows : copyRows, + pasteRows : pasteRows, + getPos : getPos, + setStartCell : setStartCell, + setEndCell : setEndCell + }); + }; + + tinymce.create('tinymce.plugins.TablePlugin', { + init : function(ed, url) { + var winMan, clipboardRows, hasCellSelection = true; // Might be selected cells on reload + + function createTableGrid(node) { + var selection = ed.selection, tblElm = ed.dom.getParent(node || selection.getNode(), 'table'); + + if (tblElm) + return new TableGrid(tblElm, ed.dom, selection); + }; + + function cleanup() { + // Restore selection possibilities + ed.getBody().style.webkitUserSelect = ''; + + if (hasCellSelection) { + ed.dom.removeClass(ed.dom.select('td.mceSelected,th.mceSelected'), 'mceSelected'); + hasCellSelection = false; + } + }; + + // Register buttons + each([ + ['table', 'table.desc', 'mceInsertTable', true], + ['delete_table', 'table.del', 'mceTableDelete'], + ['delete_col', 'table.delete_col_desc', 'mceTableDeleteCol'], + ['delete_row', 'table.delete_row_desc', 'mceTableDeleteRow'], + ['col_after', 'table.col_after_desc', 'mceTableInsertColAfter'], + ['col_before', 'table.col_before_desc', 'mceTableInsertColBefore'], + ['row_after', 'table.row_after_desc', 'mceTableInsertRowAfter'], + ['row_before', 'table.row_before_desc', 'mceTableInsertRowBefore'], + ['row_props', 'table.row_desc', 'mceTableRowProps', true], + ['cell_props', 'table.cell_desc', 'mceTableCellProps', true], + ['split_cells', 'table.split_cells_desc', 'mceTableSplitCells', true], + ['merge_cells', 'table.merge_cells_desc', 'mceTableMergeCells', true] + ], function(c) { + ed.addButton(c[0], {title : c[1], cmd : c[2], ui : c[3]}); + }); + + // Select whole table is a table border is clicked + if (!tinymce.isIE) { + ed.onClick.add(function(ed, e) { + e = e.target; + + if (e.nodeName === 'TABLE') { + ed.selection.select(e); + ed.nodeChanged(); + } + }); + } + + ed.onPreProcess.add(function(ed, args) { + var nodes, i, node, dom = ed.dom, value; + + nodes = dom.select('table', args.node); + i = nodes.length; + while (i--) { + node = nodes[i]; + dom.setAttrib(node, 'data-mce-style', ''); + + if ((value = dom.getAttrib(node, 'width'))) { + dom.setStyle(node, 'width', value); + dom.setAttrib(node, 'width', ''); + } + + if ((value = dom.getAttrib(node, 'height'))) { + dom.setStyle(node, 'height', value); + dom.setAttrib(node, 'height', ''); + } + } + }); + + // Handle node change updates + ed.onNodeChange.add(function(ed, cm, n) { + var p; + + n = ed.selection.getStart(); + p = ed.dom.getParent(n, 'td,th,caption'); + cm.setActive('table', n.nodeName === 'TABLE' || !!p); + + // Disable table tools if we are in caption + if (p && p.nodeName === 'CAPTION') + p = 0; + + cm.setDisabled('delete_table', !p); + cm.setDisabled('delete_col', !p); + cm.setDisabled('delete_table', !p); + cm.setDisabled('delete_row', !p); + cm.setDisabled('col_after', !p); + cm.setDisabled('col_before', !p); + cm.setDisabled('row_after', !p); + cm.setDisabled('row_before', !p); + cm.setDisabled('row_props', !p); + cm.setDisabled('cell_props', !p); + cm.setDisabled('split_cells', !p); + cm.setDisabled('merge_cells', !p); + }); + + ed.onInit.add(function(ed) { + var startTable, startCell, dom = ed.dom, tableGrid; + + winMan = ed.windowManager; + + // Add cell selection logic + ed.onMouseDown.add(function(ed, e) { + if (e.button != 2) { + cleanup(); + + startCell = dom.getParent(e.target, 'td,th'); + startTable = dom.getParent(startCell, 'table'); + } + }); + + dom.bind(ed.getDoc(), 'mouseover', function(e) { + var sel, table, target = e.target; + + if (startCell && (tableGrid || target != startCell) && (target.nodeName == 'TD' || target.nodeName == 'TH')) { + table = dom.getParent(target, 'table'); + if (table == startTable) { + if (!tableGrid) { + tableGrid = createTableGrid(table); + tableGrid.setStartCell(startCell); + + ed.getBody().style.webkitUserSelect = 'none'; + } + + tableGrid.setEndCell(target); + hasCellSelection = true; + } + + // Remove current selection + sel = ed.selection.getSel(); + + try { + if (sel.removeAllRanges) + sel.removeAllRanges(); + else + sel.empty(); + } catch (ex) { + // IE9 might throw errors here + } + + e.preventDefault(); + } + }); + + ed.onMouseUp.add(function(ed, e) { + var rng, sel = ed.selection, selectedCells, nativeSel = sel.getSel(), walker, node, lastNode, endNode; + + // Move selection to startCell + if (startCell) { + if (tableGrid) + ed.getBody().style.webkitUserSelect = ''; + + function setPoint(node, start) { + var walker = new tinymce.dom.TreeWalker(node, node); + + do { + // Text node + if (node.nodeType == 3 && tinymce.trim(node.nodeValue).length != 0) { + if (start) + rng.setStart(node, 0); + else + rng.setEnd(node, node.nodeValue.length); + + return; + } + + // BR element + if (node.nodeName == 'BR') { + if (start) + rng.setStartBefore(node); + else + rng.setEndBefore(node); + + return; + } + } while (node = (start ? walker.next() : walker.prev())); + } + + // Try to expand text selection as much as we can only Gecko supports cell selection + selectedCells = dom.select('td.mceSelected,th.mceSelected'); + if (selectedCells.length > 0) { + rng = dom.createRng(); + node = selectedCells[0]; + endNode = selectedCells[selectedCells.length - 1]; + rng.setStartBefore(node); + rng.setEndAfter(node); + + setPoint(node, 1); + walker = new tinymce.dom.TreeWalker(node, dom.getParent(selectedCells[0], 'table')); + + do { + if (node.nodeName == 'TD' || node.nodeName == 'TH') { + if (!dom.hasClass(node, 'mceSelected')) + break; + + lastNode = node; + } + } while (node = walker.next()); + + setPoint(lastNode); + + sel.setRng(rng); + } + + ed.nodeChanged(); + startCell = tableGrid = startTable = null; + } + }); + + ed.onKeyUp.add(function(ed, e) { + cleanup(); + }); + + ed.onKeyDown.add(function (ed, e) { + fixTableCellSelection(ed); + }); + + ed.onMouseDown.add(function (ed, e) { + if (e.button != 2) { + fixTableCellSelection(ed); + } + }); + function tableCellSelected(ed, rng, n, currentCell) { + // The decision of when a table cell is selected is somewhat involved. The fact that this code is + // required is actually a pointer to the root cause of this bug. A cell is selected when the start + // and end offsets are 0, the start container is a text, and the selection node is either a TR (most cases) + // or the parent of the table (in the case of the selection containing the last cell of a table). + var TEXT_NODE = 3, table = ed.dom.getParent(rng.startContainer, 'TABLE'), + tableParent, allOfCellSelected, tableCellSelection; + if (table) + tableParent = table.parentNode; + allOfCellSelected =rng.startContainer.nodeType == TEXT_NODE && + rng.startOffset == 0 && + rng.endOffset == 0 && + currentCell && + (n.nodeName=="TR" || n==tableParent); + tableCellSelection = (n.nodeName=="TD"||n.nodeName=="TH")&& !currentCell; + return allOfCellSelected || tableCellSelection; + // return false; + } + + // this nasty hack is here to work around some WebKit selection bugs. + function fixTableCellSelection(ed) { + if (!tinymce.isWebKit) + return; + + var rng = ed.selection.getRng(); + var n = ed.selection.getNode(); + var currentCell = ed.dom.getParent(rng.startContainer, 'TD,TH'); + + if (!tableCellSelected(ed, rng, n, currentCell)) + return; + if (!currentCell) { + currentCell=n; + } + + // Get the very last node inside the table cell + var end = currentCell.lastChild; + while (end.lastChild) + end = end.lastChild; + + // Select the entire table cell. Nothing outside of the table cell should be selected. + rng.setEnd(end, end.nodeValue.length); + ed.selection.setRng(rng); + } + ed.plugins.table.fixTableCellSelection=fixTableCellSelection; + + // Add context menu + if (ed && ed.plugins.contextmenu) { + ed.plugins.contextmenu.onContextMenu.add(function(th, m, e) { + var sm, se = ed.selection, el = se.getNode() || ed.getBody(); + + if (ed.dom.getParent(e, 'td') || ed.dom.getParent(e, 'th') || ed.dom.select('td.mceSelected,th.mceSelected').length) { + m.removeAll(); + + if (el.nodeName == 'A' && !ed.dom.getAttrib(el, 'name')) { + m.add({title : 'advanced.link_desc', icon : 'link', cmd : ed.plugins.advlink ? 'mceAdvLink' : 'mceLink', ui : true}); + m.add({title : 'advanced.unlink_desc', icon : 'unlink', cmd : 'UnLink'}); + m.addSeparator(); + } + + if (el.nodeName == 'IMG' && el.className.indexOf('mceItem') == -1) { + m.add({title : 'advanced.image_desc', icon : 'image', cmd : ed.plugins.advimage ? 'mceAdvImage' : 'mceImage', ui : true}); + m.addSeparator(); + } + + m.add({title : 'table.desc', icon : 'table', cmd : 'mceInsertTable', value : {action : 'insert'}}); + m.add({title : 'table.props_desc', icon : 'table_props', cmd : 'mceInsertTable'}); + m.add({title : 'table.del', icon : 'delete_table', cmd : 'mceTableDelete'}); + m.addSeparator(); + + // Cell menu + sm = m.addMenu({title : 'table.cell'}); + sm.add({title : 'table.cell_desc', icon : 'cell_props', cmd : 'mceTableCellProps'}); + sm.add({title : 'table.split_cells_desc', icon : 'split_cells', cmd : 'mceTableSplitCells'}); + sm.add({title : 'table.merge_cells_desc', icon : 'merge_cells', cmd : 'mceTableMergeCells'}); + + // Row menu + sm = m.addMenu({title : 'table.row'}); + sm.add({title : 'table.row_desc', icon : 'row_props', cmd : 'mceTableRowProps'}); + sm.add({title : 'table.row_before_desc', icon : 'row_before', cmd : 'mceTableInsertRowBefore'}); + sm.add({title : 'table.row_after_desc', icon : 'row_after', cmd : 'mceTableInsertRowAfter'}); + sm.add({title : 'table.delete_row_desc', icon : 'delete_row', cmd : 'mceTableDeleteRow'}); + sm.addSeparator(); + sm.add({title : 'table.cut_row_desc', icon : 'cut', cmd : 'mceTableCutRow'}); + sm.add({title : 'table.copy_row_desc', icon : 'copy', cmd : 'mceTableCopyRow'}); + sm.add({title : 'table.paste_row_before_desc', icon : 'paste', cmd : 'mceTablePasteRowBefore'}).setDisabled(!clipboardRows); + sm.add({title : 'table.paste_row_after_desc', icon : 'paste', cmd : 'mceTablePasteRowAfter'}).setDisabled(!clipboardRows); + + // Column menu + sm = m.addMenu({title : 'table.col'}); + sm.add({title : 'table.col_before_desc', icon : 'col_before', cmd : 'mceTableInsertColBefore'}); + sm.add({title : 'table.col_after_desc', icon : 'col_after', cmd : 'mceTableInsertColAfter'}); + sm.add({title : 'table.delete_col_desc', icon : 'delete_col', cmd : 'mceTableDeleteCol'}); + } else + m.add({title : 'table.desc', icon : 'table', cmd : 'mceInsertTable'}); + }); + } + + // Fix to allow navigating up and down in a table in WebKit browsers. + if (tinymce.isWebKit) { + function moveSelection(ed, e) { + var VK = tinymce.VK; + var key = e.keyCode; + + function handle(upBool, sourceNode, event) { + var siblingDirection = upBool ? 'previousSibling' : 'nextSibling'; + var currentRow = ed.dom.getParent(sourceNode, 'tr'); + var siblingRow = currentRow[siblingDirection]; + + if (siblingRow) { + moveCursorToRow(ed, sourceNode, siblingRow, upBool); + tinymce.dom.Event.cancel(event); + return true; + } else { + var tableNode = ed.dom.getParent(currentRow, 'table'); + var middleNode = currentRow.parentNode; + var parentNodeName = middleNode.nodeName.toLowerCase(); + if (parentNodeName === 'tbody' || parentNodeName === (upBool ? 'tfoot' : 'thead')) { + var targetParent = getTargetParent(upBool, tableNode, middleNode, 'tbody'); + if (targetParent !== null) { + return moveToRowInTarget(upBool, targetParent, sourceNode, event); + } + } + return escapeTable(upBool, currentRow, siblingDirection, tableNode, event); + } + } + + function getTargetParent(upBool, topNode, secondNode, nodeName) { + var tbodies = ed.dom.select('>' + nodeName, topNode); + var position = tbodies.indexOf(secondNode); + if (upBool && position === 0 || !upBool && position === tbodies.length - 1) { + return getFirstHeadOrFoot(upBool, topNode); + } else if (position === -1) { + var topOrBottom = secondNode.tagName.toLowerCase() === 'thead' ? 0 : tbodies.length - 1; + return tbodies[topOrBottom]; + } else { + return tbodies[position + (upBool ? -1 : 1)]; + } + } + + function getFirstHeadOrFoot(upBool, parent) { + var tagName = upBool ? 'thead' : 'tfoot'; + var headOrFoot = ed.dom.select('>' + tagName, parent); + return headOrFoot.length !== 0 ? headOrFoot[0] : null; + } + + function moveToRowInTarget(upBool, targetParent, sourceNode, event) { + var targetRow = getChildForDirection(targetParent, upBool); + targetRow && moveCursorToRow(ed, sourceNode, targetRow, upBool); + tinymce.dom.Event.cancel(event); + return true; + } + + function escapeTable(upBool, currentRow, siblingDirection, table, event) { + var tableSibling = table[siblingDirection]; + if (tableSibling) { + moveCursorToStartOfElement(tableSibling); + return true; + } else { + var parentCell = ed.dom.getParent(table, 'td,th'); + if (parentCell) { + return handle(upBool, parentCell, event); + } else { + var backUpSibling = getChildForDirection(currentRow, !upBool); + moveCursorToStartOfElement(backUpSibling); + return tinymce.dom.Event.cancel(event); + } + } + } + + function getChildForDirection(parent, up) { + return parent && parent[up ? 'lastChild' : 'firstChild']; + } + + function moveCursorToStartOfElement(n) { + ed.selection.setCursorLocation(n, 0); + } + + function isVerticalMovement() { + return key == VK.UP || key == VK.DOWN; + } + + function isInTable(ed) { + var node = ed.selection.getNode(); + var currentRow = ed.dom.getParent(node, 'tr'); + return currentRow !== null; + } + + function columnIndex(column) { + var colIndex = 0; + var c = column; + while (c.previousSibling) { + c = c.previousSibling; + colIndex = colIndex + getSpanVal(c, "colspan"); + } + return colIndex; + } + + function findColumn(rowElement, columnIndex) { + var c = 0; + var r = 0; + each(rowElement.children, function(cell, i) { + c = c + getSpanVal(cell, "colspan"); + r = i; + if (c > columnIndex) + return false; + }); + return r; + } + + function moveCursorToRow(ed, node, row, upBool) { + var srcColumnIndex = columnIndex(ed.dom.getParent(node, 'td,th')); + var tgtColumnIndex = findColumn(row, srcColumnIndex); + var tgtNode = row.childNodes[tgtColumnIndex]; + var rowCellTarget = getChildForDirection(tgtNode, upBool); + moveCursorToStartOfElement(rowCellTarget || tgtNode); + } + + function shouldFixCaret(preBrowserNode) { + var newNode = ed.selection.getNode(); + var newParent = ed.dom.getParent(newNode, 'td,th'); + var oldParent = ed.dom.getParent(preBrowserNode, 'td,th'); + return newParent && newParent !== oldParent && checkSameParentTable(newParent, oldParent) + } + + function checkSameParentTable(nodeOne, NodeTwo) { + return ed.dom.getParent(nodeOne, 'TABLE') === ed.dom.getParent(NodeTwo, 'TABLE'); + } + + if (isVerticalMovement() && isInTable(ed)) { + var preBrowserNode = ed.selection.getNode(); + setTimeout(function() { + if (shouldFixCaret(preBrowserNode)) { + handle(!e.shiftKey && key === VK.UP, preBrowserNode, e); + } + }, 0); + } + } + + ed.onKeyDown.add(moveSelection); + } + + // Fixes an issue on Gecko where it's impossible to place the caret behind a table + // This fix will force a paragraph element after the table but only when the forced_root_block setting is enabled + if (!tinymce.isIE) { + function fixTableCaretPos() { + var last; + + // Skip empty text nodes form the end + for (last = ed.getBody().lastChild; last && last.nodeType == 3 && !last.nodeValue.length; last = last.previousSibling) ; + + if (last && last.nodeName == 'TABLE') + ed.dom.add(ed.getBody(), 'p', null, '
    '); + }; + + // Fixes an bug where it's impossible to place the caret before a table in Gecko + // this fix solves it by detecting when the caret is at the beginning of such a table + // and then manually moves the caret infront of the table + if (tinymce.isGecko) { + ed.onKeyDown.add(function(ed, e) { + var rng, table, dom = ed.dom; + + // On gecko it's not possible to place the caret before a table + if (e.keyCode == 37 || e.keyCode == 38) { + rng = ed.selection.getRng(); + table = dom.getParent(rng.startContainer, 'table'); + + if (table && ed.getBody().firstChild == table) { + if (isAtStart(rng, table)) { + rng = dom.createRng(); + + rng.setStartBefore(table); + rng.setEndBefore(table); + + ed.selection.setRng(rng); + + e.preventDefault(); + } + } + } + }); + } + + ed.onKeyUp.add(fixTableCaretPos); + ed.onSetContent.add(fixTableCaretPos); + ed.onVisualAid.add(fixTableCaretPos); + + ed.onPreProcess.add(function(ed, o) { + var last = o.node.lastChild; + + if (last && last.childNodes.length == 1 && last.firstChild.nodeName == 'BR') + ed.dom.remove(last); + }); + + fixTableCaretPos(); + ed.startContent = ed.getContent({format : 'raw'}); + } + }); + + // Register action commands + each({ + mceTableSplitCells : function(grid) { + grid.split(); + }, + + mceTableMergeCells : function(grid) { + var rowSpan, colSpan, cell; + + cell = ed.dom.getParent(ed.selection.getNode(), 'th,td'); + if (cell) { + rowSpan = cell.rowSpan; + colSpan = cell.colSpan; + } + + if (!ed.dom.select('td.mceSelected,th.mceSelected').length) { + winMan.open({ + url : url + '/merge_cells.htm', + width : 240 + parseInt(ed.getLang('table.merge_cells_delta_width', 0)), + height : 110 + parseInt(ed.getLang('table.merge_cells_delta_height', 0)), + inline : 1 + }, { + rows : rowSpan, + cols : colSpan, + onaction : function(data) { + grid.merge(cell, data.cols, data.rows); + }, + plugin_url : url + }); + } else + grid.merge(); + }, + + mceTableInsertRowBefore : function(grid) { + grid.insertRow(true); + }, + + mceTableInsertRowAfter : function(grid) { + grid.insertRow(); + }, + + mceTableInsertColBefore : function(grid) { + grid.insertCol(true); + }, + + mceTableInsertColAfter : function(grid) { + grid.insertCol(); + }, + + mceTableDeleteCol : function(grid) { + grid.deleteCols(); + }, + + mceTableDeleteRow : function(grid) { + grid.deleteRows(); + }, + + mceTableCutRow : function(grid) { + clipboardRows = grid.cutRows(); + }, + + mceTableCopyRow : function(grid) { + clipboardRows = grid.copyRows(); + }, + + mceTablePasteRowBefore : function(grid) { + grid.pasteRows(clipboardRows, true); + }, + + mceTablePasteRowAfter : function(grid) { + grid.pasteRows(clipboardRows); + }, + + mceTableDelete : function(grid) { + grid.deleteTable(); + } + }, function(func, name) { + ed.addCommand(name, function() { + var grid = createTableGrid(); + + if (grid) { + func(grid); + ed.execCommand('mceRepaint'); + cleanup(); + } + }); + }); + + // Register dialog commands + each({ + mceInsertTable : function(val) { + winMan.open({ + url : url + '/table.htm', + width : 400 + parseInt(ed.getLang('table.table_delta_width', 0)), + height : 320 + parseInt(ed.getLang('table.table_delta_height', 0)), + inline : 1 + }, { + plugin_url : url, + action : val ? val.action : 0 + }); + }, + + mceTableRowProps : function() { + winMan.open({ + url : url + '/row.htm', + width : 400 + parseInt(ed.getLang('table.rowprops_delta_width', 0)), + height : 295 + parseInt(ed.getLang('table.rowprops_delta_height', 0)), + inline : 1 + }, { + plugin_url : url + }); + }, + + mceTableCellProps : function() { + winMan.open({ + url : url + '/cell.htm', + width : 400 + parseInt(ed.getLang('table.cellprops_delta_width', 0)), + height : 295 + parseInt(ed.getLang('table.cellprops_delta_height', 0)), + inline : 1 + }, { + plugin_url : url + }); + } + }, function(func, name) { + ed.addCommand(name, function(ui, val) { + func(val); + }); + }); + } + }); + + // Register plugin + tinymce.PluginManager.add('table', tinymce.plugins.TablePlugin); +})(tinymce); diff --git a/js/tiny_mce/plugins/table/js/cell.js b/js/tiny_mce/plugins/table/js/cell.js index b5fc1fda3d..d6f3290599 100644 --- a/js/tiny_mce/plugins/table/js/cell.js +++ b/js/tiny_mce/plugins/table/js/cell.js @@ -63,6 +63,11 @@ function init() { function updateAction() { var el, inst = ed, tdElm, trElm, tableElm, formObj = document.forms[0]; + if (!AutoValidator.validate(formObj)) { + tinyMCEPopup.alert(AutoValidator.getErrorMessages(formObj).join('. ') + '.'); + return false; + } + tinyMCEPopup.restoreSelection(); el = ed.selection.getStart(); tdElm = ed.dom.getParent(el, "td,th"); @@ -83,8 +88,6 @@ function updateAction() { return; } - ed.execCommand('mceBeginUndoLevel'); - switch (getSelectValue(formObj, 'action')) { case "cell": var celltype = getSelectValue(formObj, 'celltype'); @@ -125,6 +128,36 @@ function updateAction() { break; + case "col": + var curr, col = 0, cell = trElm.firstChild, rows = tableElm.getElementsByTagName("tr"); + + if (cell.nodeName != "TD" && cell.nodeName != "TH") + cell = nextCell(cell); + + do { + if (cell == tdElm) + break; + col += cell.getAttribute("colspan"); + } while ((cell = nextCell(cell)) != null); + + for (var i=0; i - +
    {#table_dlg.merge_cells_title} -
     
    - - - - - - - - -
    {#table_dlg.cols}:
    {#table_dlg.rows}:
    + + + + + + + + + +
    :
    :
    diff --git a/js/tiny_mce/plugins/table/row.htm b/js/tiny_mce/plugins/table/row.htm index 092e6c8270..1885401f6b 100644 --- a/js/tiny_mce/plugins/table/row.htm +++ b/js/tiny_mce/plugins/table/row.htm @@ -5,16 +5,17 @@ + - + @@ -23,7 +24,7 @@
    {#table_dlg.general_props} - +
    - +
    @@ -70,7 +71,7 @@
    @@ -80,7 +81,7 @@
    {#table_dlg.advanced_props} - +
    @@ -112,7 +113,7 @@ bug - if (hc && (cn = n.firstChild) && cn.nodeType === 1 && n.childNodes.length === 1) { - if (cn.hasAttribute ? cn.hasAttribute('_mce_bogus') : cn.getAttribute('_mce_bogus')) - w.writeText('\u00a0'); - } else if (!hc) - w.writeText('\u00a0'); // No children then padd it - } + onPreProcess = new tinymce.util.Dispatcher(self); - break; + onPostProcess = new tinymce.util.Dispatcher(self); - case 3: // Text - // Check if valid child - if (s.validate_children && t.elementName && !t.schema.isValid(t.elementName, '#text')) - return; + htmlParser = new tinymce.html.DomParser(settings, schema); - return w.writeText(n.nodeValue); + // Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed + htmlParser.addAttributeFilter('src,href,style', function(nodes, name) { + var i = nodes.length, node, value, internalName = 'data-mce-' + name, urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope, undef; - case 4: // CDATA - return w.writeCDATA(n.nodeValue); + while (i--) { + node = nodes[i]; - case 8: // Comment - return w.writeComment(n.nodeValue); - } - } else if (n.nodeType == 1) - hc = n.hasChildNodes(); + value = node.attributes.map[internalName]; + if (value !== undef) { + // Set external name to internal value and remove internal + node.attr(name, value.length > 0 ? value : null); + node.attr(internalName, null); + } else { + // No internal attribute found then convert the value we have in the DOM + value = node.attributes.map[name]; - if (hc && !closed) { - cn = n.firstChild; + if (name === "style") + value = dom.serializeStyle(dom.parseStyle(value), node.name); + else if (urlConverter) + value = urlConverter.call(urlConverterScope, value, name, node.name); - while (cn) { - t._serializeNode(cn); - t.elementName = nn; - cn = cn.nextSibling; + node.attr(name, value.length > 0 ? value : null); } } + }); - // Write element end - if (!iv) { - if (!closed) - w.writeFullEndElement(); - else - w.writeEndElement(); - } - }, + // Remove internal classes mceItem<..> + htmlParser.addAttributeFilter('class', function(nodes, name) { + var i = nodes.length, node, value; - _protect : function(o) { - var t = this; + while (i--) { + node = nodes[i]; + value = node.attr('class').replace(/\s*mce(Item\w+|Selected)\s*/g, ''); + node.attr('class', value.length > 0 ? value : null); + } + }); - o.items = o.items || []; + // Remove bookmark elements + htmlParser.addAttributeFilter('data-mce-type', function(nodes, name, args) { + var i = nodes.length, node; - function enc(s) { - return s.replace(/[\r\n\\]/g, function(c) { - if (c === '\n') - return '\\n'; - else if (c === '\\') - return '\\\\'; + while (i--) { + node = nodes[i]; - return '\\r'; - }); - }; + if (node.attributes.map['data-mce-type'] === 'bookmark' && !args.cleanup) + node.remove(); + } + }); - function dec(s) { - return s.replace(/\\[\\rn]/g, function(c) { - if (c === '\\n') - return '\n'; - else if (c === '\\\\') - return '\\'; + // Force script into CDATA sections and remove the mce- prefix also add comments around styles + htmlParser.addNodeFilter('script,style', function(nodes, name) { + var i = nodes.length, node, value; - return '\r'; - }); + function trim(value) { + return value.replace(/()/g, '\n') + .replace(/^[\r\n]*|[\r\n]*$/g, '') + .replace(/^\s*(\/\/\s*|\]\]>|-->|\]\]-->)\s*$/g, ''); }; - each(o.patterns, function(p) { - o.content = dec(enc(o.content).replace(p.pattern, function(x, a, b, c) { - b = dec(b); - - if (p.encode) - b = t._encode(b); - - o.items.push(b); - return a + '' + c; - })); - }); - - return o; - }, + while (i--) { + node = nodes[i]; + value = node.firstChild ? node.firstChild.value : ''; - _unprotect : function(h, o) { - h = h.replace(/\'; + } + } + }); - return h; - }, + // Convert comments to cdata and handle protected comments + htmlParser.addNodeFilter('#comment', function(nodes, name) { + var i = nodes.length, node; + + while (i--) { + node = nodes[i]; + + if (node.value.indexOf('[CDATA[') === 0) { + node.name = '#cdata'; + node.type = 4; + node.value = node.value.replace(/^\[CDATA\[|\]\]$/g, ''); + } else if (node.value.indexOf('mce:protected ') === 0) { + node.name = "#text"; + node.type = 3; + node.raw = true; + node.value = unescape(node.value).substr(14); + } + } + }); - _encode : function(h) { - var t = this, s = t.settings, l; + htmlParser.addNodeFilter('xml:namespace,input', function(nodes, name) { + var i = nodes.length, node; - // Entity encode - if (s.entity_encoding !== 'raw') { - if (s.entity_encoding.indexOf('named') != -1) { - t.setEntities(s.entities); - l = t.entityLookup; + while (i--) { + node = nodes[i]; + if (node.type === 7) + node.remove(); + else if (node.type === 1) { + if (name === "input" && !("type" in node.attributes.map)) + node.attr('type', 'text'); + } + } + }); - h = h.replace(/[\u007E-\uFFFF]/g, function(a) { - var v; + // Fix list elements, TODO: Replace this later + if (settings.fix_list_elements) { + htmlParser.addNodeFilter('ul,ol', function(nodes, name) { + var i = nodes.length, node, parentNode; - if (v = l[a]) - a = '&' + v + ';'; + while (i--) { + node = nodes[i]; + parentNode = node.parent; - return a; - }); + if (parentNode.name === 'ul' || parentNode.name === 'ol') { + if (node.prev && node.prev.name === 'li') { + node.prev.append(node); + } + } } + }); + } - if (s.entity_encoding.indexOf('numeric') != -1) { - h = h.replace(/[\u007E-\uFFFF]/g, function(a) { - return '&#' + a.charCodeAt(0) + ';'; - }); - } + // Remove internal data attributes + htmlParser.addAttributeFilter('data-mce-src,data-mce-href,data-mce-style', function(nodes, name) { + var i = nodes.length; + + while (i--) { + nodes[i].attr(name, null); } + }); - return h; - }, + // Return public methods + return { + schema : schema, - _setup : function() { - var t = this, s = this.settings; + addNodeFilter : htmlParser.addNodeFilter, - if (t.done) - return; + addAttributeFilter : htmlParser.addAttributeFilter, - t.done = 1; + onPreProcess : onPreProcess, - t.setRules(s.valid_elements); - t.addRules(s.extended_valid_elements); + onPostProcess : onPostProcess, - if (s.invalid_elements) - t.invalidElementsRE = new RegExp('^(' + wildcardToRE(s.invalid_elements.replace(/,/g, '|').toLowerCase()) + ')$'); + serialize : function(node, args) { + var impl, doc, oldDoc, htmlSerializer, content; - if (s.attrib_value_filter) - t.attribValueFilter = s.attribValueFilter; - }, + // Explorer won't clone contents of script and style and the + // selected index of select elements are cleared on a clone operation. + if (isIE && dom.select('script,style,select,map').length > 0) { + content = node.innerHTML; + node = node.cloneNode(false); + dom.setHTML(node, content); + } else + node = node.cloneNode(true); + + // Nodes needs to be attached to something in WebKit/Opera + // Older builds of Opera crashes if you attach the node to an document created dynamically + // and since we can't feature detect a crash we need to sniff the acutal build number + // This fix will make DOM ranges and make Sizzle happy! + impl = node.ownerDocument.implementation; + if (impl.createHTMLDocument) { + // Create an empty HTML document + doc = impl.createHTMLDocument(""); + + // Add the element or it's children if it's a body element to the new document + each(node.nodeName == 'BODY' ? node.childNodes : [node], function(node) { + doc.body.appendChild(doc.importNode(node, true)); + }); - _getAttrib : function(n, a, na) { - var i, v; + // Grab first child or body element for serialization + if (node.nodeName != 'BODY') + node = doc.body.firstChild; + else + node = doc.body; - na = na || a.name; + // set the new document in DOMUtils so createElement etc works + oldDoc = dom.doc; + dom.doc = doc; + } - if (a.forcedVal && (v = a.forcedVal)) { - if (v === '{$uid}') - return this.dom.uniqueId(); + args = args || {}; + args.format = args.format || 'html'; - return v; - } + // Pre process + if (!args.no_events) { + args.node = node; + onPreProcess.dispatch(self, args); + } - v = this.dom.getAttrib(n, na); + // Setup serializer + htmlSerializer = new tinymce.html.Serializer(settings, schema); - switch (na) { - case 'rowspan': - case 'colspan': - // Whats the point? Remove usless attribute value - if (v == '1') - v = ''; + // Parse and serialize HTML + args.content = htmlSerializer.serialize( + htmlParser.parse(args.getInner ? node.innerHTML : tinymce.trim(dom.getOuterHTML(node), args), args) + ); - break; - } + // Replace all BOM characters for now until we can find a better solution + if (!args.cleanup) + args.content = args.content.replace(/\uFEFF|\u200B/g, ''); - if (this.attribValueFilter) - v = this.attribValueFilter(na, v, n); + // Post process + if (!args.no_events) + onPostProcess.dispatch(self, args); - if (a.validVals) { - for (i = a.validVals.length - 1; i >= 0; i--) { - if (v == a.validVals[i]) - break; - } + // Restore the old document if it was changed + if (oldDoc) + dom.doc = oldDoc; - if (i == -1) - return null; - } + args.node = null; - if (v === '' && typeof(a.defaultVal) != 'undefined') { - v = a.defaultVal; + return args.content; + }, - if (v === '{$uid}') - return this.dom.uniqueId(); + addRules : function(rules) { + schema.addValidElements(rules); + }, - return v; - } else { - // Remove internal mceItemXX classes when content is extracted from editor - if (na == 'class' && this.processObj.get) - v = v.replace(/\s?mceItem\w+\s?/g, ''); + setRules : function(rules) { + schema.setValidElements(rules); } - - if (v === '') - return null; - - - return v; - } - }); + }; + }; })(tinymce); - (function(tinymce) { tinymce.dom.ScriptLoader = function(settings) { var QUEUED = 0, @@ -6850,6 +8581,17 @@ window.tinymce.dom.Sizzle = Sizzle; callback(); }; + + function error() { + // Report the error so it's easier for people to spot loading errors + if (typeof(console) !== "undefined" && console.log) + console.log("Failed to load: " + url); + + // We can't mark it as done if there is a load error since + // A) We don't want to produce 404 errors on the server and + // B) the onerror event won't fire on all browsers. + // done(); + }; id = dom.uniqueId(); @@ -6859,7 +8601,7 @@ window.tinymce.dom.Sizzle = Sizzle; // If script is from same domain and we // use IE 6 then use XHR since it's more reliable - if (uri.host == loc.hostname && uri.port == loc.port && (uri.protocol + ':') == loc.protocol) { + if (uri.host == loc.hostname && uri.port == loc.port && (uri.protocol + ':') == loc.protocol && uri.protocol.toLowerCase() != 'file') { tinymce.util.XHR.send({ url : tinymce._addVer(uri.getURI()), success : function(content) { @@ -6874,7 +8616,9 @@ window.tinymce.dom.Sizzle = Sizzle; dom.remove(script); done(); - } + }, + + error : error }); return; @@ -6888,17 +8632,26 @@ window.tinymce.dom.Sizzle = Sizzle; src : tinymce._addVer(url) }); - // Add onload and readystate listeners - elm.onload = done; - elm.onreadystatechange = function() { - var state = elm.readyState; + // Add onload listener for non IE browsers since IE9 + // fires onload event before the script is parsed and executed + if (!tinymce.isIE) + elm.onload = done; - // Loaded state is passed on IE 6 however there - // are known issues with this method but we can't use - // XHR in a cross domain loading - if (state == 'complete' || state == 'loaded') - done(); - }; + // Add onerror event will get fired on some browsers but not all of them + elm.onerror = error; + + // Opera 9.60 doesn't seem to fire the onreadystate event at correctly + if (!tinymce.isOpera) { + elm.onreadystatechange = function() { + var state = elm.readyState; + + // Loaded state is passed on IE 6 however there + // are known issues with this method but we can't use + // XHR in a cross domain loading + if (state == 'complete' || state == 'loaded') + done(); + }; + } // Most browsers support this feature so we report errors // for those at least to help users track their missing plugins etc @@ -7046,164 +8799,10 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { }; this.prev = function(shallow) { - return (node = findSibling(node, 'lastChild', 'lastSibling', shallow)); - }; -}; - -(function() { - var transitional = {}; - - function unpack(lookup, data) { - var key; - - function replace(value) { - return value.replace(/[A-Z]+/g, function(key) { - return replace(lookup[key]); - }); - }; - - // Unpack lookup - for (key in lookup) { - if (lookup.hasOwnProperty(key)) - lookup[key] = replace(lookup[key]); - } - - // Unpack and parse data into object map - replace(data).replace(/#/g, '#text').replace(/(\w+)\[([^\]]+)\]/g, function(str, name, children) { - var i, map = {}; - - children = children.split(/\|/); - - for (i = children.length - 1; i >= 0; i--) - map[children[i]] = 1; - - transitional[name] = map; - }); - }; - - // This is the XHTML 1.0 transitional elements with it's children packed to reduce it's size - // we will later include the attributes here and use it as a default for valid elements but it - // requires us to rewrite the serializer engine - unpack({ - Z : '#|H|K|N|O|P', - Y : '#|X|form|R|Q', - X : 'p|T|div|U|W|isindex|fieldset|table', - W : 'pre|hr|blockquote|address|center|noframes', - U : 'ul|ol|dl|menu|dir', - ZC : '#|p|Y|div|U|W|table|br|span|bdo|object|applet|img|map|K|N|Q', - T : 'h1|h2|h3|h4|h5|h6', - ZB : '#|X|S|Q', - S : 'R|P', - ZA : '#|a|G|J|M|O|P', - R : '#|a|H|K|N|O', - Q : 'noscript|P', - P : 'ins|del|script', - O : 'input|select|textarea|label|button', - N : 'M|L', - M : 'em|strong|dfn|code|q|samp|kbd|var|cite|abbr|acronym', - L : 'sub|sup', - K : 'J|I', - J : 'tt|i|b|u|s|strike', - I : 'big|small|font|basefont', - H : 'G|F', - G : 'br|span|bdo', - F : 'object|applet|img|map|iframe' - }, 'script[]' + - 'style[]' + - 'object[#|param|X|form|a|H|K|N|O|Q]' + - 'param[]' + - 'p[S]' + - 'a[Z]' + - 'br[]' + - 'span[S]' + - 'bdo[S]' + - 'applet[#|param|X|form|a|H|K|N|O|Q]' + - 'h1[S]' + - 'img[]' + - 'map[X|form|Q|area]' + - 'h2[S]' + - 'iframe[#|X|form|a|H|K|N|O|Q]' + - 'h3[S]' + - 'tt[S]' + - 'i[S]' + - 'b[S]' + - 'u[S]' + - 's[S]' + - 'strike[S]' + - 'big[S]' + - 'small[S]' + - 'font[S]' + - 'basefont[]' + - 'em[S]' + - 'strong[S]' + - 'dfn[S]' + - 'code[S]' + - 'q[S]' + - 'samp[S]' + - 'kbd[S]' + - 'var[S]' + - 'cite[S]' + - 'abbr[S]' + - 'acronym[S]' + - 'sub[S]' + - 'sup[S]' + - 'input[]' + - 'select[optgroup|option]' + - 'optgroup[option]' + - 'option[]' + - 'textarea[]' + - 'label[S]' + - 'button[#|p|T|div|U|W|table|G|object|applet|img|map|K|N|Q]' + - 'h4[S]' + - 'ins[#|X|form|a|H|K|N|O|Q]' + - 'h5[S]' + - 'del[#|X|form|a|H|K|N|O|Q]' + - 'h6[S]' + - 'div[#|X|form|a|H|K|N|O|Q]' + - 'ul[li]' + - 'li[#|X|form|a|H|K|N|O|Q]' + - 'ol[li]' + - 'dl[dt|dd]' + - 'dt[S]' + - 'dd[#|X|form|a|H|K|N|O|Q]' + - 'menu[li]' + - 'dir[li]' + - 'pre[ZA]' + - 'hr[]' + - 'blockquote[#|X|form|a|H|K|N|O|Q]' + - 'address[S|p]' + - 'center[#|X|form|a|H|K|N|O|Q]' + - 'noframes[#|X|form|a|H|K|N|O|Q]' + - 'isindex[]' + - 'fieldset[#|legend|X|form|a|H|K|N|O|Q]' + - 'legend[S]' + - 'table[caption|col|colgroup|thead|tfoot|tbody|tr]' + - 'caption[S]' + - 'col[]' + - 'colgroup[col]' + - 'thead[tr]' + - 'tr[th|td]' + - 'th[#|X|form|a|H|K|N|O|Q]' + - 'form[#|X|a|H|K|N|O|Q]' + - 'noscript[#|X|form|a|H|K|N|O|Q]' + - 'td[#|X|form|a|H|K|N|O|Q]' + - 'tfoot[tr]' + - 'tbody[tr]' + - 'area[]' + - 'base[]' + - 'body[#|X|form|a|H|K|N|O|Q]' - ); - - tinymce.dom.Schema = function() { - var t = this, elements = transitional; - - t.isValid = function(name, child_name) { - var element = elements[name]; - - return !!(element && (!child_name || element[child_name])); - }; + return (node = findSibling(node, 'lastChild', 'previousSibling', shallow)); }; -})(); +}; + (function(tinymce) { tinymce.dom.RangeUtils = function(dom) { var INVISIBLE_CHAR = '\uFEFF'; @@ -7227,6 +8826,24 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { return; } + function exclude(nodes) { + var node; + + // First node is excluded + node = nodes[0]; + if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) { + nodes.splice(0, 1); + } + + // Last node is excluded + node = nodes[nodes.length - 1]; + if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) { + nodes.splice(nodes.length - 1, 1); + } + + return nodes; + }; + function collectSiblings(node, name, end_node) { var siblings = []; @@ -7256,7 +8873,7 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { if (!next) siblings.reverse(); - callback(siblings); + callback(exclude(siblings)); } } }; @@ -7267,30 +8884,30 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { // If index based end position then resolve it if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) - endContainer = endContainer.childNodes[Math.min(startOffset == endOffset ? endOffset : endOffset - 1, endContainer.childNodes.length - 1)]; - - // Find common ancestor and end points - ancestor = dom.findCommonAncestor(startContainer, endContainer); + endContainer = endContainer.childNodes[Math.min(endOffset - 1, endContainer.childNodes.length - 1)]; // Same container if (startContainer == endContainer) - return callback([startContainer]); + return callback(exclude([startContainer])); + // Find common ancestor and end points + ancestor = dom.findCommonAncestor(startContainer, endContainer); + // Process left side for (node = startContainer; node; node = node.parentNode) { - if (node == endContainer) + if (node === endContainer) return walkBoundary(startContainer, ancestor, true); - if (node == ancestor) + if (node === ancestor) break; } // Process right side for (node = endContainer; node; node = node.parentNode) { - if (node == startContainer) + if (node === startContainer) return walkBoundary(endContainer, ancestor); - if (node == ancestor) + if (node === ancestor) break; } @@ -7309,48 +8926,46 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { ); if (siblings.length) - callback(siblings); + callback(exclude(siblings)); // Walk right leaf walkBoundary(endContainer, endPoint); }; - /* this.split = function(rng) { + this.split = function(rng) { var startContainer = rng.startContainer, startOffset = rng.startOffset, endContainer = rng.endContainer, endOffset = rng.endOffset; function splitText(node, offset) { - if (offset == node.nodeValue.length) - node.appendData(INVISIBLE_CHAR); - - node = node.splitText(offset); - - if (node.nodeValue === INVISIBLE_CHAR) - node.nodeValue = ''; - - return node; + return node.splitText(offset); }; // Handle single text node - if (startContainer == endContainer) { - if (startContainer.nodeType == 3) { - if (startOffset != 0) - startContainer = endContainer = splitText(startContainer, startOffset); - - if (endOffset - startOffset != startContainer.nodeValue.length) - splitText(startContainer, endOffset - startOffset); + if (startContainer == endContainer && startContainer.nodeType == 3) { + if (startOffset > 0 && startOffset < startContainer.nodeValue.length) { + endContainer = splitText(startContainer, startOffset); + startContainer = endContainer.previousSibling; + + if (endOffset > startOffset) { + endOffset = endOffset - startOffset; + startContainer = endContainer = splitText(endContainer, endOffset).previousSibling; + endOffset = endContainer.nodeValue.length; + startOffset = 0; + } else { + endOffset = 0; + } } } else { // Split startContainer text node if needed - if (startContainer.nodeType == 3 && startOffset != 0) { + if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) { startContainer = splitText(startContainer, startOffset); startOffset = 0; } // Split endContainer text node if needed - if (endContainer.nodeType == 3 && endOffset != endContainer.nodeValue.length) { + if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) { endContainer = splitText(endContainer, endOffset).previousSibling; endOffset = endContainer.nodeValue.length; } @@ -7363,7 +8978,7 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { endOffset : endOffset }; }; -*/ + }; tinymce.dom.RangeUtils.compareRanges = function(rng1, rng2) { @@ -7387,12 +9002,159 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { }; })(tinymce); +(function(tinymce) { + var Event = tinymce.dom.Event, each = tinymce.each; + + tinymce.create('tinymce.ui.KeyboardNavigation', { + KeyboardNavigation: function(settings, dom) { + var t = this, root = settings.root, items = settings.items, + enableUpDown = settings.enableUpDown, enableLeftRight = settings.enableLeftRight || !settings.enableUpDown, + excludeFromTabOrder = settings.excludeFromTabOrder, + itemFocussed, itemBlurred, rootKeydown, rootFocussed, focussedId; + + dom = dom || tinymce.DOM; + + itemFocussed = function(evt) { + focussedId = evt.target.id; + }; + + itemBlurred = function(evt) { + dom.setAttrib(evt.target.id, 'tabindex', '-1'); + }; + + rootFocussed = function(evt) { + var item = dom.get(focussedId); + dom.setAttrib(item, 'tabindex', '0'); + item.focus(); + }; + + t.focus = function() { + dom.get(focussedId).focus(); + }; + + t.destroy = function() { + each(items, function(item) { + dom.unbind(dom.get(item.id), 'focus', itemFocussed); + dom.unbind(dom.get(item.id), 'blur', itemBlurred); + }); + + dom.unbind(dom.get(root), 'focus', rootFocussed); + dom.unbind(dom.get(root), 'keydown', rootKeydown); + + items = dom = root = t.focus = itemFocussed = itemBlurred = rootKeydown = rootFocussed = null; + t.destroy = function() {}; + }; + + t.moveFocus = function(dir, evt) { + var idx = -1, controls = t.controls, newFocus; + + if (!focussedId) + return; + + each(items, function(item, index) { + if (item.id === focussedId) { + idx = index; + return false; + } + }); + + idx += dir; + if (idx < 0) { + idx = items.length - 1; + } else if (idx >= items.length) { + idx = 0; + } + + newFocus = items[idx]; + dom.setAttrib(focussedId, 'tabindex', '-1'); + dom.setAttrib(newFocus.id, 'tabindex', '0'); + dom.get(newFocus.id).focus(); + + if (settings.actOnFocus) { + settings.onAction(newFocus.id); + } + + if (evt) + Event.cancel(evt); + }; + + rootKeydown = function(evt) { + var DOM_VK_LEFT = 37, DOM_VK_RIGHT = 39, DOM_VK_UP = 38, DOM_VK_DOWN = 40, DOM_VK_ESCAPE = 27, DOM_VK_ENTER = 14, DOM_VK_RETURN = 13, DOM_VK_SPACE = 32; + + switch (evt.keyCode) { + case DOM_VK_LEFT: + if (enableLeftRight) t.moveFocus(-1); + break; + + case DOM_VK_RIGHT: + if (enableLeftRight) t.moveFocus(1); + break; + + case DOM_VK_UP: + if (enableUpDown) t.moveFocus(-1); + break; + + case DOM_VK_DOWN: + if (enableUpDown) t.moveFocus(1); + break; + + case DOM_VK_ESCAPE: + if (settings.onCancel) { + settings.onCancel(); + Event.cancel(evt); + } + break; + + case DOM_VK_ENTER: + case DOM_VK_RETURN: + case DOM_VK_SPACE: + if (settings.onAction) { + settings.onAction(focussedId); + Event.cancel(evt); + } + break; + } + }; + + // Set up state and listeners for each item. + each(items, function(item, idx) { + var tabindex; + + if (!item.id) { + item.id = dom.uniqueId('_mce_item_'); + } + + if (excludeFromTabOrder) { + dom.bind(item.id, 'blur', itemBlurred); + tabindex = '-1'; + } else { + tabindex = (idx === 0 ? '0' : '-1'); + } + + dom.setAttrib(item.id, 'tabindex', tabindex); + dom.bind(dom.get(item.id), 'focus', itemFocussed); + }); + + // Setup initial state for root element. + if (items[0]){ + focussedId = items[0].id; + } + + dom.setAttrib(root, 'tabindex', '-1'); + + // Setup listeners for root element. + dom.bind(dom.get(root), 'focus', rootFocussed); + dom.bind(dom.get(root), 'keydown', rootKeydown); + } + }); +})(tinymce); + (function(tinymce) { // Shorten class names var DOM = tinymce.DOM, is = tinymce.is; tinymce.create('tinymce.ui.Control', { - Control : function(id, s) { + Control : function(id, s, editor) { this.id = id; this.settings = s = s || {}; this.rendered = false; @@ -7401,22 +9163,23 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { this.scope = s.scope || this; this.disabled = 0; this.active = 0; + this.editor = editor; + }, + + setAriaProperty : function(property, value) { + var element = DOM.get(this.id + '_aria') || DOM.get(this.id); + if (element) { + DOM.setAttrib(element, 'aria-' + property, !!value); + } + }, + + focus : function() { + DOM.get(this.id).focus(); }, setDisabled : function(s) { - var e; - if (s != this.disabled) { - e = DOM.get(this.id); - - // Add accessibility title for unavailable actions - if (e && this.settings.unavailable_prefix) { - if (s) { - this.prevTitle = e.title; - e.title = this.settings.unavailable_prefix + ": " + e.title; - } else - e.title = this.prevTitle; - } + this.setAriaProperty('disabled', s); this.setState('Disabled', s); this.setState('Enabled', !s); @@ -7432,6 +9195,7 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { if (s != this.active) { this.setState('Active', s); this.active = s; + this.setAriaProperty('pressed', s); } }, @@ -7489,8 +9253,8 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { }); })(tinymce); tinymce.create('tinymce.ui.Container:tinymce.ui.Control', { - Container : function(id, s) { - this.parent(id, s); + Container : function(id, s, editor) { + this.parent(id, s, editor); this.controls = []; @@ -7514,10 +9278,11 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { Separator : function(id, s) { this.parent(id, s); this.classPrefix = 'mceSeparator'; + this.setDisabled(true); }, renderHTML : function() { - return tinymce.DOM.createHTML('span', {'class' : this.classPrefix}); + return tinymce.DOM.createHTML('span', {'class' : this.classPrefix, role : 'separator', 'aria-orientation' : 'vertical', tabindex : '-1'}); } }); @@ -7532,6 +9297,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { setSelected : function(s) { this.setState('Selected', s); + this.setAriaProperty('checked', !!s); this.selected = s; }, @@ -7679,12 +9445,20 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { s['class'] = s['class'] || cs['class']; s.vp_offset_x = s.vp_offset_x || cs.vp_offset_x; s.vp_offset_y = s.vp_offset_y || cs.vp_offset_y; + s.keyboard_focus = cs.keyboard_focus; m = new tinymce.ui.DropMenu(s.id || DOM.uniqueId(), s); m.onAddItem.add(t.onAddItem.dispatch, t.onAddItem); return m; }, + + focus : function() { + var t = this; + if (t.keyboardNav) { + t.keyboardNav.focus(); + } + }, update : function() { var t = this, s = t.settings, tb = DOM.get('menu_' + t.id + '_tbl'), co = DOM.get('menu_' + t.id + '_co'), tw, th; @@ -7808,13 +9582,13 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { } }); } + + Event.add(co, 'keydown', t._keyHandler, t); t.onShowMenu.dispatch(t); - if (s.keyboard_focus) { - Event.add(co, 'keydown', t._keyHandler, t); - DOM.select('a', 'menu_' + t.id)[0].focus(); // Select first link - t._focusIdx = 0; + if (s.keyboard_focus) { + t._setupKeyboardNav(); } }, @@ -7824,6 +9598,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { if (!t.isMenuVisible) return; + if (t.keyboardNav) t.keyboardNav.destroy(); Event.remove(co, 'mouseover', t.mouseOverFunc); Event.remove(co, 'click', t.mouseClickFunc); Event.remove(co, 'keydown', t._keyHandler); @@ -7868,8 +9643,11 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { destroy : function() { var t = this, co = DOM.get('menu_' + t.id); + if (t.keyboardNav) t.keyboardNav.destroy(); Event.remove(co, 'mouseover', t.mouseOverFunc); + Event.remove(DOM.select('a', co), 'focus', t.mouseOverFunc); Event.remove(co, 'click', t.mouseClickFunc); + Event.remove(co, 'keydown', t._keyHandler); if (t.element) t.element.remove(); @@ -7880,15 +9658,18 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { renderNode : function() { var t = this, s = t.settings, n, tb, co, w; - w = DOM.create('div', {id : 'menu_' + t.id, 'class' : s['class'], 'style' : 'position:absolute;left:0;top:0;z-index:200000'}); - co = DOM.add(w, 'div', {id : 'menu_' + t.id + '_co', 'class' : t.classPrefix + (s['class'] ? ' ' + s['class'] : '')}); + w = DOM.create('div', {role: 'listbox', id : 'menu_' + t.id, 'class' : s['class'], 'style' : 'position:absolute;left:0;top:0;z-index:200000;outline:0'}); + if (t.settings.parent) { + DOM.setAttrib(w, 'aria-parent', 'menu_' + t.settings.parent.id); + } + co = DOM.add(w, 'div', {role: 'presentation', id : 'menu_' + t.id + '_co', 'class' : t.classPrefix + (s['class'] ? ' ' + s['class'] : '')}); t.element = new Element('menu_' + t.id, {blocker : 1, container : s.container}); if (s.menu_line) DOM.add(co, 'span', {'class' : t.classPrefix + 'Line'}); // n = DOM.add(co, 'div', {id : 'menu_' + t.id + '_co', 'class' : 'mceMenuContainer'}); - n = DOM.add(co, 'table', {id : 'menu_' + t.id + '_tbl', border : 0, cellPadding : 0, cellSpacing : 0}); + n = DOM.add(co, 'table', {role: 'presentation', id : 'menu_' + t.id + '_tbl', border : 0, cellPadding : 0, cellSpacing : 0}); tb = DOM.add(n, 'tbody'); each(t.items, function(o) { @@ -7901,33 +9682,36 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { }, // Internal functions + _setupKeyboardNav : function(){ + var contextMenu, menuItems, t=this; + contextMenu = DOM.select('#menu_' + t.id)[0]; + menuItems = DOM.select('a[role=option]', 'menu_' + t.id); + menuItems.splice(0,0,contextMenu); + t.keyboardNav = new tinymce.ui.KeyboardNavigation({ + root: 'menu_' + t.id, + items: menuItems, + onCancel: function() { + t.hideMenu(); + }, + enableUpDown: true + }); + contextMenu.focus(); + }, - _keyHandler : function(e) { - var t = this, kc = e.keyCode; - - function focus(d) { - var i = t._focusIdx + d, e = DOM.select('a', 'menu_' + t.id)[i]; - - if (e) { - t._focusIdx = i; - e.focus(); - } - }; - - switch (kc) { - case 38: - focus(-1); // Select first link - return; - - case 40: - focus(1); - return; - - case 13: - return; - - case 27: - return this.hideMenu(); + _keyHandler : function(evt) { + var t = this, e; + switch (evt.keyCode) { + case 37: // Left + if (t.settings.parent) { + t.hideMenu(); + t.settings.parent.focus(); + Event.cancel(evt); + } + break; + case 39: // Right + if (t.mouseOverFunc) + t.mouseOverFunc(evt); + break; } }, @@ -7945,8 +9729,13 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { } n = ro = DOM.add(tb, 'tr', {id : o.id, 'class' : cp + 'Item ' + cp + 'ItemEnabled'}); - n = it = DOM.add(n, 'td'); - n = a = DOM.add(n, 'a', {href : 'javascript:;', onclick : "return false;", onmousedown : 'return false;'}); + n = it = DOM.add(n, s.titleItem ? 'th' : 'td'); + n = a = DOM.add(n, 'a', {id: o.id + '_aria', role: s.titleItem ? 'presentation' : 'option', href : 'javascript:;', onclick : "return false;", onmousedown : 'return false;'}); + + if (s.parent) { + DOM.setAttrib(a, 'aria-haspopup', 'true'); + DOM.setAttrib(a, 'aria-owns', 'menu_' + o.id); + } DOM.addClass(it, s['class']); // n = DOM.add(n, 'span', {'class' : 'item'}); @@ -7981,8 +9770,8 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { var DOM = tinymce.DOM; tinymce.create('tinymce.ui.Button:tinymce.ui.Control', { - Button : function(id, s) { - this.parent(id, s); + Button : function(id, s, ed) { + this.parent(id, s, ed); this.classPrefix = 'mceButton'; }, @@ -7990,13 +9779,14 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { var cp = this.classPrefix, s = this.settings, h, l; l = DOM.encode(s.label || ''); - h = ''; - - if (s.image) - h += '' + l + ''; + h = ''; + if (s.image && !(this.editor &&this.editor.forcedHighContrastMode) ) + h += '' + DOM.encode(s.title) + '' + l; else - h += '' + (l ? '' + l + '' : '') + ''; + h += '' + (l ? '' + l + '' : ''); + h += ''; + h += ''; return h; }, @@ -8015,10 +9805,10 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher; tinymce.create('tinymce.ui.ListBox:tinymce.ui.Control', { - ListBox : function(id, s) { + ListBox : function(id, s, ed) { var t = this; - t.parent(id, s); + t.parent(id, s, ed); t.items = []; @@ -8065,23 +9855,27 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { }, selectByIndex : function(idx) { - var t = this, e, o; + var t = this, e, o, label; if (idx != t.selectedIndex) { e = DOM.get(t.id + '_text'); + label = DOM.get(t.id + '_voiceDesc'); o = t.items[idx]; if (o) { t.selectedValue = o.value; t.selectedIndex = idx; DOM.setHTML(e, DOM.encode(o.title)); + DOM.setHTML(label, t.settings.title + " - " + o.title); DOM.removeClass(e, 'mceTitle'); + DOM.setAttrib(t.id, 'aria-valuenow', o.title); } else { DOM.setHTML(e, DOM.encode(t.settings.title)); + DOM.setHTML(label, DOM.encode(t.settings.title)); DOM.addClass(e, 'mceTitle'); t.selectedValue = t.selectedIndex = null; + DOM.setAttrib(t.id, 'aria-valuenow', t.settings.title); } - e = 0; } }, @@ -8106,16 +9900,17 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { renderHTML : function() { var h = '', t = this, s = t.settings, cp = t.classPrefix; - h = '
    - +
    @@ -122,14 +123,16 @@ - +
     
    - + +
     
    +
    diff --git a/js/tiny_mce/plugins/table/table.htm b/js/tiny_mce/plugins/table/table.htm index f269039228..b92fa741eb 100644 --- a/js/tiny_mce/plugins/table/table.htm +++ b/js/tiny_mce/plugins/table/table.htm @@ -10,12 +10,13 @@ - + + @@ -23,48 +24,48 @@
    {#table_dlg.general_props} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    @@ -72,7 +73,7 @@
    {#table_dlg.advanced_props} - +
    @@ -98,7 +99,7 @@ "}else{e+=""}if(f&&j.ListBox){if(f.Button||f.SplitButton){e+=b.createHTML("td",{"class":"mceToolbarStart"},b.createHTML("span",null,""))}}}g="mceToolbarEnd";if(j.Button){g+=" mceToolbarEndButton"}else{if(j.SplitButton){g+=" mceToolbarEndSplitButton"}else{if(j.ListBox){g+=" mceToolbarEndListBox"}}}e+=b.createHTML("td",{"class":g},b.createHTML("span",null,""));return b.createHTML("table",{id:l.id,"class":"mceToolbar"+(m["class"]?" "+m["class"]:""),cellpadding:"0",cellspacing:"0",align:l.settings.align||""},""+e+"")}});(function(b){var a=b.util.Dispatcher,c=b.each;b.create("tinymce.AddOnManager",{items:[],urls:{},lookup:{},onAdd:new a(this),get:function(d){return this.lookup[d]},requireLangPack:function(e){var d=b.settings;if(d&&d.language){b.ScriptLoader.add(this.urls[e]+"/langs/"+d.language+".js")}},add:function(e,d){this.items.push(d);this.lookup[e]=d;this.onAdd.dispatch(this,e,d);return d},load:function(h,e,d,g){var f=this;if(f.urls[h]){return}if(e.indexOf("/")!=0&&e.indexOf("://")==-1){e=b.baseURL+"/"+e}f.urls[h]=e.substring(0,e.lastIndexOf("/"));b.ScriptLoader.add(e,d,g)}});b.PluginManager=new b.AddOnManager();b.ThemeManager=new b.AddOnManager()}(tinymce));(function(j){var g=j.each,d=j.extend,k=j.DOM,i=j.dom.Event,f=j.ThemeManager,b=j.PluginManager,e=j.explode,h=j.util.Dispatcher,a,c=0;j.documentBaseURL=window.location.href.replace(/[\?#].*$/,"").replace(/[\/\\][^\/]+$/,"");if(!/[\/\\]$/.test(j.documentBaseURL)){j.documentBaseURL+="/"}j.baseURL=new j.util.URI(j.documentBaseURL).toAbsolute(j.baseURL);j.baseURI=new j.util.URI(j.baseURL);j.onBeforeUnload=new h(j);i.add(window,"beforeunload",function(l){j.onBeforeUnload.dispatch(j,l)});j.onAddEditor=new h(j);j.onRemoveEditor=new h(j);j.EditorManager=d(j,{editors:[],i18n:{},activeEditor:null,init:function(q){var n=this,p,l=j.ScriptLoader,u,o=[],m;function r(x,y,t){var v=x[y];if(!v){return}if(j.is(v,"string")){t=v.replace(/\.\w+$/,"");t=t?j.resolve(t):0;v=j.resolve(v)}return v.apply(t||this,Array.prototype.slice.call(arguments,2))}q=d({theme:"simple",language:"en"},q);n.settings=q;i.add(document,"init",function(){var s,v;r(q,"onpageload");switch(q.mode){case"exact":s=q.elements||"";if(s.length>0){g(e(s),function(x){if(k.get(x)){m=new j.Editor(x,q);o.push(m);m.render(1)}else{g(document.forms,function(y){g(y.elements,function(z){if(z.name===x){x="mce_editor_"+c++;k.setAttrib(z,"id",x);m=new j.Editor(x,q);o.push(m);m.render(1)}})})}})}break;case"textareas":case"specific_textareas":function t(y,x){return x.constructor===RegExp?x.test(y.className):k.hasClass(y,x)}g(k.select("textarea"),function(x){if(q.editor_deselector&&t(x,q.editor_deselector)){return}if(!q.editor_selector||t(x,q.editor_selector)){u=k.get(x.name);if(!x.id&&!u){x.id=x.name}if(!x.id||n.get(x.id)){x.id=k.uniqueId()}m=new j.Editor(x.id,q);o.push(m);m.render(1)}});break}if(q.oninit){s=v=0;g(o,function(x){v++;if(!x.initialized){x.onInit.add(function(){s++;if(s==v){r(q,"oninit")}})}else{s++}if(s==v){r(q,"oninit")}})}})},get:function(l){if(l===a){return this.editors}return this.editors[l]},getInstanceById:function(l){return this.get(l)},add:function(m){var l=this,n=l.editors;n[m.id]=m;n.push(m);l._setActive(m);l.onAddEditor.dispatch(l,m);return m},remove:function(n){var m=this,l,o=m.editors;if(!o[n.id]){return null}delete o[n.id];for(l=0;l':"",visual_table_class:"mceItemTable",visual:1,font_size_style_values:"xx-small,x-small,small,medium,large,x-large,xx-large",apply_source_formatting:1,directionality:"ltr",forced_root_block:"p",valid_elements:"@[id|class|style|title|dir';if(F.document_base_url!=m.documentBaseURL){E.iframeHTML+=''}E.iframeHTML+='';if(m.relaxedDomain){E.iframeHTML+='\n'; - - document.write(html); - }; - - // Firebug - if (query.debug) - include('firebug/firebug-lite.js'); - - // Core ns - include('tinymce.js'); - - // Load framework adapter - if (query.api) - include('adapter/' + query.api + '/adapter.js'); - - // Core API - include('util/Dispatcher.js'); - include('util/URI.js'); - include('util/Cookie.js'); - include('util/JSON.js'); - include('util/JSONP.js'); - include('util/XHR.js'); - include('util/JSONRequest.js'); - include('dom/DOMUtils.js'); - include('dom/Range.js'); - include('dom/TridentSelection.js'); - include('dom/Sizzle.js'); - include('dom/EventUtils.js'); - include('dom/Element.js'); - include('dom/Selection.js'); - include('dom/XMLWriter.js'); - include('dom/Schema.js'); - include('dom/StringWriter.js'); - include('dom/Serializer.js'); - include('dom/ScriptLoader.js'); - include('dom/TreeWalker.js'); - include('dom/RangeUtils.js'); - include('ui/Control.js'); - include('ui/Container.js'); - include('ui/Separator.js'); - include('ui/MenuItem.js'); - include('ui/Menu.js'); - include('ui/DropMenu.js'); - include('ui/Button.js'); - include('ui/ListBox.js'); - include('ui/NativeListBox.js'); - include('ui/MenuButton.js'); - include('ui/SplitButton.js'); - include('ui/ColorSplitButton.js'); - include('ui/Toolbar.js'); - include('AddOnManager.js'); - include('EditorManager.js'); - include('Editor.js'); - include('EditorCommands.js'); - include('UndoManager.js'); - include('ForceBlocks.js'); - include('ControlManager.js'); - include('WindowManager.js'); - include('Formatter.js'); - include('CommandManager.js'); - include('LegacyInput.js'); - - // Developer API - include('xml/Parser.js'); - include('Developer.js'); - - load(); -}()); \ No newline at end of file +/** + * tiny_mce_dev.js + * + * Copyright 2009, Moxiecode Systems AB + * Released under LGPL License. + * + * License: http://tinymce.moxiecode.com/license + * Contributing: http://tinymce.moxiecode.com/contributing + * + * This file should only be used while developing TinyMCE + * tiny_mce.js or tiny_mce_src.js should be used in a production environment. + * This file loads the js files from classes instead of a merged copy. + */ + +(function() { + var i, nl = document.getElementsByTagName('script'), base, src, p, li, query = '', it, scripts = []; + + if (window.tinyMCEPreInit) { + base = tinyMCEPreInit.base; + query = tinyMCEPreInit.query || ''; + } else { + for (i=0; i\n'; + + document.write(html); + }; + + // Firebug + if (query.debug && !("console" in window)) { + include('firebug/firebug-lite.js'); + } + + // Core ns + include('tinymce.js'); + + // Load framework adapter + if (query.api) + include('adapter/' + query.api + '/adapter.js'); + + // tinymce.util.* + include('util/Dispatcher.js'); + include('util/URI.js'); + include('util/Cookie.js'); + include('util/JSON.js'); + include('util/JSONP.js'); + include('util/XHR.js'); + include('util/JSONRequest.js'); + include('util/VK.js'); + include('util/Quirks.js'); + + // tinymce.html.* + include('html/Entities.js'); + include('html/Styles.js'); + include('html/Schema.js'); + include('html/SaxParser.js'); + include('html/Node.js'); + include('html/DomParser.js'); + include('html/Serializer.js'); + include('html/Writer.js'); + + // tinymce.dom.* + include('dom/DOMUtils.js'); + include('dom/Range.js'); + include('dom/TridentSelection.js'); + include('dom/Sizzle.js'); + include('dom/EventUtils.js'); + include('dom/Element.js'); + include('dom/Selection.js'); + include('dom/Serializer.js'); + include('dom/ScriptLoader.js'); + include('dom/TreeWalker.js'); + include('dom/RangeUtils.js'); + + // tinymce.ui.* + include('ui/KeyboardNavigation.js'); + include('ui/Control.js'); + include('ui/Container.js'); + include('ui/Separator.js'); + include('ui/MenuItem.js'); + include('ui/Menu.js'); + include('ui/DropMenu.js'); + include('ui/Button.js'); + include('ui/ListBox.js'); + include('ui/NativeListBox.js'); + include('ui/MenuButton.js'); + include('ui/SplitButton.js'); + include('ui/ColorSplitButton.js'); + include('ui/ToolbarGroup.js'); + include('ui/Toolbar.js'); + + // tinymce.* + include('AddOnManager.js'); + include('EditorManager.js'); + include('Editor.js'); + include('EditorCommands.js'); + include('UndoManager.js'); + include('ForceBlocks.js'); + include('ControlManager.js'); + include('WindowManager.js'); + include('Formatter.js'); + include('LegacyInput.js'); + + load(); +}()); diff --git a/js/tiny_mce/tiny_mce_jquery.js b/js/tiny_mce/tiny_mce_jquery.js index b93ed9bf45..e7139c08ad 100644 --- a/js/tiny_mce/tiny_mce_jquery.js +++ b/js/tiny_mce/tiny_mce_jquery.js @@ -1 +1 @@ -(function(c){var a=/^\s*|\s*$/g,d;var b={majorVersion:"3",minorVersion:"3.7",releaseDate:"2010-06-10",_init:function(){var r=this,o=document,m=navigator,f=m.userAgent,l,e,k,j,h,q;r.isOpera=c.opera&&opera.buildNumber;r.isWebKit=/WebKit/.test(f);r.isIE=!r.isWebKit&&!r.isOpera&&(/MSIE/gi).test(f)&&(/Explorer/gi).test(m.appName);r.isIE6=r.isIE&&/MSIE [56]/.test(f);r.isGecko=!r.isWebKit&&/Gecko/.test(f);r.isMac=f.indexOf("Mac")!=-1;r.isAir=/adobeair/i.test(f);r.isIDevice=/(iPad|iPhone)/.test(f);if(c.tinyMCEPreInit){r.suffix=tinyMCEPreInit.suffix;r.baseURL=tinyMCEPreInit.base;r.query=tinyMCEPreInit.query;return}r.suffix="";e=o.getElementsByTagName("base");for(l=0;l=c.length){for(e=0,b=g.length;e=c.length||g[e]!=c[e]){f=e+1;break}}}if(g.length=g.length||g[e]!=c[e]){f=e+1;break}}}if(f==1){return h}for(e=0,b=g.length-(f-1);e=0;c--){if(f[c].length==0||f[c]=="."){continue}if(f[c]==".."){b++;continue}if(b>0){b--;continue}h.push(f[c])}c=e.length-b;if(c<=0){g=h.reverse().join("/")}else{g=e.slice(0,c).join("/")+"/"+h.reverse().join("/")}if(g.indexOf("/")!==0){g="/"+g}if(d&&g.lastIndexOf("/")!==g.length-1){g+=d}return g},getURI:function(d){var c,b=this;if(!b.source||d){c="";if(!d){if(b.protocol){c+=b.protocol+"://"}if(b.userInfo){c+=b.userInfo+"@"}if(b.host){c+=b.host}if(b.port){c+=":"+b.port}}if(b.path){c+=b.path}if(b.query){c+="?"+b.query}if(b.anchor){c+="#"+b.anchor}b.source=c}return b.source}})})();(function(){var a=tinymce.each;tinymce.create("static tinymce.util.Cookie",{getHash:function(d){var b=this.get(d),c;if(b){a(b.split("&"),function(e){e=e.split("=");c=c||{};c[unescape(e[0])]=unescape(e[1])})}return c},setHash:function(j,b,g,f,i,c){var h="";a(b,function(e,d){h+=(!h?"":"&")+escape(d)+"="+escape(e)});this.set(j,h,g,f,i,c)},get:function(i){var h=document.cookie,g,f=i+"=",d;if(!h){return}d=h.indexOf("; "+f);if(d==-1){d=h.indexOf(f);if(d!=0){return null}}else{d+=2}g=h.indexOf(";",d);if(g==-1){g=h.length}return unescape(h.substring(d+f.length,g))},set:function(i,b,g,f,h,c){document.cookie=i+"="+escape(b)+((g)?"; expires="+g.toGMTString():"")+((f)?"; path="+escape(f):"")+((h)?"; domain="+h:"")+((c)?"; secure":"")},remove:function(e,b){var c=new Date();c.setTime(c.getTime()-1000);this.set(e,"",c,b,c)}})})();tinymce.create("static tinymce.util.JSON",{serialize:function(e){var c,a,d=tinymce.util.JSON.serialize,b;if(e==null){return"null"}b=typeof e;if(b=="string"){a="\bb\tt\nn\ff\rr\"\"''\\\\";return'"'+e.replace(/([\u0080-\uFFFF\x00-\x1f\"])/g,function(g,f){c=a.indexOf(f);if(c+1){return"\\"+a.charAt(c+1)}g=f.charCodeAt().toString(16);return"\\u"+"0000".substring(g.length)+g})+'"'}if(b=="object"){if(e.hasOwnProperty&&e instanceof Array){for(c=0,a="[";c0?",":"")+d(e[c])}return a+"]"}a="{";for(c in e){a+=typeof e[c]!="function"?(a.length>1?',"':'"')+c+'":'+d(e[c]):""}return a+"}"}return""+e},parse:function(s){try{return eval("("+s+")")}catch(ex){}}});tinymce.create("static tinymce.util.XHR",{send:function(g){var a,e,b=window,h=0;g.scope=g.scope||this;g.success_scope=g.success_scope||g.scope;g.error_scope=g.error_scope||g.scope;g.async=g.async===false?false:true;g.data=g.data||"";function d(i){a=0;try{a=new ActiveXObject(i)}catch(c){}return a}a=b.XMLHttpRequest?new XMLHttpRequest():d("Microsoft.XMLHTTP")||d("Msxml2.XMLHTTP");if(a){if(a.overrideMimeType){a.overrideMimeType(g.content_type)}a.open(g.type||(g.data?"POST":"GET"),g.url,g.async);if(g.content_type){a.setRequestHeader("Content-Type",g.content_type)}a.setRequestHeader("X-Requested-With","XMLHttpRequest");a.send(g.data);function f(){if(!g.async||a.readyState==4||h++>10000){if(g.success&&h<10000&&a.status==200){g.success.call(g.success_scope,""+a.responseText,a,g)}else{if(g.error){g.error.call(g.error_scope,h>10000?"TIMED_OUT":"GENERAL",a,g)}}a=null}else{b.setTimeout(f,10)}}if(!g.async){return f()}e=b.setTimeout(f,10)}}});(function(){var c=tinymce.extend,b=tinymce.util.JSON,a=tinymce.util.XHR;tinymce.create("tinymce.util.JSONRequest",{JSONRequest:function(d){this.settings=c({},d);this.count=0},send:function(f){var e=f.error,d=f.success;f=c(this.settings,f);f.success=function(h,g){h=b.parse(h);if(typeof(h)=="undefined"){h={error:"JSON Parse error."}}if(h.error){e.call(f.error_scope||f.scope,h.error,g)}else{d.call(f.success_scope||f.scope,h.result)}};f.error=function(h,g){e.call(f.error_scope||f.scope,h,g)};f.data=b.serialize({id:f.id||"c"+(this.count++),method:f.method,params:f.params});f.content_type="application/json";a.send(f)},"static":{sendRPC:function(d){return new tinymce.util.JSONRequest().send(d)}}})}());(function(m){var k=m.each,j=m.is,i=m.isWebKit,d=m.isIE,a=/^(H[1-6R]|P|DIV|ADDRESS|PRE|FORM|T(ABLE|BODY|HEAD|FOOT|H|R|D)|LI|OL|UL|CAPTION|BLOCKQUOTE|CENTER|DL|DT|DD|DIR|FIELDSET|NOSCRIPT|MENU|ISINDEX|SAMP)$/,e=g("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected"),f=g("src,href,style,coords,shape"),c={"&":"&",'"':""","<":"<",">":">"},n=/[<>&\"]/g,b=/^([a-z0-9],?)+$/i,h=/<(\w+)((?:\s+\w+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)(\s*\/?)>/g,l=/(\w+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g;function g(q){var p={},o;q=q.split(",");for(o=q.length;o>=0;o--){p[q[o]]=1}return p}m.create("tinymce.dom.DOMUtils",{doc:null,root:null,files:null,pixelStyles:/^(top|left|bottom|right|width|height|borderWidth)$/,props:{"for":"htmlFor","class":"className",className:"className",checked:"checked",disabled:"disabled",maxlength:"maxLength",readonly:"readOnly",selected:"selected",value:"value",id:"id",name:"name",type:"type"},DOMUtils:function(u,q){var p=this,o;p.doc=u;p.win=window;p.files={};p.cssFlicker=false;p.counter=0;p.boxModel=!m.isIE||u.compatMode=="CSS1Compat";p.stdMode=u.documentMode===8;p.settings=q=m.extend({keep_values:false,hex_colors:1,process_html:1},q);if(m.isIE6){try{u.execCommand("BackgroundImageCache",false,true)}catch(r){p.cssFlicker=true}}if(q.valid_styles){p._styles={};k(q.valid_styles,function(t,s){p._styles[s]=m.explode(t)})}m.addUnload(p.destroy,p)},getRoot:function(){var o=this,p=o.settings;return(p&&o.get(p.root_element))||o.doc.body},getViewPort:function(p){var q,o;p=!p?this.win:p;q=p.document;o=this.boxModel?q.documentElement:q.body;return{x:p.pageXOffset||o.scrollLeft,y:p.pageYOffset||o.scrollTop,w:p.innerWidth||o.clientWidth,h:p.innerHeight||o.clientHeight}},getRect:function(s){var r,o=this,q;s=o.get(s);r=o.getPos(s);q=o.getSize(s);return{x:r.x,y:r.y,w:q.w,h:q.h}},getSize:function(r){var p=this,o,q;r=p.get(r);o=p.getStyle(r,"width");q=p.getStyle(r,"height");if(o.indexOf("px")===-1){o=0}if(q.indexOf("px")===-1){q=0}return{w:parseInt(o)||r.offsetWidth||r.clientWidth,h:parseInt(q)||r.offsetHeight||r.clientHeight}},getParent:function(q,p,o){return this.getParents(q,p,o,false)},getParents:function(z,v,s,y){var q=this,p,u=q.settings,x=[];z=q.get(z);y=y===undefined;if(u.strict_root){s=s||q.getRoot()}if(j(v,"string")){p=v;if(v==="*"){v=function(o){return o.nodeType==1}}else{v=function(o){return q.is(o,p)}}}while(z){if(z==s||!z.nodeType||z.nodeType===9){break}if(!v||v(z)){if(y){x.push(z)}else{return z}}z=z.parentNode}return y?x:null},get:function(o){var p;if(o&&this.doc&&typeof(o)=="string"){p=o;o=this.doc.getElementById(o);if(o&&o.id!==p){return this.doc.getElementsByName(p)[1]}}return o},getNext:function(p,o){return this._findSib(p,o,"nextSibling")},getPrev:function(p,o){return this._findSib(p,o,"previousSibling")},add:function(s,v,o,r,u){var q=this;return this.run(s,function(y){var x,t;x=j(v,"string")?q.doc.createElement(v):v;q.setAttribs(x,o);if(r){if(r.nodeType){x.appendChild(r)}else{q.setHTML(x,r)}}return !u?y.appendChild(x):x})},create:function(q,o,p){return this.add(this.doc.createElement(q),q,o,p,1)},createHTML:function(v,p,s){var u="",r=this,q;u+="<"+v;for(q in p){if(p.hasOwnProperty(q)){u+=" "+q+'="'+r.encode(p[q])+'"'}}if(m.is(s)){return u+">"+s+""}return u+" />"},remove:function(o,p){return this.run(o,function(r){var q,s;q=r.parentNode;if(!q){return null}if(p){while(s=r.firstChild){if(!m.isIE||s.nodeType!==3||s.nodeValue){q.insertBefore(s,r)}else{r.removeChild(s)}}}return q.removeChild(r)})},setStyle:function(r,o,p){var q=this;return q.run(r,function(v){var u,t;u=v.style;o=o.replace(/-(\D)/g,function(x,s){return s.toUpperCase()});if(q.pixelStyles.test(o)&&(m.is(p,"number")||/^[\-0-9\.]+$/.test(p))){p+="px"}switch(o){case"opacity":if(d){u.filter=p===""?"":"alpha(opacity="+(p*100)+")";if(!r.currentStyle||!r.currentStyle.hasLayout){u.display="inline-block"}}u[o]=u["-moz-opacity"]=u["-khtml-opacity"]=p||"";break;case"float":d?u.styleFloat=p:u.cssFloat=p;break;default:u[o]=p||""}if(q.settings.update_styles){q.setAttrib(v,"_mce_style")}})},getStyle:function(r,o,q){r=this.get(r);if(!r){return false}if(this.doc.defaultView&&q){o=o.replace(/[A-Z]/g,function(s){return"-"+s});try{return this.doc.defaultView.getComputedStyle(r,null).getPropertyValue(o)}catch(p){return null}}o=o.replace(/-(\D)/g,function(t,s){return s.toUpperCase()});if(o=="float"){o=d?"styleFloat":"cssFloat"}if(r.currentStyle&&q){return r.currentStyle[o]}return r.style[o]},setStyles:function(u,v){var q=this,r=q.settings,p;p=r.update_styles;r.update_styles=0;k(v,function(o,s){q.setStyle(u,s,o)});r.update_styles=p;if(r.update_styles){q.setAttrib(u,r.cssText)}},setAttrib:function(q,r,o){var p=this;if(!q||!r){return}if(p.settings.strict){r=r.toLowerCase()}return this.run(q,function(u){var t=p.settings;switch(r){case"style":if(!j(o,"string")){k(o,function(s,x){p.setStyle(u,x,s)});return}if(t.keep_values){if(o&&!p._isRes(o)){u.setAttribute("_mce_style",o,2)}else{u.removeAttribute("_mce_style",2)}}u.style.cssText=o;break;case"class":u.className=o||"";break;case"src":case"href":if(t.keep_values){if(t.url_converter){o=t.url_converter.call(t.url_converter_scope||p,o,r,u)}p.setAttrib(u,"_mce_"+r,o,2)}break;case"shape":u.setAttribute("_mce_style",o);break}if(j(o)&&o!==null&&o.length!==0){u.setAttribute(r,""+o,2)}else{u.removeAttribute(r,2)}})},setAttribs:function(q,r){var p=this;return this.run(q,function(o){k(r,function(s,t){p.setAttrib(o,t,s)})})},getAttrib:function(r,s,q){var o,p=this;r=p.get(r);if(!r||r.nodeType!==1){return false}if(!j(q)){q=""}if(/^(src|href|style|coords|shape)$/.test(s)){o=r.getAttribute("_mce_"+s);if(o){return o}}if(d&&p.props[s]){o=r[p.props[s]];o=o&&o.nodeValue?o.nodeValue:o}if(!o){o=r.getAttribute(s,2)}if(/^(checked|compact|declare|defer|disabled|ismap|multiple|nohref|noshade|nowrap|readonly|selected)$/.test(s)){if(r[p.props[s]]===true&&o===""){return s}return o?s:""}if(r.nodeName==="FORM"&&r.getAttributeNode(s)){return r.getAttributeNode(s).nodeValue}if(s==="style"){o=o||r.style.cssText;if(o){o=p.serializeStyle(p.parseStyle(o),r.nodeName);if(p.settings.keep_values&&!p._isRes(o)){r.setAttribute("_mce_style",o)}}}if(i&&s==="class"&&o){o=o.replace(/(apple|webkit)\-[a-z\-]+/gi,"")}if(d){switch(s){case"rowspan":case"colspan":if(o===1){o=""}break;case"size":if(o==="+0"||o===20||o===0){o=""}break;case"width":case"height":case"vspace":case"checked":case"disabled":case"readonly":if(o===0){o=""}break;case"hspace":if(o===-1){o=""}break;case"maxlength":case"tabindex":if(o===32768||o===2147483647||o==="32768"){o=""}break;case"multiple":case"compact":case"noshade":case"nowrap":if(o===65535){return s}return q;case"shape":o=o.toLowerCase();break;default:if(s.indexOf("on")===0&&o){o=(""+o).replace(/^function\s+\w+\(\)\s+\{\s+(.*)\s+\}$/,"$1")}}}return(o!==undefined&&o!==null&&o!=="")?""+o:q},getPos:function(A,s){var p=this,o=0,z=0,u,v=p.doc,q;A=p.get(A);s=s||v.body;if(A){if(d&&!p.stdMode){A=A.getBoundingClientRect();u=p.boxModel?v.documentElement:v.body;o=p.getStyle(p.select("html")[0],"borderWidth");o=(o=="medium"||p.boxModel&&!p.isIE6)&&2||o;A.top+=p.win.self!=p.win.top?2:0;return{x:A.left+u.scrollLeft-o,y:A.top+u.scrollTop-o}}q=A;while(q&&q!=s&&q.nodeType){o+=q.offsetLeft||0;z+=q.offsetTop||0;q=q.offsetParent}q=A.parentNode;while(q&&q!=s&&q.nodeType){o-=q.scrollLeft||0;z-=q.scrollTop||0;q=q.parentNode}}return{x:o,y:z}},parseStyle:function(r){var u=this,v=u.settings,x={};if(!r){return x}function p(D,A,C){var z,B,o,y;z=x[D+"-top"+A];if(!z){return}B=x[D+"-right"+A];if(z!=B){return}o=x[D+"-bottom"+A];if(B!=o){return}y=x[D+"-left"+A];if(o!=y){return}x[C]=y;delete x[D+"-top"+A];delete x[D+"-right"+A];delete x[D+"-bottom"+A];delete x[D+"-left"+A]}function q(y,s,o,A){var z;z=x[s];if(!z){return}z=x[o];if(!z){return}z=x[A];if(!z){return}x[y]=x[s]+" "+x[o]+" "+x[A];delete x[s];delete x[o];delete x[A]}r=r.replace(/&(#?[a-z0-9]+);/g,"&$1_MCE_SEMI_");k(r.split(";"),function(s){var o,t=[];if(s){s=s.replace(/_MCE_SEMI_/g,";");s=s.replace(/url\([^\)]+\)/g,function(y){t.push(y);return"url("+t.length+")"});s=s.split(":");o=m.trim(s[1]);o=o.replace(/url\(([^\)]+)\)/g,function(z,y){return t[parseInt(y)-1]});o=o.replace(/rgb\([^\)]+\)/g,function(y){return u.toHex(y)});if(v.url_converter){o=o.replace(/url\([\'\"]?([^\)\'\"]+)[\'\"]?\)/g,function(y,z){return"url("+v.url_converter.call(v.url_converter_scope||u,u.decode(z),"style",null)+")"})}x[m.trim(s[0]).toLowerCase()]=o}});p("border","","border");p("border","-width","border-width");p("border","-color","border-color");p("border","-style","border-style");p("padding","","padding");p("margin","","margin");q("border","border-width","border-style","border-color");if(d){if(x.border=="medium none"){x.border=""}}return x},serializeStyle:function(v,p){var q=this,r="";function u(s,o){if(o&&s){if(o.indexOf("-")===0){return}switch(o){case"font-weight":if(s==700){s="bold"}break;case"color":case"background-color":s=s.toLowerCase();break}r+=(r?" ":"")+o+": "+s+";"}}if(p&&q._styles){k(q._styles["*"],function(o){u(v[o],o)});k(q._styles[p.toLowerCase()],function(o){u(v[o],o)})}else{k(v,u)}return r},loadCSS:function(o){var q=this,r=q.doc,p;if(!o){o=""}p=q.select("head")[0];k(o.split(","),function(s){var t;if(q.files[s]){return}q.files[s]=true;t=q.create("link",{rel:"stylesheet",href:m._addVer(s)});if(d&&r.documentMode){t.onload=function(){r.recalc();t.onload=null}}p.appendChild(t)})},addClass:function(o,p){return this.run(o,function(q){var r;if(!p){return 0}if(this.hasClass(q,p)){return q.className}r=this.removeClass(q,p);return q.className=(r!=""?(r+" "):"")+p})},removeClass:function(q,r){var o=this,p;return o.run(q,function(t){var s;if(o.hasClass(t,r)){if(!p){p=new RegExp("(^|\\s+)"+r+"(\\s+|$)","g")}s=t.className.replace(p," ");s=m.trim(s!=" "?s:"");t.className=s;if(!s){t.removeAttribute("class");t.removeAttribute("className")}return s}return t.className})},hasClass:function(p,o){p=this.get(p);if(!p||!o){return false}return(" "+p.className+" ").indexOf(" "+o+" ")!==-1},show:function(o){return this.setStyle(o,"display","block")},hide:function(o){return this.setStyle(o,"display","none")},isHidden:function(o){o=this.get(o);return !o||o.style.display=="none"||this.getStyle(o,"display")=="none"},uniqueId:function(o){return(!o?"mce_":o)+(this.counter++)},setHTML:function(q,p){var o=this;return this.run(q,function(v){var r,t,s,z,u,r;p=o.processHTML(p);if(d){function y(){while(v.firstChild){v.firstChild.removeNode()}try{v.innerHTML="
    "+p;v.removeChild(v.firstChild)}catch(x){r=o.create("div");r.innerHTML="
    "+p;k(r.childNodes,function(B,A){if(A){v.appendChild(B)}})}}if(o.settings.fix_ie_paragraphs){p=p.replace(/

    <\/p>|]+)><\/p>|/gi,' 

    ')}y();if(o.settings.fix_ie_paragraphs){s=v.getElementsByTagName("p");for(t=s.length-1,r=0;t>=0;t--){z=s[t];if(!z.hasChildNodes()){if(!z._mce_keep){r=1;break}z.removeAttribute("_mce_keep")}}}if(r){p=p.replace(/

    ]+)>|

    /ig,'

    ');p=p.replace(/<\/p>/gi,"
    ");y();if(o.settings.fix_ie_paragraphs){s=v.getElementsByTagName("DIV");for(t=s.length-1;t>=0;t--){z=s[t];if(z._mce_tmp){u=o.doc.createElement("p");z.cloneNode(false).outerHTML.replace(/([a-z0-9\-_]+)=/gi,function(A,x){var B;if(x!=="_mce_tmp"){B=z.getAttribute(x);if(!B&&x==="class"){B=z.className}u.setAttribute(x,B)}});for(r=0;r]+)\/>|/gi,"");if(q.keep_values){if(/)/g,"\n");t=t.replace(/^[\r\n]*|[\r\n]*$/g,"");t=t.replace(/^\s*(\/\/\s*|\]\]>|-->|\]\]-->)\s*$/g,"");return t}r=r.replace(/]+|)>([\s\S]*?)<\/script>/gi,function(s,x,t){if(!x){x=' type="text/javascript"'}x=x.replace(/src=\"([^\"]+)\"?/i,function(y,z){if(q.url_converter){z=p.encode(q.url_converter.call(q.url_converter_scope||p,p.decode(z),"src","script"))}return'_mce_src="'+z+'"'});if(m.trim(t)){v.push(o(t));t=""}return""+t+""});r=r.replace(/]+|)>([\s\S]*?)<\/style>/gi,function(s,x,t){if(t){v.push(o(t));t=""}return""+t+""});r=r.replace(/]+|)>([\s\S]*?)<\/noscript>/g,function(s,x,t){return""})}r=r.replace(//g,"");function u(s){return s.replace(h,function(y,z,x,t){return"<"+z+x.replace(l,function(B,A,E,D,C){var F;A=A.toLowerCase();E=E||D||C||"";if(e[A]){if(E==="false"||E==="0"){return}return A+'="'+A+'"'}if(f[A]&&x.indexOf("_mce_"+A)==-1){F=p.decode(E);if(q.url_converter&&(A=="src"||A=="href")){F=q.url_converter.call(q.url_converter_scope||p,F,A,z)}if(A=="style"){F=p.serializeStyle(p.parseStyle(F),A)}return A+'="'+E+'" _mce_'+A+'="'+p.encode(F)+'"'}return B})+t+">"})}r=u(r);r=r.replace(/MCE_SCRIPT:([0-9]+)/g,function(t,s){return v[s]})}return r},getOuterHTML:function(o){var p;o=this.get(o);if(!o){return null}if(o.outerHTML!==undefined){return o.outerHTML}p=(o.ownerDocument||this.doc).createElement("body");p.appendChild(o.cloneNode(true));return p.innerHTML},setOuterHTML:function(r,p,s){var o=this;function q(u,t,x){var y,v;v=x.createElement("body");v.innerHTML=t;y=v.lastChild;while(y){o.insertAfter(y.cloneNode(true),u);y=y.previousSibling}o.remove(u)}return this.run(r,function(u){u=o.get(u);if(u.nodeType==1){s=s||u.ownerDocument||o.doc;if(d){try{if(d&&u.nodeType==1){u.outerHTML=p}else{q(u,p,s)}}catch(t){q(u,p,s)}}else{q(u,p,s)}}})},decode:function(p){var q,r,o;if(/&[\w#]+;/.test(p)){q=this.doc.createElement("div");q.innerHTML=p;r=q.firstChild;o="";if(r){do{o+=r.nodeValue}while(r=r.nextSibling)}return o||p}return p},encode:function(o){return(""+o).replace(n,function(p){return c[p]})},insertAfter:function(o,p){p=this.get(p);return this.run(o,function(r){var q,s;q=p.parentNode;s=p.nextSibling;if(s){q.insertBefore(r,s)}else{q.appendChild(r)}return r})},isBlock:function(o){if(o.nodeType&&o.nodeType!==1){return false}o=o.nodeName||o;return a.test(o)},replace:function(s,r,p){var q=this;if(j(r,"array")){s=s.cloneNode(true)}return q.run(r,function(t){if(p){k(m.grep(t.childNodes),function(o){s.appendChild(o)})}return t.parentNode.replaceChild(s,t)})},rename:function(r,o){var q=this,p;if(r.nodeName!=o.toUpperCase()){p=q.create(o);k(q.getAttribs(r),function(s){q.setAttrib(p,s.nodeName,q.getAttrib(r,s.nodeName))});q.replace(p,r,1)}return p||r},findCommonAncestor:function(q,o){var r=q,p;while(r){p=o;while(p&&r!=p){p=p.parentNode}if(r==p){break}r=r.parentNode}if(!r&&q.ownerDocument){return q.ownerDocument.documentElement}return r},toHex:function(o){var q=/^\s*rgb\s*?\(\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?\)\s*$/i.exec(o);function p(r){r=parseInt(r).toString(16);return r.length>1?r:"0"+r}if(q){o="#"+p(q[1])+p(q[2])+p(q[3]);return o}return o},getClasses:function(){var s=this,o=[],r,u={},v=s.settings.class_filter,q;if(s.classes){return s.classes}function x(t){k(t.imports,function(y){x(y)});k(t.cssRules||t.rules,function(y){switch(y.type||1){case 1:if(y.selectorText){k(y.selectorText.split(","),function(z){z=z.replace(/^\s*|\s*$|^\s\./g,"");if(/\.mce/.test(z)||!/\.[\w\-]+$/.test(z)){return}q=z;z=z.replace(/.*\.([a-z0-9_\-]+).*/i,"$1");if(v&&!(z=v(z,q))){return}if(!u[z]){o.push({"class":z});u[z]=1}})}break;case 3:x(y.styleSheet);break}})}try{k(s.doc.styleSheets,x)}catch(p){}if(o.length>0){s.classes=o}return o},run:function(u,r,q){var p=this,v;if(p.doc&&typeof(u)==="string"){u=p.get(u)}if(!u){return false}q=q||this;if(!u.nodeType&&(u.length||u.length===0)){v=[];k(u,function(s,o){if(s){if(typeof(s)=="string"){s=p.doc.getElementById(s)}v.push(r.call(q,s,o))}});return v}return r.call(q,u)},getAttribs:function(q){var p;q=this.get(q);if(!q){return[]}if(d){p=[];if(q.nodeName=="OBJECT"){return q.attributes}if(q.nodeName==="OPTION"&&this.getAttrib(q,"selected")){p.push({specified:1,nodeName:"selected"})}q.cloneNode(false).outerHTML.replace(/<\/?[\w:\-]+ ?|=[\"][^\"]+\"|=\'[^\']+\'|=[\w\-]+|>/gi,"").replace(/[\w:\-]+/gi,function(o){p.push({specified:1,nodeName:o})});return p}return q.attributes},destroy:function(p){var o=this;if(o.events){o.events.destroy()}o.win=o.doc=o.root=o.events=null;if(!p){m.removeUnload(o.destroy)}},createRng:function(){var o=this.doc;return o.createRange?o.createRange():new m.dom.Range(this)},nodeIndex:function(s,t){var o=0,q,r,p;if(s){for(q=s.nodeType,s=s.previousSibling,r=s;s;s=s.previousSibling){p=s.nodeType;if(t&&p==3){if(p==q||!s.nodeValue.length){continue}}o++;q=p}}return o},split:function(u,s,y){var z=this,o=z.createRng(),v,q,x;function p(A){var t,r=A.childNodes;if(A.nodeType==1&&A.getAttribute("_mce_type")=="bookmark"){return}for(t=r.length-1;t>=0;t--){p(r[t])}if(A.nodeType!=9){if(A.nodeType==3&&A.nodeValue.length>0){return}if(A.nodeType==1){r=A.childNodes;if(r.length==1&&r[0]&&r[0].nodeType==1&&r[0].getAttribute("_mce_type")=="bookmark"){A.parentNode.insertBefore(r[0],A)}if(r.length||/^(br|hr|input|img)$/i.test(A.nodeName)){return}}z.remove(A)}return A}if(u&&s){o.setStart(u.parentNode,z.nodeIndex(u));o.setEnd(s.parentNode,z.nodeIndex(s));v=o.extractContents();o=z.createRng();o.setStart(s.parentNode,z.nodeIndex(s)+1);o.setEnd(u.parentNode,z.nodeIndex(u)+1);q=o.extractContents();x=u.parentNode;x.insertBefore(p(v),u);if(y){x.replaceChild(y,s)}else{x.insertBefore(s,u)}x.insertBefore(p(q),u);z.remove(u);return y||s}},bind:function(s,o,r,q){var p=this;if(!p.events){p.events=new m.dom.EventUtils()}return p.events.add(s,o,r,q||this)},unbind:function(r,o,q){var p=this;if(!p.events){p.events=new m.dom.EventUtils()}return p.events.remove(r,o,q)},_findSib:function(r,o,p){var q=this,s=o;if(r){if(j(s,"string")){s=function(t){return q.is(t,o)}}for(r=r[p];r;r=r[p]){if(s(r)){return r}}}return null},_isRes:function(o){return/^(top|left|bottom|right|width|height)/i.test(o)||/;\s*(top|left|bottom|right|width|height)/i.test(o)}});m.DOM=new m.dom.DOMUtils(document,{process_html:0})})(tinymce);(function(a){function b(c){var N=this,e=c.doc,S=0,E=1,j=2,D=true,R=false,U="startOffset",h="startContainer",P="endContainer",z="endOffset",k=tinymce.extend,n=c.nodeIndex;k(N,{startContainer:e,startOffset:0,endContainer:e,endOffset:0,collapsed:D,commonAncestorContainer:e,START_TO_START:0,START_TO_END:1,END_TO_END:2,END_TO_START:3,setStart:q,setEnd:s,setStartBefore:g,setStartAfter:I,setEndBefore:J,setEndAfter:u,collapse:A,selectNode:x,selectNodeContents:F,compareBoundaryPoints:v,deleteContents:p,extractContents:H,cloneContents:d,insertNode:C,surroundContents:M,cloneRange:K});function q(V,t){B(D,V,t)}function s(V,t){B(R,V,t)}function g(t){q(t.parentNode,n(t))}function I(t){q(t.parentNode,n(t)+1)}function J(t){s(t.parentNode,n(t))}function u(t){s(t.parentNode,n(t)+1)}function A(t){if(t){N[P]=N[h];N[z]=N[U]}else{N[h]=N[P];N[U]=N[z]}N.collapsed=D}function x(t){g(t);u(t)}function F(t){q(t,0);s(t,t.nodeType===1?t.childNodes.length:t.nodeValue.length)}function v(W,X){var Z=N[h],Y=N[U],V=N[P],t=N[z];if(W===0){return G(Z,Y,Z,Y)}if(W===1){return G(Z,Y,V,t)}if(W===2){return G(V,t,V,t)}if(W===3){return G(V,t,Z,Y)}}function p(){m(j)}function H(){return m(S)}function d(){return m(E)}function C(Y){var V=this[h],t=this[U],X,W;if((V.nodeType===3||V.nodeType===4)&&V.nodeValue){if(!t){V.parentNode.insertBefore(Y,V)}else{if(t>=V.nodeValue.length){c.insertAfter(Y,V)}else{X=V.splitText(t);V.parentNode.insertBefore(Y,X)}}}else{if(V.childNodes.length>0){W=V.childNodes[t]}if(W){V.insertBefore(Y,W)}else{V.appendChild(Y)}}}function M(V){var t=N.extractContents();N.insertNode(V);V.appendChild(t);N.selectNode(V)}function K(){return k(new b(c),{startContainer:N[h],startOffset:N[U],endContainer:N[P],endOffset:N[z],collapsed:N.collapsed,commonAncestorContainer:N.commonAncestorContainer})}function O(t,V){var W;if(t.nodeType==3){return t}if(V<0){return t}W=t.firstChild;while(W&&V>0){--V;W=W.nextSibling}if(W){return W}return t}function l(){return(N[h]==N[P]&&N[U]==N[z])}function G(X,Z,V,Y){var aa,W,t,ab,ad,ac;if(X==V){if(Z==Y){return 0}if(Z0){N.collapse(V)}}else{N.collapse(V)}N.collapsed=l();N.commonAncestorContainer=c.findCommonAncestor(N[h],N[P])}function m(ab){var aa,X=0,ad=0,V,Z,W,Y,t,ac;if(N[h]==N[P]){return f(ab)}for(aa=N[P],V=aa.parentNode;V;aa=V,V=V.parentNode){if(V==N[h]){return r(aa,ab)}++X}for(aa=N[h],V=aa.parentNode;V;aa=V,V=V.parentNode){if(V==N[P]){return T(aa,ab)}++ad}Z=ad-X;W=N[h];while(Z>0){W=W.parentNode;Z--}Y=N[P];while(Z<0){Y=Y.parentNode;Z++}for(t=W.parentNode,ac=Y.parentNode;t!=ac;t=t.parentNode,ac=ac.parentNode){W=t;Y=ac}return o(W,Y,ab)}function f(Z){var ab,Y,X,aa,t,W,V;if(Z!=j){ab=e.createDocumentFragment()}if(N[U]==N[z]){return ab}if(N[h].nodeType==3){Y=N[h].nodeValue;X=Y.substring(N[U],N[z]);if(Z!=E){N[h].deleteData(N[U],N[z]-N[U]);N.collapse(D)}if(Z==j){return}ab.appendChild(e.createTextNode(X));return ab}aa=O(N[h],N[U]);t=N[z]-N[U];while(t>0){W=aa.nextSibling;V=y(aa,Z);if(ab){ab.appendChild(V)}--t;aa=W}if(Z!=E){N.collapse(D)}return ab}function r(ab,Y){var aa,Z,V,t,X,W;if(Y!=j){aa=e.createDocumentFragment()}Z=i(ab,Y);if(aa){aa.appendChild(Z)}V=n(ab);t=V-N[U];if(t<=0){if(Y!=E){N.setEndBefore(ab);N.collapse(R)}return aa}Z=ab.previousSibling;while(t>0){X=Z.previousSibling;W=y(Z,Y);if(aa){aa.insertBefore(W,aa.firstChild)}--t;Z=X}if(Y!=E){N.setEndBefore(ab);N.collapse(R)}return aa}function T(Z,Y){var ab,V,aa,t,X,W;if(Y!=j){ab=e.createDocumentFragment()}aa=Q(Z,Y);if(ab){ab.appendChild(aa)}V=n(Z);++V;t=N[z]-V;aa=Z.nextSibling;while(t>0){X=aa.nextSibling;W=y(aa,Y);if(ab){ab.appendChild(W)}--t;aa=X}if(Y!=E){N.setStartAfter(Z);N.collapse(D)}return ab}function o(Z,t,ac){var W,ae,Y,aa,ab,V,ad,X;if(ac!=j){ae=e.createDocumentFragment()}W=Q(Z,ac);if(ae){ae.appendChild(W)}Y=Z.parentNode;aa=n(Z);ab=n(t);++aa;V=ab-aa;ad=Z.nextSibling;while(V>0){X=ad.nextSibling;W=y(ad,ac);if(ae){ae.appendChild(W)}ad=X;--V}W=i(t,ac);if(ae){ae.appendChild(W)}if(ac!=E){N.setStartAfter(Z);N.collapse(D)}return ae}function i(aa,ab){var W=O(N[P],N[z]-1),ac,Z,Y,t,V,X=W!=N[P];if(W==aa){return L(W,X,R,ab)}ac=W.parentNode;Z=L(ac,R,R,ab);while(ac){while(W){Y=W.previousSibling;t=L(W,X,R,ab);if(ab!=j){Z.insertBefore(t,Z.firstChild)}X=D;W=Y}if(ac==aa){return Z}W=ac.previousSibling;ac=ac.parentNode;V=L(ac,R,R,ab);if(ab!=j){V.appendChild(Z)}Z=V}}function Q(aa,ab){var X=O(N[h],N[U]),Y=X!=N[h],ac,Z,W,t,V;if(X==aa){return L(X,Y,D,ab)}ac=X.parentNode;Z=L(ac,R,D,ab);while(ac){while(X){W=X.nextSibling;t=L(X,Y,D,ab);if(ab!=j){Z.appendChild(t)}Y=D;X=W}if(ac==aa){return Z}X=ac.nextSibling;ac=ac.parentNode;V=L(ac,R,D,ab);if(ab!=j){V.appendChild(Z)}Z=V}}function L(t,Y,ab,ac){var X,W,Z,V,aa;if(Y){return y(t,ac)}if(t.nodeType==3){X=t.nodeValue;if(ab){V=N[U];W=X.substring(V);Z=X.substring(0,V)}else{V=N[z];W=X.substring(0,V);Z=X.substring(V)}if(ac!=E){t.nodeValue=Z}if(ac==j){return}aa=t.cloneNode(R);aa.nodeValue=W;return aa}if(ac==j){return}return t.cloneNode(R)}function y(V,t){if(t!=j){return t==E?V.cloneNode(D):V}V.parentNode.removeChild(V)}}a.Range=b})(tinymce.dom);(function(){function a(g){var i=this,j="\uFEFF",e,h,d=g.dom,c=true,f=false;function b(){var n=g.getRng(),k=d.createRng(),m,o;m=n.item?n.item(0):n.parentElement();if(m.ownerDocument!=d.doc){return k}if(n.item||!m.hasChildNodes()){k.setStart(m.parentNode,d.nodeIndex(m));k.setEnd(k.startContainer,k.startOffset+1);return k}o=g.isCollapsed();function l(s){var u,q,t,p,A=0,x,y,z,r,v;r=n.duplicate();r.collapse(s);u=d.create("a");z=r.parentElement();if(!z.hasChildNodes()){k[s?"setStart":"setEnd"](z,0);return}z.appendChild(u);r.moveToElementText(u);v=n.compareEndPoints(s?"StartToStart":"EndToEnd",r);if(v>0){k[s?"setStartAfter":"setEndAfter"](z);d.remove(u);return}p=tinymce.grep(z.childNodes);x=p.length-1;while(A<=x){y=Math.floor((A+x)/2);z.insertBefore(u,p[y]);r.moveToElementText(u);v=n.compareEndPoints(s?"StartToStart":"EndToEnd",r);if(v>0){A=y+1}else{if(v<0){x=y-1}else{found=true;break}}}q=v>0||y==0?u.nextSibling:u.previousSibling;if(q.nodeType==1){d.remove(u);t=d.nodeIndex(q);q=q.parentNode;if(!s||y>0){t++}}else{if(v>0||y==0){r.setEndPoint(s?"StartToStart":"EndToEnd",n);t=r.text.length}else{r.setEndPoint(s?"StartToStart":"EndToEnd",n);t=q.nodeValue.length-r.text.length}d.remove(u)}k[s?"setStart":"setEnd"](q,t)}l(true);if(!o){l()}return k}this.addRange=function(l){var t,A,z=g.dom.doc,r=z.body,u,n,y,o,s,k,p,q,x,m;this.destroy();y=l.startContainer;o=l.startOffset;s=l.endContainer;k=l.endOffset;t=r.createTextRange();if(y==z||s==z){t=r.createTextRange();t.collapse();t.select();return}if(y.nodeType==1&&y.hasChildNodes()){q=y.childNodes.length-1;if(o>q){x=1;y=y.childNodes[q]}else{y=y.childNodes[o]}if(y.nodeType==3){o=0}}if(s.nodeType==1&&s.hasChildNodes()){q=s.childNodes.length-1;if(k==0){m=1;s=s.childNodes[0]}else{s=s.childNodes[Math.min(q,k-1)];if(s.nodeType==3){k=s.nodeValue.length}}}if(y==s&&y.nodeType==1){if(/^(IMG|TABLE)$/.test(y.nodeName)&&o!=k){t=r.createControlRange();t.addElement(y)}else{t=r.createTextRange();if(!y.hasChildNodes()&&y.canHaveHTML){y.innerHTML=j}t.moveToElementText(y);if(y.innerHTML==j){t.collapse(c);y.removeChild(y.firstChild)}}if(o==k){t.collapse(k<=l.endContainer.childNodes.length-1)}t.select();t.scrollIntoView();return}t=r.createTextRange();p=z.createElement("span");p.innerHTML=" ";if(y.nodeType==3){if(x){d.insertAfter(p,y)}else{y.parentNode.insertBefore(p,y)}t.moveToElementText(p);p.parentNode.removeChild(p);if(o>0){t.move("character",o)}}else{t.moveToElementText(y);if(x){t.collapse(f)}}if(y==s&&y.nodeType==3){try{t.moveEnd("character",k-o);t.select();t.scrollIntoView()}catch(v){}return}A=r.createTextRange();if(s.nodeType==3){s.parentNode.insertBefore(p,s);A.moveToElementText(p);p.parentNode.removeChild(p);A.move("character",k);t.setEndPoint("EndToStart",A)}else{A.moveToElementText(s);A.collapse(!!m);t.setEndPoint("EndToEnd",A)}t.select();t.scrollIntoView()};this.getRangeAt=function(){if(!e||!tinymce.dom.RangeUtils.compareRanges(h,g.getRng())){e=b();h=g.getRng()}try{e.startContainer.nextSibling}catch(k){e=b();h=null}return e};this.destroy=function(){h=e=null};if(g.dom.boxModel){(function(){var q=d.doc,l=q.body,n,o;q.documentElement.unselectable=c;function p(r,u){var s=l.createTextRange();try{s.moveToPoint(r,u)}catch(t){s=null}return s}function m(s){var r;if(s.button){r=p(s.x,s.y);if(r){if(r.compareEndPoints("StartToStart",o)>0){r.setEndPoint("StartToStart",o)}else{r.setEndPoint("EndToEnd",o)}r.select()}}else{k()}}function k(){d.unbind(q,"mouseup",k);d.unbind(q,"mousemove",m);n=0}d.bind(q,"mousedown",function(r){if(r.target.nodeName==="HTML"){if(n){k()}n=1;o=p(r.x,r.y);if(o){d.bind(q,"mouseup",k);d.bind(q,"mousemove",m);o.select()}}})})()}}tinymce.dom.TridentSelection=a})();(function(d){var f=d.each,c=d.DOM,b=d.isIE,e=d.isWebKit,a;d.create("tinymce.dom.EventUtils",{EventUtils:function(){this.inits=[];this.events=[]},add:function(m,p,l,j){var g,h=this,i=h.events,k;if(p instanceof Array){k=[];f(p,function(o){k.push(h.add(m,o,l,j))});return k}if(m&&m.hasOwnProperty&&m instanceof Array){k=[];f(m,function(n){n=c.get(n);k.push(h.add(n,p,l,j))});return k}m=c.get(m);if(!m){return}g=function(n){if(h.disabled){return}n=n||window.event;if(n&&b){if(!n.target){n.target=n.srcElement}d.extend(n,h._stoppers)}if(!j){return l(n)}return l.call(j,n)};if(p=="unload"){d.unloads.unshift({func:g});return g}if(p=="init"){if(h.domLoaded){g()}else{h.inits.push(g)}return g}i.push({obj:m,name:p,func:l,cfunc:g,scope:j});h._add(m,p,g);return l},remove:function(l,m,k){var h=this,g=h.events,i=false,j;if(l&&l.hasOwnProperty&&l instanceof Array){j=[];f(l,function(n){n=c.get(n);j.push(h.remove(n,m,k))});return j}l=c.get(l);f(g,function(o,n){if(o.obj==l&&o.name==m&&(!k||(o.func==k||o.cfunc==k))){g.splice(n,1);h._remove(l,m,o.cfunc);i=true;return false}});return i},clear:function(l){var j=this,g=j.events,h,k;if(l){l=c.get(l);for(h=g.length-1;h>=0;h--){k=g[h];if(k.obj===l){j._remove(k.obj,k.name,k.cfunc);k.obj=k.cfunc=null;g.splice(h,1)}}}},cancel:function(g){if(!g){return false}this.stop(g);return this.prevent(g)},stop:function(g){if(g.stopPropagation){g.stopPropagation()}else{g.cancelBubble=true}return false},prevent:function(g){if(g.preventDefault){g.preventDefault()}else{g.returnValue=false}return false},destroy:function(){var g=this;f(g.events,function(j,h){g._remove(j.obj,j.name,j.cfunc);j.obj=j.cfunc=null});g.events=[];g=null},_add:function(h,i,g){if(h.attachEvent){h.attachEvent("on"+i,g)}else{if(h.addEventListener){h.addEventListener(i,g,false)}else{h["on"+i]=g}}},_remove:function(i,j,h){if(i){try{if(i.detachEvent){i.detachEvent("on"+j,h)}else{if(i.removeEventListener){i.removeEventListener(j,h,false)}else{i["on"+j]=null}}}catch(g){}}},_pageInit:function(h){var g=this;if(g.domLoaded){return}g.domLoaded=true;f(g.inits,function(i){i()});g.inits=[]},_wait:function(i){var g=this,h=i.document;if(i.tinyMCE_GZ&&tinyMCE_GZ.loaded){g.domLoaded=1;return}if(h.attachEvent){h.attachEvent("onreadystatechange",function(){if(h.readyState==="complete"){h.detachEvent("onreadystatechange",arguments.callee);g._pageInit(i)}});if(h.documentElement.doScroll&&i==i.top){(function(){if(g.domLoaded){return}try{h.documentElement.doScroll("left")}catch(j){setTimeout(arguments.callee,0);return}g._pageInit(i)})()}}else{if(h.addEventListener){g._add(i,"DOMContentLoaded",function(){g._pageInit(i)})}}g._add(i,"load",function(){g._pageInit(i)})},_stoppers:{preventDefault:function(){this.returnValue=false},stopPropagation:function(){this.cancelBubble=true}}});a=d.dom.Event=new d.dom.EventUtils();a._wait(window);d.addUnload(function(){a.destroy()})})(tinymce);(function(a){a.dom.Element=function(f,d){var b=this,e,c;b.settings=d=d||{};b.id=f;b.dom=e=d.dom||a.DOM;if(!a.isIE){c=e.get(b.id)}a.each(("getPos,getRect,getParent,add,setStyle,getStyle,setStyles,setAttrib,setAttribs,getAttrib,addClass,removeClass,hasClass,getOuterHTML,setOuterHTML,remove,show,hide,isHidden,setHTML,get").split(/,/),function(g){b[g]=function(){var h=[f],j;for(j=0;j_';if(j.startContainer==k&&j.endContainer==k){k.body.innerHTML=i}else{j.deleteContents();if(k.body.childNodes.length==0){k.body.innerHTML=i}else{j.insertNode(j.createContextualFragment(i))}}l=f.dom.get("__caret");j=k.createRange();j.setStartBefore(l);j.setEndBefore(l);f.setRng(j);f.dom.remove("__caret")}else{if(j.item){k.execCommand("Delete",false,null);j=f.getRng()}j.pasteHTML(i)}f.onSetContent.dispatch(f,g)},getStart:function(){var g=this.getRng(),h,f,j,i;if(g.duplicate||g.item){if(g.item){return g.item(0)}j=g.duplicate();j.collapse(1);h=j.parentElement();f=i=g.parentElement();while(i=i.parentNode){if(i==h){h=f;break}}if(h&&h.nodeName=="BODY"){return h.firstChild||h}return h}else{h=g.startContainer;if(h.nodeType==1&&h.hasChildNodes()){h=h.childNodes[Math.min(h.childNodes.length-1,g.startOffset)]}if(h&&h.nodeType==3){return h.parentNode}return h}},getEnd:function(){var g=this,h=g.getRng(),i,f;if(h.duplicate||h.item){if(h.item){return h.item(0)}h=h.duplicate();h.collapse(0);i=h.parentElement();if(i&&i.nodeName=="BODY"){return i.lastChild||i}return i}else{i=h.endContainer;f=h.endOffset;if(i.nodeType==1&&i.hasChildNodes()){i=i.childNodes[f>0?f-1:f]}if(i&&i.nodeType==3){return i.parentNode}return i}},getBookmark:function(q,r){var u=this,m=u.dom,g,j,i,n,h,o,p,l="\uFEFF",s;function f(v,x){var t=0;d(m.select(v),function(z,y){if(z==x){t=y}});return t}if(q==2){function k(){var v=u.getRng(true),t=m.getRoot(),x={};function y(B,G){var A=B[G?"startContainer":"endContainer"],F=B[G?"startOffset":"endOffset"],z=[],C,E,D=0;if(A.nodeType==3){if(r){for(C=A.previousSibling;C&&C.nodeType==3;C=C.previousSibling){F+=C.nodeValue.length}}z.push(F)}else{E=A.childNodes;if(F>=E.length&&E.length){D=1;F=Math.max(0,E.length-1)}z.push(u.dom.nodeIndex(E[F],r)+D)}for(;A&&A!=t;A=A.parentNode){z.push(u.dom.nodeIndex(A,r))}return z}x.start=y(v,true);if(!u.isCollapsed()){x.end=y(v)}return x}return k()}if(q){return{rng:u.getRng()}}g=u.getRng();i=m.uniqueId();n=tinyMCE.activeEditor.selection.isCollapsed();s="overflow:hidden;line-height:0px";if(g.duplicate||g.item){if(!g.item){j=g.duplicate();g.collapse();g.pasteHTML(''+l+"");if(!n){j.collapse(false);j.pasteHTML(''+l+"")}}else{o=g.item(0);h=o.nodeName;return{name:h,index:f(h,o)}}}else{o=u.getNode();h=o.nodeName;if(h=="IMG"){return{name:h,index:f(h,o)}}j=g.cloneRange();if(!n){j.collapse(false);j.insertNode(m.create("span",{_mce_type:"bookmark",id:i+"_end",style:s},l))}g.collapse(true);g.insertNode(m.create("span",{_mce_type:"bookmark",id:i+"_start",style:s},l))}u.moveToBookmark({id:i,keep:1});return{id:i}},moveToBookmark:function(n){var r=this,l=r.dom,i,h,f,q,j,s,o,p;if(r.tridentSel){r.tridentSel.destroy()}if(n){if(n.start){f=l.createRng();q=l.getRoot();function g(z){var t=n[z?"start":"end"],v,x,y,u;if(t){for(x=q,v=t.length-1;v>=1;v--){u=x.childNodes;if(u.length){x=u[t[v]]}}if(z){f.setStart(x,t[0])}else{f.setEnd(x,t[0])}}}g(true);g();r.setRng(f)}else{if(n.id){function k(A){var u=l.get(n.id+"_"+A),z,t,x,y,v=n.keep;if(u){z=u.parentNode;if(A=="start"){if(!v){t=l.nodeIndex(u)}else{z=u.firstChild;t=1}j=s=z;o=p=t}else{if(!v){t=l.nodeIndex(u)}else{z=u.firstChild;t=1}s=z;p=t}if(!v){y=u.previousSibling;x=u.nextSibling;d(c.grep(u.childNodes),function(B){if(B.nodeType==3){B.nodeValue=B.nodeValue.replace(/\uFEFF/g,"")}});while(u=l.get(n.id+"_"+A)){l.remove(u,1)}if(y&&x&&y.nodeType==x.nodeType&&y.nodeType==3){t=y.nodeValue.length;y.appendData(x.nodeValue);l.remove(x);if(A=="start"){j=s=y;o=p=t}else{s=y;p=t}}}}}function m(t){if(!a&&l.isBlock(t)&&!t.innerHTML){t.innerHTML='
    '}return t}k("start");k("end");f=l.createRng();f.setStart(m(j),o);f.setEnd(m(s),p);r.setRng(f)}else{if(n.name){r.select(l.select(n.name)[n.index])}else{if(n.rng){r.setRng(n.rng)}}}}}},select:function(k,j){var i=this,l=i.dom,g=l.createRng(),f;f=l.nodeIndex(k);g.setStart(k.parentNode,f);g.setEnd(k.parentNode,f+1);if(j){function h(m,o){var n=new c.dom.TreeWalker(m,m);do{if(m.nodeType==3&&c.trim(m.nodeValue).length!=0){if(o){g.setStart(m,0)}else{g.setEnd(m,m.nodeValue.length)}return}if(m.nodeName=="BR"){if(o){g.setStartBefore(m)}else{g.setEndBefore(m)}return}}while(m=(o?n.next():n.prev()))}h(k,1);h(k)}i.setRng(g);return k},isCollapsed:function(){var f=this,h=f.getRng(),g=f.getSel();if(!h||h.item){return false}if(h.compareEndPoints){return h.compareEndPoints("StartToEnd",h)===0}return !g||h.collapsed},collapse:function(f){var g=this,h=g.getRng(),i;if(h.item){i=h.item(0);h=this.win.document.body.createTextRange();h.moveToElementText(i)}h.collapse(!!f);g.setRng(h)},getSel:function(){var g=this,f=this.win;return f.getSelection?f.getSelection():f.document.selection},getRng:function(j){var g=this,h,i;if(j&&g.tridentSel){return g.tridentSel.getRangeAt(0)}try{if(h=g.getSel()){i=h.rangeCount>0?h.getRangeAt(0):(h.createRange?h.createRange():g.win.document.createRange())}}catch(f){}if(!i){i=g.win.document.createRange?g.win.document.createRange():g.win.document.body.createTextRange()}if(g.selectedRange&&g.explicitRange){if(i.compareBoundaryPoints(i.START_TO_START,g.selectedRange)===0&&i.compareBoundaryPoints(i.END_TO_END,g.selectedRange)===0){i=g.explicitRange}else{g.selectedRange=null;g.explicitRange=null}}return i},setRng:function(i){var h,g=this;if(!g.tridentSel){h=g.getSel();if(h){g.explicitRange=i;h.removeAllRanges();h.addRange(i);g.selectedRange=h.getRangeAt(0)}}else{if(i.cloneRange){g.tridentSel.addRange(i);return}try{i.select()}catch(f){}}},setNode:function(g){var f=this;f.setContent(f.dom.getOuterHTML(g));return g},getNode:function(){var g=this,f=g.getRng(),h=g.getSel(),i;if(f.setStart){if(!f){return g.dom.getRoot()}i=f.commonAncestorContainer;if(!f.collapsed){if(f.startContainer==f.endContainer){if(f.startOffset-f.endOffset<2){if(f.startContainer.hasChildNodes()){i=f.startContainer.childNodes[f.startOffset]}}}if(c.isWebKit&&h.anchorNode&&h.anchorNode.nodeType==1){return h.anchorNode.childNodes[h.anchorOffset]}}if(i&&i.nodeType==3){return i.parentNode}return i}return f.item?f.item(0):f.parentElement()},getSelectedBlocks:function(g,f){var i=this,j=i.dom,m,h,l,k=[];m=j.getParent(g||i.getStart(),j.isBlock);h=j.getParent(f||i.getEnd(),j.isBlock);if(m){k.push(m)}if(m&&h&&m!=h){l=m;while((l=l.nextSibling)&&l!=h){if(j.isBlock(l)){k.push(l)}}}if(h&&m!=h){k.push(h)}return k},destroy:function(g){var f=this;f.win=null;if(f.tridentSel){f.tridentSel.destroy()}if(!g){c.removeUnload(f.destroy)}}})})(tinymce);(function(a){a.create("tinymce.dom.XMLWriter",{node:null,XMLWriter:function(c){function b(){var e=document.implementation;if(!e||!e.createDocument){try{return new ActiveXObject("MSXML2.DOMDocument")}catch(d){}try{return new ActiveXObject("Microsoft.XmlDom")}catch(d){}}else{return e.createDocument("","",null)}}this.doc=b();this.valid=a.isOpera||a.isWebKit;this.reset()},reset:function(){var b=this,c=b.doc;if(c.firstChild){c.removeChild(c.firstChild)}b.node=c.appendChild(c.createElement("html"))},writeStartElement:function(c){var b=this;b.node=b.node.appendChild(b.doc.createElement(c))},writeAttribute:function(c,b){if(this.valid){b=b.replace(/>/g,"%MCGT%")}this.node.setAttribute(c,b)},writeEndElement:function(){this.node=this.node.parentNode},writeFullEndElement:function(){var b=this,c=b.node;c.appendChild(b.doc.createTextNode(""));b.node=c.parentNode},writeText:function(b){if(this.valid){b=b.replace(/>/g,"%MCGT%")}this.node.appendChild(this.doc.createTextNode(b))},writeCDATA:function(b){this.node.appendChild(this.doc.createCDATASection(b))},writeComment:function(b){if(a.isIE){b=b.replace(/^\-|\-$/g," ")}this.node.appendChild(this.doc.createComment(b.replace(/\-\-/g," ")))},getContent:function(){var b;b=this.doc.xml||new XMLSerializer().serializeToString(this.doc);b=b.replace(/<\?[^?]+\?>||<\/html>||]+>/g,"");b=b.replace(/ ?\/>/g," />");if(this.valid){b=b.replace(/\%MCGT%/g,">")}return b}})})(tinymce);(function(a){a.create("tinymce.dom.StringWriter",{str:null,tags:null,count:0,settings:null,indent:null,StringWriter:function(b){this.settings=a.extend({indent_char:" ",indentation:0},b);this.reset()},reset:function(){this.indent="";this.str="";this.tags=[];this.count=0},writeStartElement:function(b){this._writeAttributesEnd();this.writeRaw("<"+b);this.tags.push(b);this.inAttr=true;this.count++;this.elementCount=this.count},writeAttribute:function(d,b){var c=this;c.writeRaw(" "+c.encode(d)+'="'+c.encode(b)+'"')},writeEndElement:function(){var b;if(this.tags.length>0){b=this.tags.pop();if(this._writeAttributesEnd(1)){this.writeRaw("")}if(this.settings.indentation>0){this.writeRaw("\n")}}},writeFullEndElement:function(){if(this.tags.length>0){this._writeAttributesEnd();this.writeRaw("");if(this.settings.indentation>0){this.writeRaw("\n")}}},writeText:function(b){this._writeAttributesEnd();this.writeRaw(this.encode(b));this.count++},writeCDATA:function(b){this._writeAttributesEnd();this.writeRaw("");this.count++},writeComment:function(b){this._writeAttributesEnd();this.writeRaw("");this.count++},writeRaw:function(b){this.str+=b},encode:function(b){return b.replace(/[<>&"]/g,function(c){switch(c){case"<":return"<";case">":return">";case"&":return"&";case'"':return"""}return c})},getContent:function(){return this.str},_writeAttributesEnd:function(b){if(!this.inAttr){return}this.inAttr=false;if(b&&this.elementCount==this.count){this.writeRaw(" />");return false}this.writeRaw(">");return true}})})(tinymce);(function(e){var g=e.extend,f=e.each,b=e.util.Dispatcher,d=e.isIE,a=e.isGecko;function c(h){return h.replace(/([?+*])/g,".$1")}e.create("tinymce.dom.Serializer",{Serializer:function(j){var i=this;i.key=0;i.onPreProcess=new b(i);i.onPostProcess=new b(i);try{i.writer=new e.dom.XMLWriter()}catch(h){i.writer=new e.dom.StringWriter()}i.settings=j=g({dom:e.DOM,valid_nodes:0,node_filter:0,attr_filter:0,invalid_attrs:/^(_mce_|_moz_|sizset|sizcache)/,closed:/^(br|hr|input|meta|img|link|param|area)$/,entity_encoding:"named",entities:"160,nbsp,161,iexcl,162,cent,163,pound,164,curren,165,yen,166,brvbar,167,sect,168,uml,169,copy,170,ordf,171,laquo,172,not,173,shy,174,reg,175,macr,176,deg,177,plusmn,178,sup2,179,sup3,180,acute,181,micro,182,para,183,middot,184,cedil,185,sup1,186,ordm,187,raquo,188,frac14,189,frac12,190,frac34,191,iquest,192,Agrave,193,Aacute,194,Acirc,195,Atilde,196,Auml,197,Aring,198,AElig,199,Ccedil,200,Egrave,201,Eacute,202,Ecirc,203,Euml,204,Igrave,205,Iacute,206,Icirc,207,Iuml,208,ETH,209,Ntilde,210,Ograve,211,Oacute,212,Ocirc,213,Otilde,214,Ouml,215,times,216,Oslash,217,Ugrave,218,Uacute,219,Ucirc,220,Uuml,221,Yacute,222,THORN,223,szlig,224,agrave,225,aacute,226,acirc,227,atilde,228,auml,229,aring,230,aelig,231,ccedil,232,egrave,233,eacute,234,ecirc,235,euml,236,igrave,237,iacute,238,icirc,239,iuml,240,eth,241,ntilde,242,ograve,243,oacute,244,ocirc,245,otilde,246,ouml,247,divide,248,oslash,249,ugrave,250,uacute,251,ucirc,252,uuml,253,yacute,254,thorn,255,yuml,402,fnof,913,Alpha,914,Beta,915,Gamma,916,Delta,917,Epsilon,918,Zeta,919,Eta,920,Theta,921,Iota,922,Kappa,923,Lambda,924,Mu,925,Nu,926,Xi,927,Omicron,928,Pi,929,Rho,931,Sigma,932,Tau,933,Upsilon,934,Phi,935,Chi,936,Psi,937,Omega,945,alpha,946,beta,947,gamma,948,delta,949,epsilon,950,zeta,951,eta,952,theta,953,iota,954,kappa,955,lambda,956,mu,957,nu,958,xi,959,omicron,960,pi,961,rho,962,sigmaf,963,sigma,964,tau,965,upsilon,966,phi,967,chi,968,psi,969,omega,977,thetasym,978,upsih,982,piv,8226,bull,8230,hellip,8242,prime,8243,Prime,8254,oline,8260,frasl,8472,weierp,8465,image,8476,real,8482,trade,8501,alefsym,8592,larr,8593,uarr,8594,rarr,8595,darr,8596,harr,8629,crarr,8656,lArr,8657,uArr,8658,rArr,8659,dArr,8660,hArr,8704,forall,8706,part,8707,exist,8709,empty,8711,nabla,8712,isin,8713,notin,8715,ni,8719,prod,8721,sum,8722,minus,8727,lowast,8730,radic,8733,prop,8734,infin,8736,ang,8743,and,8744,or,8745,cap,8746,cup,8747,int,8756,there4,8764,sim,8773,cong,8776,asymp,8800,ne,8801,equiv,8804,le,8805,ge,8834,sub,8835,sup,8836,nsub,8838,sube,8839,supe,8853,oplus,8855,otimes,8869,perp,8901,sdot,8968,lceil,8969,rceil,8970,lfloor,8971,rfloor,9001,lang,9002,rang,9674,loz,9824,spades,9827,clubs,9829,hearts,9830,diams,338,OElig,339,oelig,352,Scaron,353,scaron,376,Yuml,710,circ,732,tilde,8194,ensp,8195,emsp,8201,thinsp,8204,zwnj,8205,zwj,8206,lrm,8207,rlm,8211,ndash,8212,mdash,8216,lsquo,8217,rsquo,8218,sbquo,8220,ldquo,8221,rdquo,8222,bdquo,8224,dagger,8225,Dagger,8240,permil,8249,lsaquo,8250,rsaquo,8364,euro",valid_elements:"*[*]",extended_valid_elements:0,invalid_elements:0,fix_table_elements:1,fix_list_elements:true,fix_content_duplication:true,convert_fonts_to_spans:false,font_size_classes:0,apply_source_formatting:0,indent_mode:"simple",indent_char:"\t",indent_levels:1,remove_linebreaks:1,remove_redundant_brs:1,element_format:"xhtml"},j);i.dom=j.dom;i.schema=j.schema;if(j.entity_encoding=="named"&&!j.entities){j.entity_encoding="raw"}if(j.remove_redundant_brs){i.onPostProcess.add(function(k,l){l.content=l.content.replace(/(
    \s*)+<\/(p|h[1-6]|div|li)>/gi,function(n,m,o){if(/^
    \s*<\//.test(n)){return""}return n})})}if(j.element_format=="html"){i.onPostProcess.add(function(k,l){l.content=l.content.replace(/<([^>]+) \/>/g,"<$1>")})}if(j.fix_list_elements){i.onPreProcess.add(function(v,s){var l,z,y=["ol","ul"],u,t,q,k=/^(OL|UL)$/,A;function m(r,x){var o=x.split(","),p;while((r=r.previousSibling)!=null){for(p=0;p=1767){f(i.dom.select("p table",l.node).reverse(),function(p){var o=i.dom.getParent(p.parentNode,"table,p");if(o.nodeName!="TABLE"){try{i.dom.split(o,p)}catch(m){}}})}})}},setEntities:function(o){var n=this,j,m,h={},k;if(n.entityLookup){return}j=o.split(",");for(m=0;m1){f(q[1].split("|"),function(u){var p={},t;k=k||[];u=u.replace(/::/g,"~");u=/^([!\-])?([\w*.?~_\-]+|)([=:<])?(.+)?$/.exec(u);u[2]=u[2].replace(/~/g,":");if(u[1]=="!"){r=r||[];r.push(u[2])}if(u[1]=="-"){for(t=0;t=1767)){p=j.createHTMLDocument("");f(r.nodeName=="BODY"?r.childNodes:[r],function(h){p.body.appendChild(p.importNode(h,true))});if(r.nodeName!="BODY"){r=p.body.firstChild}else{r=p.body}i=k.dom.doc;k.dom.doc=p}k.key=""+(parseInt(k.key)+1);if(!q.no_events){q.node=r;k.onPreProcess.dispatch(k,q)}k.writer.reset();k._info=q;k._serializeNode(r,q.getInner);q.content=k.writer.getContent();if(i){k.dom.doc=i}if(!q.no_events){k.onPostProcess.dispatch(k,q)}k._postProcess(q);q.node=null;return e.trim(q.content)},_postProcess:function(n){var i=this,k=i.settings,j=n.content,m=[],l;if(n.format=="html"){l=i._protect({content:j,patterns:[{pattern:/(]*>)(.*?)(<\/script>)/g},{pattern:/(]*>)(.*?)(<\/noscript>)/g},{pattern:/(]*>)(.*?)(<\/style>)/g},{pattern:/(]*>)(.*?)(<\/pre>)/g,encode:1},{pattern:/()/g}]});j=l.content;if(k.entity_encoding!=="raw"){j=i._encode(j)}if(!n.set){j=j.replace(/

    \s+<\/p>|]+)>\s+<\/p>/g,k.entity_encoding=="numeric"?" 

    ":" 

    ");if(k.remove_linebreaks){j=j.replace(/\r?\n|\r/g," ");j=j.replace(/(<[^>]+>)\s+/g,"$1 ");j=j.replace(/\s+(<\/[^>]+>)/g," $1");j=j.replace(/<(p|h[1-6]|blockquote|hr|div|table|tbody|tr|td|body|head|html|title|meta|style|pre|script|link|object) ([^>]+)>\s+/g,"<$1 $2>");j=j.replace(/<(p|h[1-6]|blockquote|hr|div|table|tbody|tr|td|body|head|html|title|meta|style|pre|script|link|object)>\s+/g,"<$1>");j=j.replace(/\s+<\/(p|h[1-6]|blockquote|hr|div|table|tbody|tr|td|body|head|html|title|meta|style|pre|script|link|object)>/g,"")}if(k.apply_source_formatting&&k.indent_mode=="simple"){j=j.replace(/<(\/?)(ul|hr|table|meta|link|tbody|tr|object|body|head|html|map)(|[^>]+)>\s*/g,"\n<$1$2$3>\n");j=j.replace(/\s*<(p|h[1-6]|blockquote|div|title|style|pre|script|td|li|area)(|[^>]+)>/g,"\n<$1$2>");j=j.replace(/<\/(p|h[1-6]|blockquote|div|title|style|pre|script|td|li)>\s*/g,"\n");j=j.replace(/\n\n/g,"\n")}}j=i._unprotect(j,l);j=j.replace(//g,"");if(k.entity_encoding=="raw"){j=j.replace(/

     <\/p>|]+)> <\/p>/g,"\u00a0

    ")}j=j.replace(/]+|)>([\s\S]*?)<\/noscript>/g,function(h,p,o){return""+i.dom.decode(o.replace(//g,""))+""})}n.content=j},_serializeNode:function(D,I){var z=this,A=z.settings,x=z.writer,q,j,u,F,E,H,B,h,y,k,r,C,p,m,G,o;if(!A.node_filter||A.node_filter(D)){switch(D.nodeType){case 1:if(D.hasAttribute?D.hasAttribute("_mce_bogus"):D.getAttribute("_mce_bogus")){return}p=G=false;q=D.hasChildNodes();k=D.getAttribute("_mce_name")||D.nodeName.toLowerCase();o=D.getAttribute("_mce_type");if(o){if(!z._info.cleanup){p=true;return}else{G=1}}if(d){if(D.scopeName!=="HTML"&&D.scopeName!=="html"){k=D.scopeName+":"+k}}if(k.indexOf("mce:")===0){k=k.substring(4)}if(!G){if(!z.validElementsRE||!z.validElementsRE.test(k)||(z.invalidElementsRE&&z.invalidElementsRE.test(k))||I){p=true;break}}if(d){if(A.fix_content_duplication){if(D._mce_serialized==z.key){return}D._mce_serialized=z.key}if(k.charAt(0)=="/"){k=k.substring(1)}}else{if(a){if(D.nodeName==="BR"&&D.getAttribute("type")=="_moz"){return}}}if(A.validate_children){if(z.elementName&&!z.schema.isValid(z.elementName,k)){p=true;break}z.elementName=k}r=z.findRule(k);if(!r){p=true;break}k=r.name||k;m=A.closed.test(k);if((!q&&r.noEmpty)||(d&&!k)){p=true;break}if(r.requiredAttribs){H=r.requiredAttribs;for(F=H.length-1;F>=0;F--){if(this.dom.getAttrib(D,H[F])!==""){break}}if(F==-1){p=true;break}}x.writeStartElement(k);if(r.attribs){for(F=0,B=r.attribs,E=B.length;F-1;F--){h=B[F];if(h.specified){H=h.nodeName.toLowerCase();if(A.invalid_attrs.test(H)||!r.validAttribsRE.test(H)){continue}C=z.findAttribRule(r,H);y=z._getAttrib(D,C,H);if(y!==null){x.writeAttribute(H,y)}}}}if(o&&G){x.writeAttribute("_mce_type",o)}if(k==="script"&&e.trim(D.innerHTML)){x.writeText("// ");x.writeCDATA(D.innerHTML.replace(/|<\[CDATA\[|\]\]>/g,""));q=false;break}if(r.padd){if(q&&(u=D.firstChild)&&u.nodeType===1&&D.childNodes.length===1){if(u.hasAttribute?u.hasAttribute("_mce_bogus"):u.getAttribute("_mce_bogus")){x.writeText("\u00a0")}}else{if(!q){x.writeText("\u00a0")}}}break;case 3:if(A.validate_children&&z.elementName&&!z.schema.isValid(z.elementName,"#text")){return}return x.writeText(D.nodeValue);case 4:return x.writeCDATA(D.nodeValue);case 8:return x.writeComment(D.nodeValue)}}else{if(D.nodeType==1){q=D.hasChildNodes()}}if(q&&!m){u=D.firstChild;while(u){z._serializeNode(u);z.elementName=k;u=u.nextSibling}}if(!p){if(!m){x.writeFullEndElement()}else{x.writeEndElement()}}},_protect:function(j){var i=this;j.items=j.items||[];function h(l){return l.replace(/[\r\n\\]/g,function(m){if(m==="\n"){return"\\n"}else{if(m==="\\"){return"\\\\"}}return"\\r"})}function k(l){return l.replace(/\\[\\rn]/g,function(m){if(m==="\\n"){return"\n"}else{if(m==="\\\\"){return"\\"}}return"\r"})}f(j.patterns,function(l){j.content=k(h(j.content).replace(l.pattern,function(n,o,m,p){m=k(m);if(l.encode){m=i._encode(m)}j.items.push(m);return o+""+p}))});return j},_unprotect:function(i,j){i=i.replace(/\"))}if(a&&j.ListBox){if(a.Button||a.SplitButton){e+=b.createHTML("td",{"class":"mceToolbarEnd"},b.createHTML("span",null,""))}}if(b.stdMode){e+='
    "}else{e+=""}if(f&&j.ListBox){if(f.Button||f.SplitButton){e+=b.createHTML("td",{"class":"mceToolbarStart"},b.createHTML("span",null,""))}}}g="mceToolbarEnd";if(j.Button){g+=" mceToolbarEndButton"}else{if(j.SplitButton){g+=" mceToolbarEndSplitButton"}else{if(j.ListBox){g+=" mceToolbarEndListBox"}}}e+=b.createHTML("td",{"class":g},b.createHTML("span",null,""));return b.createHTML("table",{id:l.id,"class":"mceToolbar"+(m["class"]?" "+m["class"]:""),cellpadding:"0",cellspacing:"0",align:l.settings.align||""},""+e+"")}});(function(b){var a=b.util.Dispatcher,c=b.each;b.create("tinymce.AddOnManager",{items:[],urls:{},lookup:{},onAdd:new a(this),get:function(d){return this.lookup[d]},requireLangPack:function(e){var d=b.settings;if(d&&d.language){b.ScriptLoader.add(this.urls[e]+"/langs/"+d.language+".js")}},add:function(e,d){this.items.push(d);this.lookup[e]=d;this.onAdd.dispatch(this,e,d);return d},load:function(h,e,d,g){var f=this;if(f.urls[h]){return}if(e.indexOf("/")!=0&&e.indexOf("://")==-1){e=b.baseURL+"/"+e}f.urls[h]=e.substring(0,e.lastIndexOf("/"));b.ScriptLoader.add(e,d,g)}});b.PluginManager=new b.AddOnManager();b.ThemeManager=new b.AddOnManager()}(tinymce));(function(j){var g=j.each,d=j.extend,k=j.DOM,i=j.dom.Event,f=j.ThemeManager,b=j.PluginManager,e=j.explode,h=j.util.Dispatcher,a,c=0;j.documentBaseURL=window.location.href.replace(/[\?#].*$/,"").replace(/[\/\\][^\/]+$/,"");if(!/[\/\\]$/.test(j.documentBaseURL)){j.documentBaseURL+="/"}j.baseURL=new j.util.URI(j.documentBaseURL).toAbsolute(j.baseURL);j.baseURI=new j.util.URI(j.baseURL);j.onBeforeUnload=new h(j);i.add(window,"beforeunload",function(l){j.onBeforeUnload.dispatch(j,l)});j.onAddEditor=new h(j);j.onRemoveEditor=new h(j);j.EditorManager=d(j,{editors:[],i18n:{},activeEditor:null,init:function(q){var n=this,p,l=j.ScriptLoader,u,o=[],m;function r(x,y,t){var v=x[y];if(!v){return}if(j.is(v,"string")){t=v.replace(/\.\w+$/,"");t=t?j.resolve(t):0;v=j.resolve(v)}return v.apply(t||this,Array.prototype.slice.call(arguments,2))}q=d({theme:"simple",language:"en"},q);n.settings=q;i.add(document,"init",function(){var s,v;r(q,"onpageload");switch(q.mode){case"exact":s=q.elements||"";if(s.length>0){g(e(s),function(x){if(k.get(x)){m=new j.Editor(x,q);o.push(m);m.render(1)}else{g(document.forms,function(y){g(y.elements,function(z){if(z.name===x){x="mce_editor_"+c++;k.setAttrib(z,"id",x);m=new j.Editor(x,q);o.push(m);m.render(1)}})})}})}break;case"textareas":case"specific_textareas":function t(y,x){return x.constructor===RegExp?x.test(y.className):k.hasClass(y,x)}g(k.select("textarea"),function(x){if(q.editor_deselector&&t(x,q.editor_deselector)){return}if(!q.editor_selector||t(x,q.editor_selector)){u=k.get(x.name);if(!x.id&&!u){x.id=x.name}if(!x.id||n.get(x.id)){x.id=k.uniqueId()}m=new j.Editor(x.id,q);o.push(m);m.render(1)}});break}if(q.oninit){s=v=0;g(o,function(x){v++;if(!x.initialized){x.onInit.add(function(){s++;if(s==v){r(q,"oninit")}})}else{s++}if(s==v){r(q,"oninit")}})}})},get:function(l){if(l===a){return this.editors}return this.editors[l]},getInstanceById:function(l){return this.get(l)},add:function(m){var l=this,n=l.editors;n[m.id]=m;n.push(m);l._setActive(m);l.onAddEditor.dispatch(l,m);if(j.adapter){j.adapter.patchEditor(m)}return m},remove:function(n){var m=this,l,o=m.editors;if(!o[n.id]){return null}delete o[n.id];for(l=0;l':"",visual_table_class:"mceItemTable",visual:1,font_size_style_values:"xx-small,x-small,small,medium,large,x-large,xx-large",apply_source_formatting:1,directionality:"ltr",forced_root_block:"p",valid_elements:"@[id|class|style|title|dir';if(F.document_base_url!=m.documentBaseURL){E.iframeHTML+=''}E.iframeHTML+='';if(m.relaxedDomain){E.iframeHTML+=''; + // Load the CSS by injecting them into the HTML this will reduce "flicker" + for (i = 0; i < t.contentCSS.length; i++) { + t.iframeHTML += ''; + } bi = s.body_id || 'tinymce'; if (bi.indexOf('=') != -1) { @@ -8743,33 +10760,35 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { bc = bc[t.id] || ''; } - t.iframeHTML += ''; + t.iframeHTML += '
    '; // Domain relaxing enabled, then set document domain - if (tinymce.relaxedDomain) { + if (tinymce.relaxedDomain && (isIE || (tinymce.isOpera && parseFloat(opera.version()) < 11))) { // We need to write the contents here in IE since multiple writes messes up refresh button and back button - if (isIE || (tinymce.isOpera && parseFloat(opera.version()) >= 9.5)) - u = 'javascript:(function(){document.open();document.domain="' + document.domain + '";var ed = window.parent.tinyMCE.get("' + t.id + '");document.write(ed.iframeHTML);document.close();ed.setupIframe();})()'; - else if (tinymce.isOpera) - u = 'javascript:(function(){document.open();document.domain="' + document.domain + '";document.close();ed.setupIframe();})()'; + u = 'javascript:(function(){document.open();document.domain="' + document.domain + '";var ed = window.parent.tinyMCE.get("' + t.id + '");document.write(ed.iframeHTML);document.close();ed.setupIframe();})()'; } // Create iframe - n = DOM.add(o.iframeContainer, 'iframe', { + // TODO: ACC add the appropriate description on this. + n = DOM.add(o.iframeContainer, 'iframe', { id : t.id + "_ifr", src : u || 'javascript:""', // Workaround for HTTPS warning in IE6/7 frameBorder : '0', + allowTransparency : "true", + title : s.aria_label, style : { width : '100%', - height : h + height : h, + display : 'block' // Important for Gecko to render the iframe correctly } }); t.contentAreaContainer = o.iframeContainer; DOM.get(o.editorContainer).style.display = t.orgDisplay; DOM.get(t.id).style.display = 'none'; + DOM.setAttrib(t.id, 'aria-hidden', true); - if (!isIE || !tinymce.relaxedDomain) + if (!tinymce.relaxedDomain || !u) t.setupIframe(); e = n = o = null; // Cleanup @@ -8783,30 +10802,21 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { d.open(); d.write(t.iframeHTML); d.close(); - } - // Design mode needs to be added here Ctrl+A will fail otherwise - if (!isIE) { - try { - if (!s.readonly) - d.designMode = 'On'; - } catch (ex) { - // Will fail on Gecko if the editor is placed in an hidden container element - // The design mode will be set ones the editor is focused - } + if (tinymce.relaxedDomain) + d.domain = tinymce.relaxedDomain; } - // IE needs to use contentEditable or it will display non secure items for HTTPS - if (isIE) { - // It will not steal focus if we hide it while setting contentEditable - b = t.getBody(); - DOM.hide(b); + // It will not steal focus while setting contentEditable + b = t.getBody(); + b.disabled = true; - if (!s.readonly) - b.contentEditable = true; + if (!s.readonly) + b.contentEditable = true; - DOM.show(b); - } + b.disabled = false; + + t.schema = new tinymce.html.Schema(s); t.dom = new tinymce.dom.DOMUtils(t.getDoc(), { keep_values : true, @@ -8816,16 +10826,85 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { class_filter : s.class_filter, update_styles : 1, fix_ie_paragraphs : 1, - valid_styles : s.valid_styles + schema : t.schema }); - t.schema = new tinymce.dom.Schema(); + t.parser = new tinymce.html.DomParser(s, t.schema); - t.serializer = new tinymce.dom.Serializer(extend(s, { - valid_elements : s.verify_html === false ? '*[*]' : s.valid_elements, - dom : t.dom, - schema : t.schema - })); + // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included. + if (!t.settings.allow_html_in_named_anchor) { + t.parser.addAttributeFilter('name', function(nodes, name) { + var i = nodes.length, sibling, prevSibling, parent, node; + + while (i--) { + node = nodes[i]; + if (node.name === 'a' && node.firstChild) { + parent = node.parent; + + // Move children after current node + sibling = node.lastChild; + do { + prevSibling = sibling.prev; + parent.insert(sibling, node); + sibling = prevSibling; + } while (sibling); + } + } + }); + } + + // Convert src and href into data-mce-src, data-mce-href and data-mce-style + t.parser.addAttributeFilter('src,href,style', function(nodes, name) { + var i = nodes.length, node, dom = t.dom, value, internalName; + + while (i--) { + node = nodes[i]; + value = node.attr(name); + internalName = 'data-mce-' + name; + + // Add internal attribute if we need to we don't on a refresh of the document + if (!node.attributes.map[internalName]) { + if (name === "style") + node.attr(internalName, dom.serializeStyle(dom.parseStyle(value), node.name)); + else + node.attr(internalName, t.convertURL(value, name, node.name)); + } + } + }); + + // Keep scripts from executing + t.parser.addNodeFilter('script', function(nodes, name) { + var i = nodes.length, node; + + while (i--) { + node = nodes[i]; + node.attr('type', 'mce-' + (node.attr('type') || 'text/javascript')); + } + }); + + t.parser.addNodeFilter('#cdata', function(nodes, name) { + var i = nodes.length, node; + + while (i--) { + node = nodes[i]; + node.type = 8; + node.name = '#comment'; + node.value = '[CDATA[' + node.value + ']]'; + } + }); + + t.parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', function(nodes, name) { + var i = nodes.length, node, nonEmptyElements = t.schema.getNonEmptyElements(); + + while (i--) { + node = nodes[i]; + + if (node.isEmpty(nonEmptyElements)) + node.empty().append(new tinymce.html.Node('br', 1)).shortEnded = true; + } + }); + + t.serializer = new tinymce.dom.Serializer(s, t.dom, t.schema); t.selection = new tinymce.dom.Selection(t.dom, t.getWin(), t.serializer); @@ -8835,18 +10914,18 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { t.formatter.register({ alignleft : [ {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'left'}}, - {selector : 'img,table', styles : {'float' : 'left'}} + {selector : 'img,table', collapsed : false, styles : {'float' : 'left'}} ], aligncenter : [ {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'center'}}, - {selector : 'img', styles : {display : 'block', marginLeft : 'auto', marginRight : 'auto'}}, - {selector : 'table', styles : {marginLeft : 'auto', marginRight : 'auto'}} + {selector : 'img', collapsed : false, styles : {display : 'block', marginLeft : 'auto', marginRight : 'auto'}}, + {selector : 'table', collapsed : false, styles : {marginLeft : 'auto', marginRight : 'auto'}} ], alignright : [ {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'right'}}, - {selector : 'img,table', styles : {'float' : 'right'}} + {selector : 'img,table', collapsed : false, styles : {'float' : 'right'}} ], alignfull : [ @@ -8854,33 +10933,47 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { ], bold : [ - {inline : 'strong'}, + {inline : 'strong', remove : 'all'}, {inline : 'span', styles : {fontWeight : 'bold'}}, - {inline : 'b'} + {inline : 'b', remove : 'all'} ], italic : [ - {inline : 'em'}, + {inline : 'em', remove : 'all'}, {inline : 'span', styles : {fontStyle : 'italic'}}, - {inline : 'i'} + {inline : 'i', remove : 'all'} ], underline : [ {inline : 'span', styles : {textDecoration : 'underline'}, exact : true}, - {inline : 'u'} + {inline : 'u', remove : 'all'} ], strikethrough : [ {inline : 'span', styles : {textDecoration : 'line-through'}, exact : true}, - {inline : 'u'} + {inline : 'strike', remove : 'all'} ], - forecolor : {inline : 'span', styles : {color : '%value'}}, - hilitecolor : {inline : 'span', styles : {backgroundColor : '%value'}}, + forecolor : {inline : 'span', styles : {color : '%value'}, wrap_links : false}, + hilitecolor : {inline : 'span', styles : {backgroundColor : '%value'}, wrap_links : false}, fontname : {inline : 'span', styles : {fontFamily : '%value'}}, fontsize : {inline : 'span', styles : {fontSize : '%value'}}, fontsize_class : {inline : 'span', attributes : {'class' : '%value'}}, blockquote : {block : 'blockquote', wrapper : 1, remove : 'all'}, + subscript : {inline : 'sub'}, + superscript : {inline : 'sup'}, + + link : {inline : 'a', selector : 'a', remove : 'all', split : true, deep : true, + onmatch : function(node) { + return true; + }, + + onformat : function(elm, fmt, vars) { + each(vars, function(value, key) { + t.dom.setAttrib(elm, key, value); + }); + } + }, removeformat : [ {selector : 'b,strong,em,i,font,u,strike', remove : 'all', split : true, expand : false, block_expand : true, deep : true}, @@ -8901,7 +10994,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Pass through t.undoManager.onAdd.add(function(um, l) { - if (!l.initial) + if (um.hasUndo()) return t.onChange.dispatch(t, l, um); }); @@ -8939,35 +11032,14 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { t.controlManager.onPostRender.dispatch(t, t.controlManager); t.onPostRender.dispatch(t); + t.quirks = new tinymce.util.Quirks(this); + if (s.directionality) t.getBody().dir = s.directionality; if (s.nowrap) t.getBody().style.whiteSpace = "nowrap"; - if (s.custom_elements) { - function handleCustom(ed, o) { - each(explode(s.custom_elements), function(v) { - var n; - - if (v.indexOf('~') === 0) { - v = v.substring(1); - n = 'span'; - } else - n = 'div'; - - o.content = o.content.replace(new RegExp('<(' + v + ')([^>]*)>', 'g'), '<' + n + ' _mce_name="$1"$2>'); - o.content = o.content.replace(new RegExp('', 'g'), ''); - }); - }; - - t.onBeforeSetContent.add(handleCustom); - t.onPostProcess.add(function(ed, o) { - if (o.set) - handleCustom(ed, o); - }); - } - if (s.handle_node_change_callback) { t.onNodeChange.add(function(ed, cm, n) { t.execCallback('handle_node_change_callback', t.id, n, -1, -1, true, t.selection.isCollapsed()); @@ -8989,16 +11061,22 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }); } - if (s.convert_newlines_to_brs) { + if (s.protect) { t.onBeforeSetContent.add(function(ed, o) { - if (o.initial) - o.content = o.content.replace(/\r?\n/g, '
    '); + if (s.protect) { + each(s.protect, function(pattern) { + o.content = o.content.replace(pattern, function(str) { + return ''; + }); + }); + } }); } - if (s.fix_nesting && isIE) { + if (s.convert_newlines_to_brs) { t.onBeforeSetContent.add(function(ed, o) { - o.content = t._fixNesting(o.content); + if (o.initial) + o.content = o.content.replace(/\r?\n/g, '
    '); }); } @@ -9095,7 +11173,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { var pn = n.parentNode; if (ed.dom.isBlock(pn) && pn.lastChild === n) - ed.dom.add(pn, 'br', {'_mce_bogus' : 1}); + ed.dom.add(pn, 'br', {'data-mce-bogus' : 1}); }); }; @@ -9105,72 +11183,62 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }); t.onSetContent.add(t.selection.onSetContent.add(fixLinks)); - - if (!s.readonly) { - try { - // Design mode must be set here once again to fix a bug where - // Ctrl+A/Delete/Backspace didn't work if the editor was added using mceAddControl then removed then added again - d.designMode = 'Off'; - d.designMode = 'On'; - } catch (ex) { - // Will fail on Gecko if the editor is placed in an hidden container element - // The design mode will be set ones the editor is focused - } - } } - // A small timeout was needed since firefox will remove. Bug: #1838304 - setTimeout(function () { - if (t.removed) - return; + t.load({initial : true, format : 'html'}); + t.startContent = t.getContent({format : 'raw'}); + t.undoManager.add(); + t.initialized = true; - t.load({initial : true, format : (s.cleanup_on_startup ? 'html' : 'raw')}); - t.startContent = t.getContent({format : 'raw'}); - t.initialized = true; + t.onInit.dispatch(t); + t.execCallback('setupcontent_callback', t.id, t.getBody(), t.getDoc()); + t.execCallback('init_instance_callback', t); + t.focus(true); + t.nodeChanged({initial : 1}); - t.onInit.dispatch(t); - t.execCallback('setupcontent_callback', t.id, t.getBody(), t.getDoc()); - t.execCallback('init_instance_callback', t); - t.focus(true); - t.nodeChanged({initial : 1}); + // Load specified content CSS last + each(t.contentCSS, function(u) { + t.dom.loadCSS(u); + }); - // Load specified content CSS last - if (s.content_css) { - tinymce.each(explode(s.content_css), function(u) { - t.dom.loadCSS(t.documentBaseURI.toAbsolute(u)); - }); - } + // Handle auto focus + if (s.auto_focus) { + setTimeout(function () { + var ed = tinymce.get(s.auto_focus); - // Handle auto focus - if (s.auto_focus) { - setTimeout(function () { - var ed = tinymce.get(s.auto_focus); + ed.selection.select(ed.getBody(), 1); + ed.selection.collapse(1); + ed.getBody().focus(); + ed.getWin().focus(); + }, 100); + } - ed.selection.select(ed.getBody(), 1); - ed.selection.collapse(1); - ed.getWin().focus(); - }, 100); - } - }, 1); - e = null; }, focus : function(sf) { - var oed, t = this, ce = t.settings.content_editable, ieRng, controlElm, doc = t.getDoc(); + var oed, t = this, selection = t.selection, ce = t.settings.content_editable, ieRng, controlElm, doc = t.getDoc(); if (!sf) { // Get selected control element - ieRng = t.selection.getRng(); + ieRng = selection.getRng(); if (ieRng.item) { controlElm = ieRng.item(0); } + t._refreshContentEditable(); + selection.normalize(); + // Is not content editable if (!ce) t.getWin().focus(); + // Focus the body as well since it's contentEditable + if (tinymce.isGecko) { + t.getBody().focus(); + } + // Restore selected control element // This is needed when for example an image is selected within a // layer a call to focus will then remove the control selection @@ -9255,7 +11323,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, nodeChanged : function(o) { - var t = this, s = t.selection, n = (isIE ? s.getNode() : s.getStart()) || t.getBody(); + var t = this, s = t.selection, n = s.getStart() || t.getBody(); // Fix for bug #1896577 it seems that this can not be fired while the editor is loading if (t.initialized) { @@ -9288,16 +11356,16 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { t.buttons[n] = s; }, - addCommand : function(n, f, s) { - this.execCommands[n] = {func : f, scope : s || this}; + addCommand : function(name, callback, scope) { + this.execCommands[name] = {func : callback, scope : scope || this}; }, - addQueryStateHandler : function(n, f, s) { - this.queryStateCommands[n] = {func : f, scope : s || this}; + addQueryStateHandler : function(name, callback, scope) { + this.queryStateCommands[name] = {func : callback, scope : scope || this}; }, - addQueryValueHandler : function(n, f, s) { - this.queryValueCommands[n] = {func : f, scope : s || this}; + addQueryValueHandler : function(name, callback, scope) { + this.queryValueCommands[name] = {func : callback, scope : scope || this}; }, addShortcut : function(pa, desc, cmd_func, sc) { @@ -9400,12 +11468,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { return true; } - // Execute global commands - if (tinymce.GlobalCommands.execCommand(t, cmd, ui, val)) { - t.onExecCommand.dispatch(t, cmd, ui, val, a); - return true; - } - // Editor commands if (t.editorCommands.execCommand(cmd, ui, val)) { t.onExecCommand.dispatch(t, cmd, ui, val, a); @@ -9537,7 +11599,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Add undo level will trigger onchange event if (!o.no_events) { - t.undoManager.typing = 0; + t.undoManager.typing = false; t.undoManager.add(); } @@ -9569,66 +11631,87 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { return h; }, - setContent : function(h, o) { - var t = this; + setContent : function(content, args) { + var self = this, rootNode, body = self.getBody(), forcedRootBlockName; - o = o || {}; - o.format = o.format || 'html'; - o.set = true; - o.content = h; + // Setup args object + args = args || {}; + args.format = args.format || 'html'; + args.set = true; + args.content = content; - if (!o.no_events) - t.onBeforeSetContent.dispatch(t, o); + // Do preprocessing + if (!args.no_events) + self.onBeforeSetContent.dispatch(self, args); + + content = args.content; // Padd empty content in Gecko and Safari. Commands will otherwise fail on the content // It will also be impossible to place the caret in the editor unless there is a BR element present - if (!tinymce.isIE && (h.length === 0 || /^\s+$/.test(h))) { - o.content = t.dom.setHTML(t.getBody(), '
    '); - o.format = 'raw'; - } + if (!tinymce.isIE && (content.length === 0 || /^\s+$/.test(content))) { + forcedRootBlockName = self.settings.forced_root_block; + if (forcedRootBlockName) + content = '<' + forcedRootBlockName + '>
    '; + else + content = '
    '; - o.content = t.dom.setHTML(t.getBody(), tinymce.trim(o.content)); + body.innerHTML = content; + self.selection.select(body, true); + self.selection.collapse(true); + return; + } - if (o.format != 'raw' && t.settings.cleanup) { - o.getInner = true; - o.content = t.dom.setHTML(t.getBody(), t.serializer.serialize(t.getBody(), o)); + // Parse and serialize the html + if (args.format !== 'raw') { + content = new tinymce.html.Serializer({}, self.schema).serialize( + self.parser.parse(content) + ); } - if (!o.no_events) - t.onSetContent.dispatch(t, o); + // Set the new cleaned contents to the editor + args.content = tinymce.trim(content); + self.dom.setHTML(body, args.content); + + // Do post processing + if (!args.no_events) + self.onSetContent.dispatch(self, args); - return o.content; + self.selection.normalize(); + + return args.content; }, - getContent : function(o) { - var t = this, h; + getContent : function(args) { + var self = this, content; - o = o || {}; - o.format = o.format || 'html'; - o.get = true; + // Setup args object + args = args || {}; + args.format = args.format || 'html'; + args.get = true; - if (!o.no_events) - t.onBeforeGetContent.dispatch(t, o); + // Do preprocessing + if (!args.no_events) + self.onBeforeGetContent.dispatch(self, args); - if (o.format != 'raw' && t.settings.cleanup) { - o.getInner = true; - h = t.serializer.serialize(t.getBody(), o); - } else - h = t.getBody().innerHTML; + // Get raw contents or by default the cleaned contents + if (args.format == 'raw') + content = self.getBody().innerHTML; + else + content = self.serializer.serialize(self.getBody(), args); - h = h.replace(/^\s*|\s*$/g, ''); - o.content = h; + args.content = tinymce.trim(content); - if (!o.no_events) - t.onGetContent.dispatch(t, o); + // Do post processing + if (!args.no_events) + self.onGetContent.dispatch(self, args); - return o.content; + return args.content; }, isDirty : function() { - var t = this; + var self = this; - return tinymce.trim(t.startContent) != tinymce.trim(t.getContent({format : 'raw', no_events : 1})) && !t.isNotDirty; + return tinymce.trim(self.startContent) != tinymce.trim(self.getContent({format : 'raw', no_events : 1})) && !self.isNotDirty; }, getContainer : function() { @@ -9806,7 +11889,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { _addEvents : function() { // 'focus', 'blur', 'dblclick', 'beforedeactivate', submit, reset - var t = this, i, s = t.settings, lo = { + var t = this, i, s = t.settings, dom = t.dom, lo = { mouseup : 'onMouseUp', mousedown : 'onMouseDown', click : 'onClick', @@ -9838,35 +11921,26 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { each(lo, function(v, k) { switch (k) { case 'contextmenu': - if (tinymce.isOpera) { - // Fake contextmenu on Opera - t.dom.bind(t.getBody(), 'mousedown', function(e) { - if (e.ctrlKey) { - e.fakeType = 'contextmenu'; - eventHandler(e); - } - }); - } else - t.dom.bind(t.getBody(), k, eventHandler); + dom.bind(t.getDoc(), k, eventHandler); break; case 'paste': - t.dom.bind(t.getBody(), k, function(e) { + dom.bind(t.getBody(), k, function(e) { eventHandler(e); }); break; case 'submit': case 'reset': - t.dom.bind(t.getElement().form || DOM.getParent(t.id, 'form'), k, eventHandler); + dom.bind(t.getElement().form || DOM.getParent(t.id, 'form'), k, eventHandler); break; default: - t.dom.bind(s.content_editable ? t.getBody() : t.getDoc(), k, eventHandler); + dom.bind(s.content_editable ? t.getBody() : t.getDoc(), k, eventHandler); } }); - t.dom.bind(s.content_editable ? t.getBody() : (isGecko ? t.getDoc() : t.getWin()), 'focus', function(e) { + dom.bind(s.content_editable ? t.getBody() : (isGecko ? t.getDoc() : t.getWin()), 'focus', function(e) { t.focus(true); }); @@ -9874,22 +11948,12 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Fixes bug where a specified document_base_uri could result in broken images // This will also fix drag drop of images in Gecko if (tinymce.isGecko) { - // Convert all images to absolute URLs -/* t.onSetContent.add(function(ed, o) { - each(ed.dom.select('img'), function(e) { - var v; - - if (v = e.getAttribute('_mce_src')) - e.src = t.documentBaseURI.toAbsolute(v); - }) - });*/ - - t.dom.bind(t.getDoc(), 'DOMNodeInserted', function(e) { + dom.bind(t.getDoc(), 'DOMNodeInserted', function(e) { var v; e = e.target; - if (e.nodeType === 1 && e.nodeName === 'IMG' && (v = e.getAttribute('_mce_src'))) + if (e.nodeType === 1 && e.nodeName === 'IMG' && (v = e.getAttribute('data-mce-src'))) e.src = t.documentBaseURI.toAbsolute(v); }); } @@ -9900,14 +11964,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { var t = this, d = t.getDoc(), s = t.settings; if (isGecko && !s.readonly) { - if (t._isHidden()) { - try { - if (!s.content_editable) - d.designMode = 'On'; - } catch (ex) { - // Fails if it's hidden - } - } + t._refreshContentEditable(); try { // Try new Gecko method @@ -9930,19 +11987,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { t.onMouseDown.add(setOpts); } - // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250 - // WebKit can't even do simple things like selecting an image - // This also fixes so it's possible to select mceItemAnchors - if (tinymce.isWebKit) { - t.onClick.add(function(ed, e) { - e = e.target; - - // Needs tobe the setBaseAndExtend or it will fail to select floated images - if (e.nodeName == 'IMG' || (e.nodeName == 'A' && t.dom.hasClass(e, 'mceItemAnchor'))) - t.selection.getSel().setBaseAndExtent(e, 0, e, 1); - }); - } - // Add node change handlers t.onMouseUp.add(t.nodeChanged); //t.onClick.add(t.nodeChanged); @@ -9953,6 +11997,35 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { t.nodeChanged(); }); + + // Add block quote deletion handler + t.onKeyDown.add(function(ed, e) { + // Was the BACKSPACE key pressed? + if (e.keyCode != 8) + return; + + var n = ed.selection.getRng().startContainer; + var offset = ed.selection.getRng().startOffset; + + while (n && n.nodeType && n.nodeType != 1 && n.parentNode) + n = n.parentNode; + + // Is the cursor at the beginning of a blockquote? + if (n && n.parentNode && n.parentNode.tagName === 'BLOCKQUOTE' && n.parentNode.firstChild == n && offset == 0) { + // Remove the blockquote + ed.formatter.toggle('blockquote', null, n.parentNode); + + // Move the caret to the beginning of n + var rng = ed.selection.getRng(); + rng.setStart(n, 0); + rng.setEnd(n, 0); + ed.selection.setRng(rng); + ed.selection.collapse(false); + } + }); + + + // Add reset handler t.onReset.add(function() { t.setContent(t.startContent, {format : 'raw'}); @@ -9974,9 +12047,9 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { for (i=1; i<=6; i++) t.addShortcut('ctrl+' + i, '', ['FormatBlock', false, 'h' + i]); - t.addShortcut('ctrl+7', '', ['FormatBlock', false, '

    ']); - t.addShortcut('ctrl+8', '', ['FormatBlock', false, '

    ']); - t.addShortcut('ctrl+9', '', ['FormatBlock', false, '
    ']); + t.addShortcut('ctrl+7', '', ['FormatBlock', false, 'p']); + t.addShortcut('ctrl+8', '', ['FormatBlock', false, 'div']); + t.addShortcut('ctrl+9', '', ['FormatBlock', false, 'address']); function find(e) { var v = null; @@ -10032,7 +12105,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (tinymce.isIE) { // Fix so resize will only update the width and height attributes not the styles of an image // It will also block mceItemNoResize items - t.dom.bind(t.getDoc(), 'controlselect', function(e) { + dom.bind(t.getDoc(), 'controlselect', function(e) { var re = t.resizeInfo, cb; e = e.target; @@ -10042,28 +12115,28 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { return; if (re) - t.dom.unbind(re.node, re.ev, re.cb); + dom.unbind(re.node, re.ev, re.cb); - if (!t.dom.hasClass(e, 'mceItemNoResize')) { + if (!dom.hasClass(e, 'mceItemNoResize')) { ev = 'resizeend'; - cb = t.dom.bind(e, ev, function(e) { + cb = dom.bind(e, ev, function(e) { var v; e = e.target; - if (v = t.dom.getStyle(e, 'width')) { - t.dom.setAttrib(e, 'width', v.replace(/[^0-9%]+/g, '')); - t.dom.setStyle(e, 'width', ''); + if (v = dom.getStyle(e, 'width')) { + dom.setAttrib(e, 'width', v.replace(/[^0-9%]+/g, '')); + dom.setStyle(e, 'width', ''); } - if (v = t.dom.getStyle(e, 'height')) { - t.dom.setAttrib(e, 'height', v.replace(/[^0-9%]+/g, '')); - t.dom.setStyle(e, 'height', ''); + if (v = dom.getStyle(e, 'height')) { + dom.setAttrib(e, 'height', v.replace(/[^0-9%]+/g, '')); + dom.setStyle(e, 'height', ''); } }); } else { ev = 'resizestart'; - cb = t.dom.bind(e, 'resizestart', Event.cancel, Event); + cb = dom.bind(e, 'resizestart', Event.cancel, Event); } re = t.resizeInfo = { @@ -10072,27 +12145,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { cb : cb }; }); - - t.onKeyDown.add(function(ed, e) { - switch (e.keyCode) { - case 8: - // Fix IE control + backspace browser bug - if (t.selection.getRng().item) { - ed.dom.remove(t.selection.getRng().item(0)); - return Event.cancel(e); - } - } - }); - - /*if (t.dom.boxModel) { - t.getBody().style.height = '100%'; - - Event.add(t.getWin(), 'resize', function(e) { - var docElm = t.getDoc().documentElement; - - docElm.style.height = (docElm.offsetHeight - 10) + 'px'; - }); - }*/ } if (tinymce.isOpera) { @@ -10104,81 +12156,61 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Add custom undo/redo handlers if (s.custom_undo_redo) { function addUndo() { - t.undoManager.typing = 0; + t.undoManager.typing = false; t.undoManager.add(); }; - t.dom.bind(t.getDoc(), 'focusout', function(e) { + dom.bind(t.getDoc(), 'focusout', function(e) { if (!t.removed && t.undoManager.typing) addUndo(); }); + // Add undo level when contents is drag/dropped within the editor + t.dom.bind(t.dom.getRoot(), 'dragend', function(e) { + addUndo(); + }); + t.onKeyUp.add(function(ed, e) { - if ((e.keyCode >= 33 && e.keyCode <= 36) || (e.keyCode >= 37 && e.keyCode <= 40) || e.keyCode == 13 || e.keyCode == 45 || e.ctrlKey) + var keyCode = e.keyCode; + + if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 13 || keyCode == 45 || e.ctrlKey) addUndo(); }); t.onKeyDown.add(function(ed, e) { - var rng, tmpRng, parent, offset; - - // IE has a really odd bug where the DOM might include an node that doesn't have - // a proper structure. If you try to access nodeValue it would throw an illegal value exception. - // This seems to only happen when you delete contents and it seems to be avoidable if you refresh the element - // after you delete contents from it. See: #3008923 - if (isIE && e.keyCode == 46) { - rng = t.selection.getRng(); - - if (rng.parentElement) { - parent = rng.parentElement(); - - // Get the current caret position within the element - tmpRng = rng.duplicate(); - tmpRng.moveToElementText(parent); - tmpRng.setEndPoint('EndToEnd', rng); - offset = tmpRng.text.length; - - // Select next word when ctrl key is used in combo with delete - if (e.ctrlKey) { - rng.moveEnd('word', 1); - rng.select(); - } + var keyCode = e.keyCode, sel; - // Delete contents - t.selection.getSel().clear(); + if (keyCode == 8) { + sel = t.getDoc().selection; - // Check if we are within the same parent - if (rng.parentElement() == parent) { - try { - // Update the HTML and hopefully it will remove the artifacts - parent.innerHTML = parent.innerHTML; - } catch (ex) { - // And since it's IE it can sometimes produce an unknown runtime error - } - - // Restore the caret position - tmpRng.moveToElementText(parent); - tmpRng.collapse(); - tmpRng.move('character', offset); - tmpRng.select(); - } + // Fix IE control + backspace browser bug + if (sel && sel.createRange && sel.createRange().item) { + t.undoManager.beforeChange(); + ed.dom.remove(sel.createRange().item(0)); + addUndo(); - // Block the default delete behavior since it might be broken - e.preventDefault(); - return; + return Event.cancel(e); } } - // Is caracter positon keys - if ((e.keyCode >= 33 && e.keyCode <= 36) || (e.keyCode >= 37 && e.keyCode <= 40) || e.keyCode == 13 || e.keyCode == 45) { + // Is caracter positon keys left,right,up,down,home,end,pgdown,pgup,enter + if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 13 || keyCode == 45) { + // Add position before enter key is pressed, used by IE since it still uses the default browser behavior + // Todo: Remove this once we normalize enter behavior on IE + if (tinymce.isIE && keyCode == 13) + t.undoManager.beforeChange(); + if (t.undoManager.typing) addUndo(); return; } - if (!t.undoManager.typing) { + // If key isn't shift,ctrl,alt,capslock,metakey + if ((keyCode < 16 || keyCode > 20) && keyCode != 224 && keyCode != 91 && !t.undoManager.typing) { + t.undoManager.beforeChange(); + t.undoManager.typing = true; t.undoManager.add(); - t.undoManager.typing = 1; } }); @@ -10187,68 +12219,83 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { addUndo(); }); } - }, - _isHidden : function() { - var s; + // Bug fix for FireFox keeping styles from end of selection instead of start. + if (tinymce.isGecko) { + function getAttributeApplyFunction() { + var template = t.dom.getAttribs(t.selection.getStart().cloneNode(false)); - if (!isGecko) - return 0; + return function() { + var target = t.selection.getStart(); - // Weird, wheres that cursor selection? - s = this.selection.getSel(); - return (!s || !s.rangeCount || s.rangeCount == 0); - }, + if (target !== t.getBody()) { + t.dom.setAttrib(target, "style", null); - // Fix for bug #1867292 - _fixNesting : function(s) { - var d = [], i; + each(template, function(attr) { + target.setAttributeNode(attr.cloneNode(true)); + }); + } + }; + } - s = s.replace(/<(\/)?([^\s>]+)[^>]*?>/g, function(a, b, c) { - var e; + function isSelectionAcrossElements() { + var s = t.selection; - // Handle end element - if (b === '/') { - if (!d.length) - return ''; + return !s.isCollapsed() && s.getStart() != s.getEnd(); + } - if (c !== d[d.length - 1].tag) { - for (i=d.length - 1; i>=0; i--) { - if (d[i].tag === c) { - d[i].close = 1; - break; - } - } + t.onKeyPress.add(function(ed, e) { + var applyAttributes; - return ''; - } else { - d.pop(); + if ((e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) { + applyAttributes = getAttributeApplyFunction(); + t.getDoc().execCommand('delete', false, null); + applyAttributes(); - if (d.length && d[d.length - 1].close) { - a = a + ''; - d.pop(); - } + return Event.cancel(e); } - } else { - // Ignore these - if (/^(br|hr|input|meta|img|link|param)$/i.test(c)) - return a; + }); - // Ignore closed ones - if (/\/>$/.test(a)) - return a; + t.dom.bind(t.getDoc(), 'cut', function(e) { + var applyAttributes; - d.push({tag : c}); // Push start element - } + if (isSelectionAcrossElements()) { + applyAttributes = getAttributeApplyFunction(); + t.onKeyUp.addToTop(Event.cancel, Event); - return a; - }); + setTimeout(function() { + applyAttributes(); + t.onKeyUp.remove(Event.cancel, Event); + }, 0); + } + }); + } + }, - // End all open tags - for (i=d.length - 1; i>=0; i--) - s += ''; + _refreshContentEditable : function() { + var self = this, body, parent; - return s; + // Check if the editor was hidden and the re-initalize contentEditable mode by removing and adding the body again + if (self._isHidden()) { + body = self.getBody(); + parent = body.parentNode; + + parent.removeChild(body); + parent.appendChild(body); + + body.focus(); + } + }, + + _isHidden : function() { + var s; + + if (!isGecko) + return 0; + + // Weird, wheres that cursor selection? + s = this.selection.getSel(); + return (!s || !s.rangeCount || s.rangeCount == 0); } }); })(tinymce); @@ -10262,6 +12309,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { selection = editor.selection, commands = {state: {}, exec : {}, value : {}}, settings = editor.settings, + formatter = editor.formatter, bookmark; function execCommand(command, ui, value) { @@ -10327,11 +12375,11 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }; function isFormatMatch(name) { - return editor.formatter.match(name); + return formatter.match(name); }; function toggleFormat(name, value) { - editor.formatter.toggle(name, value ? {value : value} : undefined); + formatter.toggle(name, value ? {value : value} : undefined); }; function storeSelection(type) { @@ -10391,10 +12439,11 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Remove all other alignments first each('left,center,right,full'.split(','), function(name) { if (align != name) - editor.formatter.remove('align' + name); + formatter.remove('align' + name); }); toggleFormat('align' + align); + execCommand('mceRepaint'); }, // Override list commands to fix WebKit bug @@ -10420,7 +12469,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, // Override commands to use the text formatter engine - 'Bold,Italic,Underline,Strikethrough' : function(command) { + 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) { toggleFormat(command); }, @@ -10447,7 +12496,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, RemoveFormat : function(command) { - editor.formatter.remove(command); + formatter.remove(command); }, mceBlockQuote : function(command) { @@ -10455,7 +12504,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, FormatBlock : function(command, ui, value) { - return toggleFormat(value); + return toggleFormat(value || 'p'); }, mceCleanup : function() { @@ -10475,30 +12524,156 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { editor.dom.remove(node, TRUE); restoreSelection(); } - }, + }, + + mceSelectNodeDepth : function(command, ui, value) { + var counter = 0; + + dom.getParent(selection.getNode(), function(node) { + if (node.nodeType == 1 && counter++ == value) { + selection.select(node); + return FALSE; + } + }, editor.getBody()); + }, + + mceSelectNode : function(command, ui, value) { + selection.select(value); + }, + + mceInsertContent : function(command, ui, value) { + var parser, serializer, parentNode, rootNode, fragment, args, + marker, nodeRect, viewPortRect, rng, node, node2, bookmarkHtml, viewportBodyElement; + + // Setup parser and serializer + parser = editor.parser; + serializer = new tinymce.html.Serializer({}, editor.schema); + bookmarkHtml = '\uFEFF'; + + // Run beforeSetContent handlers on the HTML to be inserted + args = {content: value, format: 'html'}; + selection.onBeforeSetContent.dispatch(selection, args); + value = args.content; + + // Add caret at end of contents if it's missing + if (value.indexOf('{$caret}') == -1) + value += '{$caret}'; + + // Replace the caret marker with a span bookmark element + value = value.replace(/\{\$caret\}/, bookmarkHtml); + + // Insert node maker where we will insert the new HTML and get it's parent + if (!selection.isCollapsed()) + editor.getDoc().execCommand('Delete', false, null); + + parentNode = selection.getNode(); + + // Parse the fragment within the context of the parent node + args = {context : parentNode.nodeName.toLowerCase()}; + fragment = parser.parse(value, args); + + // Move the caret to a more suitable location + node = fragment.lastChild; + if (node.attr('id') == 'mce_marker') { + marker = node; + + for (node = node.prev; node; node = node.walk(true)) { + if (node.type == 3 || !dom.isBlock(node.name)) { + node.parent.insert(marker, node, node.name === 'br'); + break; + } + } + } + + // If parser says valid we can insert the contents into that parent + if (!args.invalid) { + value = serializer.serialize(fragment); + + // Check if parent is empty or only has one BR element then set the innerHTML of that parent + node = parentNode.firstChild; + node2 = parentNode.lastChild; + if (!node || (node === node2 && node.nodeName === 'BR')) + dom.setHTML(parentNode, value); + else + selection.setContent(value); + } else { + // If the fragment was invalid within that context then we need + // to parse and process the parent it's inserted into + + // Insert bookmark node and get the parent + selection.setContent(bookmarkHtml); + parentNode = editor.selection.getNode(); + rootNode = editor.getBody(); + + // Opera will return the document node when selection is in root + if (parentNode.nodeType == 9) + parentNode = node = rootNode; + else + node = parentNode; + + // Find the ancestor just before the root element + while (node !== rootNode) { + parentNode = node; + node = node.parentNode; + } + + // Get the outer/inner HTML depending on if we are in the root and parser and serialize that + value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode); + value = serializer.serialize( + parser.parse( + // Need to replace by using a function since $ in the contents would otherwise be a problem + value.replace(//i, function() { + return serializer.serialize(fragment); + }) + ) + ); + + // Set the inner/outer HTML depending on if we are in the root or not + if (parentNode == rootNode) + dom.setHTML(rootNode, value); + else + dom.setOuterHTML(parentNode, value); + } - mceSelectNodeDepth : function(command, ui, value) { - var counter = 0; + marker = dom.get('mce_marker'); - dom.getParent(selection.getNode(), function(node) { - if (node.nodeType == 1 && counter++ == value) { - selection.select(node); - return FALSE; - } - }, editor.getBody()); - }, + // Scroll range into view scrollIntoView on element can't be used since it will scroll the main view port as well + nodeRect = dom.getRect(marker); + viewPortRect = dom.getViewPort(editor.getWin()); - mceSelectNode : function(command, ui, value) { - selection.select(value); - }, + // Check if node is out side the viewport if it is then scroll to it + if ((nodeRect.y + nodeRect.h > viewPortRect.y + viewPortRect.h || nodeRect.y < viewPortRect.y) || + (nodeRect.x > viewPortRect.x + viewPortRect.w || nodeRect.x < viewPortRect.x)) { + viewportBodyElement = tinymce.isIE ? editor.getDoc().documentElement : editor.getBody(); + viewportBodyElement.scrollLeft = nodeRect.x; + viewportBodyElement.scrollTop = nodeRect.y - viewPortRect.h + 25; + } - mceInsertContent : function(command, ui, value) { - selection.setContent(value); + // Move selection before marker and remove it + rng = dom.createRng(); + + // If previous sibling is a text node set the selection to the end of that node + node = marker.previousSibling; + if (node && node.nodeType == 3) { + rng.setStart(node, node.nodeValue.length); + } else { + // If the previous sibling isn't a text node or doesn't exist set the selection before the marker node + rng.setStartBefore(marker); + rng.setEndBefore(marker); + } + + // Remove the marker node and set the new range + dom.remove(marker); + selection.setRng(rng); + + // Dispatch after event and add any visual elements needed + selection.onSetContent.dispatch(selection, args); + editor.addVisual(); }, mceInsertRawHTML : function(command, ui, value) { selection.setContent('tiny_mce_marker'); - editor.setContent(editor.getContent().replace(/tiny_mce_marker/g, value)); + editor.setContent(editor.getContent().replace(/tiny_mce_marker/g, function() { return value })); }, mceSetContent : function(command, ui, value) { @@ -10544,11 +12719,11 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, mceToggleFormat : function(command, ui, value) { - editor.formatter.toggle(value); + formatter.toggle(value); }, InsertHorizontalRule : function() { - selection.setContent('
    '); + editor.execCommand('mceInsertContent', false, '
    '); }, mceToggleVisualAid : function() { @@ -10557,33 +12732,37 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, mceReplaceContent : function(command, ui, value) { - selection.setContent(value.replace(/\{\$selection\}/g, selection.getContent({format : 'text'}))); + editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({format : 'text'}))); }, mceInsertLink : function(command, ui, value) { - var link = dom.getParent(selection.getNode(), 'a'); + var anchor; - if (tinymce.is(value, 'string')) + if (typeof(value) == 'string') value = {href : value}; - if (!link) { - execNativeCommand('CreateLink', FALSE, 'javascript:mctmp(0);'); - each(dom.select('a[href=javascript:mctmp(0);]'), function(link) { - dom.setAttribs(link, value); - }); - } else { - if (value.href) - dom.setAttribs(link, value); - else - editor.dom.remove(link, TRUE); + anchor = dom.getParent(selection.getNode(), 'a'); + + // Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here. + value.href = value.href.replace(' ', '%20'); + + // Remove existing links if there could be child links or that the href isn't specified + if (!anchor || !value.href) { + formatter.remove('link'); + } + + // Apply new link to selection + if (value.href) { + formatter.apply('link', value, anchor); } }, - + selectAll : function() { - var root = dom.getRoot(); - var rng = dom.createRng(); + var root = dom.getRoot(), rng = dom.createRng(); + rng.setStart(root, 0); rng.setEnd(root, root.childNodes.length); + editor.selection.setRng(rng); } }); @@ -10595,7 +12774,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { return isFormatMatch('align' + command.substring(7)); }, - 'Bold,Italic,Underline,Strikethrough' : function(command) { + 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) { return isFormatMatch(command); }, @@ -10652,23 +12831,30 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } }; })(tinymce); + (function(tinymce) { var Dispatcher = tinymce.util.Dispatcher; tinymce.UndoManager = function(editor) { - var self, index = 0, data = []; + var self, index = 0, data = [], beforeBookmark; function getContent() { return tinymce.trim(editor.getContent({format : 'raw', no_events : 1})); }; return self = { - typing : 0, + typing : false, onAdd : new Dispatcher(self), + onUndo : new Dispatcher(self), + onRedo : new Dispatcher(self), + beforeChange : function() { + beforeBookmark = editor.selection.getBookmark(2, true); + }, + add : function(level) { var i, settings = editor.settings, lastLevel; @@ -10677,10 +12863,12 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Add undo level if needed lastLevel = data[index]; - if (lastLevel && lastLevel.content == level.content) { - if (index > 0 || data.length == 1) - return null; - } + if (lastLevel && lastLevel.content == level.content) + return null; + + // Set before bookmark on previous level + if (data[index]) + data[index].beforeBookmark = beforeBookmark; // Time to compress if (settings.custom_undo_redo_levels) { @@ -10697,13 +12885,8 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { level.bookmark = editor.selection.getBookmark(2, true); // Crop array if needed - if (index < data.length - 1) { - // Treat first level as initial - if (index == 0) - data = []; - else - data.length = index + 1; - } + if (index < data.length - 1) + data.length = index + 1; data.push(level); index = data.length - 1; @@ -10719,14 +12902,14 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (self.typing) { self.add(); - self.typing = 0; + self.typing = false; } if (index > 0) { level = data[--index]; editor.setContent(level.content, {format : 'raw'}); - editor.selection.moveToBookmark(level.bookmark); + editor.selection.moveToBookmark(level.beforeBookmark); self.onUndo.dispatch(self, level); } @@ -10751,15 +12934,16 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { clear : function() { data = []; - index = self.typing = 0; + index = 0; + self.typing = false; }, hasUndo : function() { - return index > 0 || self.typing; + return index > 0 || this.typing; }, hasRedo : function() { - return index < data.length - 1; + return index < data.length - 1 && !this.typing; } }; }; @@ -10808,24 +12992,15 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { return rng2.cloneContents().textContent.length == 0; }; - function isEmpty(n) { - n = n.innerHTML; - - n = n.replace(/<(img|hr|table|input|select|textarea)[ \>]/gi, '-'); // Keep these convert them to - chars - n = n.replace(/<[^>]+>/g, ''); // Remove all tags - - return n.replace(/[ \u00a0\t\r\n]+/g, '') == ''; - }; - function splitList(selection, dom, li) { var listBlock, block; - if (isEmpty(li)) { + if (dom.isEmpty(li)) { listBlock = dom.getParent(li, 'ul,ol'); if (!dom.getParent(listBlock.parentNode, 'ul,ol')) { dom.split(listBlock, li); - block = dom.create('p', 0, '
    '); + block = dom.create('p', 0, '
    '); dom.replace(block, li); selection.select(block, 1); } @@ -10846,45 +13021,94 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { s.element = elm.toUpperCase(); ed.onPreInit.add(t.setup, t); + }, - t.reOpera = new RegExp('(\\u00a0| | )<\/' + elm + '>', 'gi'); - t.rePadd = new RegExp(']+)><\\\/p>|]+)\\\/>|]+)>\\s+<\\\/p>|

    <\\\/p>||

    \\s+<\\\/p>'.replace(/p/g, elm), 'gi'); - t.reNbsp2BR1 = new RegExp(']+)>[\\s\\u00a0]+<\\\/p>|

    [\\s\\u00a0]+<\\\/p>'.replace(/p/g, elm), 'gi'); - t.reNbsp2BR2 = new RegExp('<%p()([^>]+)>( | )<\\\/%p>|<%p>( | )<\\\/%p>'.replace(/%p/g, elm), 'gi'); - t.reBR2Nbsp = new RegExp(']+)>\\s*
    \\s*<\\\/p>|

    \\s*
    \\s*<\\\/p>'.replace(/p/g, elm), 'gi'); + setup : function() { + var t = this, ed = t.editor, s = ed.settings, dom = ed.dom, selection = ed.selection, blockElements = ed.schema.getBlockElements(); - function padd(ed, o) { - if (isOpera) - o.content = o.content.replace(t.reOpera, ''); + // Force root blocks + if (s.forced_root_block) { + function addRootBlocks() { + var node = selection.getStart(), rootNode = ed.getBody(), rng, startContainer, startOffset, endContainer, endOffset, rootBlockNode, tempNode, offset = -0xFFFFFF; - o.content = o.content.replace(t.rePadd, '<' + elm + '$1$2$3$4$5$6>\u00a0'); + if (!node || node.nodeType !== 1) + return; - if (!isIE && !isOpera && o.set) { - // Use   instead of BR in padded paragraphs - o.content = o.content.replace(t.reNbsp2BR1, '<' + elm + '$1$2>
    '); - o.content = o.content.replace(t.reNbsp2BR2, '<' + elm + '$1$2>
    '); - } else - o.content = o.content.replace(t.reBR2Nbsp, '<' + elm + '$1$2>\u00a0'); - }; + // Check if node is wrapped in block + while (node != rootNode) { + if (blockElements[node.nodeName]) + return; - ed.onBeforeSetContent.add(padd); - ed.onPostProcess.add(padd); + node = node.parentNode; + } - if (s.forced_root_block) { - ed.onInit.add(t.forceRoots, t); - ed.onSetContent.add(t.forceRoots, t); - ed.onBeforeGetContent.add(t.forceRoots, t); - } - }, + // Get current selection + rng = selection.getRng(); + if (rng.setStart) { + startContainer = rng.startContainer; + startOffset = rng.startOffset; + endContainer = rng.endContainer; + endOffset = rng.endOffset; + } else { + // Force control range into text range + if (rng.item) { + rng = ed.getDoc().body.createTextRange(); + rng.moveToElementText(rng.item(0)); + } - setup : function() { - var t = this, ed = t.editor, s = ed.settings, dom = ed.dom, selection = ed.selection; + tmpRng = rng.duplicate(); + tmpRng.collapse(true); + startOffset = tmpRng.move('character', offset) * -1; - // Force root blocks when typing and when getting output - if (s.forced_root_block) { - ed.onBeforeExecCommand.add(t.forceRoots, t); - ed.onKeyUp.add(t.forceRoots, t); - ed.onPreProcess.add(t.forceRoots, t); + if (!tmpRng.collapsed) { + tmpRng = rng.duplicate(); + tmpRng.collapse(false); + endOffset = (tmpRng.move('character', offset) * -1) - startOffset; + } + } + + // Wrap non block elements and text nodes + for (node = rootNode.firstChild; node; node) { + if (node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName])) { + if (!rootBlockNode) { + rootBlockNode = dom.create(s.forced_root_block); + node.parentNode.insertBefore(rootBlockNode, node); + } + + tempNode = node; + node = node.nextSibling; + rootBlockNode.appendChild(tempNode); + } else { + rootBlockNode = null; + node = node.nextSibling; + } + } + + if (rng.setStart) { + rng.setStart(startContainer, startOffset); + rng.setEnd(endContainer, endOffset); + selection.setRng(rng); + } else { + try { + rng = ed.getDoc().body.createTextRange(); + rng.moveToElementText(rootNode); + rng.collapse(true); + rng.moveStart('character', startOffset); + + if (endOffset > 0) + rng.moveEnd('character', endOffset); + + rng.select(); + } catch (ex) { + // Ignore + } + } + + ed.nodeChanged(); + }; + + ed.onKeyUp.add(addRootBlocks); + ed.onClick.add(addRootBlocks); } if (s.force_br_newlines) { @@ -10934,12 +13158,12 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { var parent = ed.selection.getStart(), fmt = t._previousFormats; // Parent is an empty block - if (!parent.hasChildNodes()) { + if (!parent.hasChildNodes() && fmt) { parent = dom.getParent(parent, dom.isBlock); - if (parent) { + if (parent && parent.nodeName != 'LI') { parent.innerHTML = ''; - + if (t._previousFormats) { parent.appendChild(fmt.wrapper); fmt.inner.innerHTML = '\uFEFF'; @@ -10947,7 +13171,9 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { parent.innerHTML = '\uFEFF'; selection.select(parent, 1); + selection.collapse(true); ed.getDoc().execCommand('Delete', false, null); + t._previousFormats = 0; } } } @@ -11000,21 +13226,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }); } - // Padd empty inline elements within block elements - // For example:

    becomes

     

    - ed.onPreProcess.add(function(ed, o) { - each(dom.select('p,h1,h2,h3,h4,h5,h6,div', o.node), function(p) { - if (isEmpty(p)) { - each(dom.select('span,em,strong,b,i', o.node), function(n) { - if (!n.hasChildNodes()) { - n.appendChild(ed.getDoc().createTextNode('\u00a0')); - return FALSE; // Break the loop one padding is enough - } - }); - } - }); - }); - // IE specific fixes if (isIE) { // Replaces IE:s auto generated paragraphs with the specified element name @@ -11044,155 +13255,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } }, - find : function(n, t, s) { - var ed = this.editor, w = ed.getDoc().createTreeWalker(n, 4, null, FALSE), c = -1; - - while (n = w.nextNode()) { - c++; - - // Index by node - if (t == 0 && n == s) - return c; - - // Node by index - if (t == 1 && c == s) - return n; - } - - return -1; - }, - - forceRoots : function(ed, e) { - var t = this, ed = t.editor, b = ed.getBody(), d = ed.getDoc(), se = ed.selection, s = se.getSel(), r = se.getRng(), si = -2, ei, so, eo, tr, c = -0xFFFFFF; - var nx, bl, bp, sp, le, nl = b.childNodes, i, n, eid; - - // Fix for bug #1863847 - //if (e && e.keyCode == 13) - // return TRUE; - - // Wrap non blocks into blocks - for (i = nl.length - 1; i >= 0; i--) { - nx = nl[i]; - - // Ignore internal elements - if (nx.nodeType === 1 && nx.getAttribute('_mce_type')) { - bl = null; - continue; - } - - // Is text or non block element - if (nx.nodeType === 3 || (!t.dom.isBlock(nx) && nx.nodeType !== 8 && !/^(script|mce:script|style|mce:style)$/i.test(nx.nodeName))) { - if (!bl) { - // Create new block but ignore whitespace - if (nx.nodeType != 3 || /[^\s]/g.test(nx.nodeValue)) { - // Store selection - if (si == -2 && r) { - if (!isIE) { - // If selection is element then mark it - if (r.startContainer.nodeType == 1 && (n = r.startContainer.childNodes[r.startOffset]) && n.nodeType == 1) { - // Save the id of the selected element - eid = n.getAttribute("id"); - n.setAttribute("id", "__mce"); - } else { - // If element is inside body, might not be the case in contentEdiable mode - if (ed.dom.getParent(r.startContainer, function(e) {return e === b;})) { - so = r.startOffset; - eo = r.endOffset; - si = t.find(b, 0, r.startContainer); - ei = t.find(b, 0, r.endContainer); - } - } - } else { - // Force control range into text range - if (r.item) { - tr = d.body.createTextRange(); - tr.moveToElementText(r.item(0)); - r = tr; - } - - tr = d.body.createTextRange(); - tr.moveToElementText(b); - tr.collapse(1); - bp = tr.move('character', c) * -1; - - tr = r.duplicate(); - tr.collapse(1); - sp = tr.move('character', c) * -1; - - tr = r.duplicate(); - tr.collapse(0); - le = (tr.move('character', c) * -1) - sp; - - si = sp - bp; - ei = le; - } - } - - // Uses replaceChild instead of cloneNode since it removes selected attribute from option elements on IE - // See: http://support.microsoft.com/kb/829907 - bl = ed.dom.create(ed.settings.forced_root_block); - nx.parentNode.replaceChild(bl, nx); - bl.appendChild(nx); - } - } else { - if (bl.hasChildNodes()) - bl.insertBefore(nx, bl.firstChild); - else - bl.appendChild(nx); - } - } else - bl = null; // Time to create new block - } - - // Restore selection - if (si != -2) { - if (!isIE) { - bl = b.getElementsByTagName(ed.settings.element)[0]; - r = d.createRange(); - - // Select last location or generated block - if (si != -1) - r.setStart(t.find(b, 1, si), so); - else - r.setStart(bl, 0); - - // Select last location or generated block - if (ei != -1) - r.setEnd(t.find(b, 1, ei), eo); - else - r.setEnd(bl, 0); - - if (s) { - s.removeAllRanges(); - s.addRange(r); - } - } else { - try { - r = s.createRange(); - r.moveToElementText(b); - r.collapse(1); - r.moveStart('character', si); - r.moveEnd('character', ei); - r.select(); - } catch (ex) { - // Ignore - } - } - } else if (!isIE && (n = ed.dom.get('__mce'))) { - // Restore the id of the selected element - if (eid) - n.setAttribute('id', eid); - else - n.removeAttribute('id'); - - // Move caret before selected element - r = d.createRange(); - r.setStartBefore(n); - r.setEndBefore(n); - se.setRng(r); - } - }, - getParentBlock : function(n) { var d = this.dom; @@ -11203,6 +13265,8 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { var t = this, ed = t.editor, dom = ed.dom, d = ed.getDoc(), se = ed.settings, s = ed.selection.getSel(), r = s.getRangeAt(0), b = d.body; var rb, ra, dir, sn, so, en, eo, sb, eb, bn, bef, aft, sc, ec, n, vp = dom.getViewPort(ed.getWin()), y, ch, car; + ed.undoManager.beforeChange(); + // If root blocks are forced then use Operas default behavior since it's really good // Removed due to bug: #1853816 // if (se.forced_root_block && isOpera) @@ -11264,6 +13328,11 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { ra.setStart(en, 0); } + // If the body is totally empty add a BR element this might happen on webkit + if (!d.body.hasChildNodes()) { + d.body.appendChild(dom.create('br')); + } + // Never use body as start or end node sn = sn.nodeName == "HTML" ? d.body : sn; // Fix for Opera bug: https://bugs.opera.com/show_bug.cgi?id=273224&comments=yes sn = sn.nodeName == "BODY" ? sn.firstChild : sn; @@ -11378,10 +13447,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (aft.firstChild && aft.firstChild.nodeName == bn) aft.innerHTML = aft.firstChild.innerHTML; - // Padd empty blocks - if (isEmpty(bef)) - bef.innerHTML = '
    '; - function appendStyles(e, en) { var nl = [], nn, n, i; @@ -11406,14 +13471,18 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { nn = nn.appendChild(nl[i]); // Padd most inner style element - nl[0].innerHTML = isOpera ? ' ' : '
    '; // Extra space for Opera so that the caret can move there + nl[0].innerHTML = isOpera ? '\u00a0' : '
    '; // Extra space for Opera so that the caret can move there return nl[0]; // Move caret to most inner element } else - e.innerHTML = isOpera ? ' ' : '
    '; // Extra space for Opera so that the caret can move there + e.innerHTML = isOpera ? '\u00a0' : '
    '; // Extra space for Opera so that the caret can move there }; + + // Padd empty blocks + if (dom.isEmpty(bef)) + appendStyles(bef, sn); // Fill empty afterblook with current style - if (isEmpty(aft)) + if (dom.isEmpty(aft)) car = appendStyles(aft, en); // Opera needs this one backwards for older versions @@ -11429,27 +13498,26 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { aft.normalize(); bef.normalize(); - function first(n) { - return d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, FALSE).nextNode() || n; - }; - // Move cursor and scroll into view - r = d.createRange(); - r.selectNodeContents(isGecko ? first(car || aft) : car || aft); - r.collapse(1); - s.removeAllRanges(); - s.addRange(r); + ed.selection.select(aft, true); + ed.selection.collapse(true); // scrollIntoView seems to scroll the parent window in most browsers now including FF 3.0b4 so it's time to stop using it and do it our selfs y = ed.dom.getPos(aft).y; - ch = aft.clientHeight; + //ch = aft.clientHeight; // Is element within viewport - if (y < vp.y || y + ch > vp.y + vp.h) { + if (y < vp.y || y + 25 > vp.y + vp.h) { ed.getWin().scrollTo(0, y < vp.y ? y : y - vp.h + 25); // Needs to be hardcoded to roughly one line of text if a huge text block is broken into two blocks - //console.debug('SCROLL!', 'vp.y: ' + vp.y, 'y' + y, 'vp.h' + vp.h, 'clientHeight' + aft.clientHeight, 'yyy: ' + (y < vp.y ? y : y - vp.h + aft.clientHeight)); + + /*console.debug( + 'Element: y=' + y + ', h=' + ch + ', ' + + 'Viewport: y=' + vp.y + ", h=" + vp.h + ', bottom=' + (vp.y + vp.h) + );*/ } + ed.undoManager.add(); + return FALSE; }, @@ -11660,11 +13728,16 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { id = t.prefix + id; - if (ed.settings.use_native_selects) + + function useNativeListForAccessibility(ed) { + return ed.settings.use_accessible_selects && !tinymce.isGecko + } + + if (ed.settings.use_native_selects || useNativeListForAccessibility(ed)) c = new tinymce.ui.NativeListBox(id, s); else { cls = cc || t._cls.listbox || tinymce.ui.ListBox; - c = new cls(id, s); + c = new cls(id, s, ed); } t.controls[id] = c; @@ -11719,11 +13792,11 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (s.menu_button) { cls = cc || t._cls.menubutton || tinymce.ui.MenuButton; - c = new cls(id, s); + c = new cls(id, s, ed); ed.onMouseDown.add(c.hideMenu, c); } else { cls = t._cls.button || tinymce.ui.Button; - c = new cls(id, s); + c = new cls(id, s, ed); } return t.add(c); @@ -11766,7 +13839,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { id = t.prefix + id; cls = cc || t._cls.splitbutton || tinymce.ui.SplitButton; - c = t.add(new cls(id, s)); + c = t.add(new cls(id, s, ed)); ed.onMouseDown.add(c.hideMenu, c); return c; @@ -11806,7 +13879,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { id = t.prefix + id; cls = cc || t._cls.colorsplitbutton || tinymce.ui.ColorSplitButton; - c = new cls(id, s); + c = new cls(id, s, ed); ed.onMouseDown.add(c.hideMenu, c); // Remove the menu element when the editor is removed @@ -11838,13 +13911,25 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { id = t.prefix + id; cls = cc || t._cls.toolbar || tinymce.ui.Toolbar; - c = new cls(id, s); + c = new cls(id, s, t.editor); if (t.get(id)) return null; return t.add(c); }, + + createToolbarGroup : function(id, s, cc) { + var c, t = this, cls; + id = t.prefix + id; + cls = cc || this._cls.toolbarGroup || tinymce.ui.ToolbarGroup; + c = new cls(id, s, t.editor); + + if (t.get(id)) + return null; + + return t.add(c); + }, createSeparator : function(cc) { var cls = cc || this._cls.separator || tinymce.ui.Separator; @@ -11981,53 +14066,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } }); }(tinymce)); -(function(tinymce) { - function CommandManager() { - var execCommands = {}, queryStateCommands = {}, queryValueCommands = {}; - - function add(collection, cmd, func, scope) { - if (typeof(cmd) == 'string') - cmd = [cmd]; - - tinymce.each(cmd, function(cmd) { - collection[cmd.toLowerCase()] = {func : func, scope : scope}; - }); - }; - - tinymce.extend(this, { - add : function(cmd, func, scope) { - add(execCommands, cmd, func, scope); - }, - - addQueryStateHandler : function(cmd, func, scope) { - add(queryStateCommands, cmd, func, scope); - }, - - addQueryValueHandler : function(cmd, func, scope) { - add(queryValueCommands, cmd, func, scope); - }, - - execCommand : function(scope, cmd, ui, value, args) { - if (cmd = execCommands[cmd.toLowerCase()]) { - if (cmd.func.call(scope || cmd.scope, ui, value, args) !== false) - return true; - } - }, - - queryCommandValue : function() { - if (cmd = queryValueCommands[cmd.toLowerCase()]) - return cmd.func.call(scope || cmd.scope, ui, value, args); - }, - - queryCommandState : function() { - if (cmd = queryStateCommands[cmd.toLowerCase()]) - return cmd.func.call(scope || cmd.scope, ui, value, args); - } - }); - }; - - tinymce.GlobalCommands = new CommandManager(); -})(tinymce); (function(tinymce) { tinymce.Formatter = function(ed) { var formats = {}, @@ -12036,7 +14074,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { selection = ed.selection, TreeWalker = tinymce.dom.TreeWalker, rangeUtils = new tinymce.dom.RangeUtils(dom), - isValid = ed.schema.isValid, + isValid = ed.schema.isValidChild, isBlock = dom.isBlock, forcedRootBlock = ed.settings.forced_root_block, nodeIndex = dom.nodeIndex, @@ -12044,8 +14082,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { MCE_ATTR_RE = /^(src|href|style)$/, FALSE = false, TRUE = true, - undefined, - pendingFormats = {apply : [], remove : []}; + undefined; function isArray(obj) { return obj instanceof Array; @@ -12105,8 +14142,31 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } }; + var getTextDecoration = function(node) { + var decoration; + + ed.dom.getParent(node, function(n) { + decoration = ed.dom.getStyle(n, 'text-decoration'); + return decoration && decoration !== 'none'; + }); + + return decoration; + }; + + var processUnderlineAndColor = function(node) { + var textDecoration; + if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) { + textDecoration = getTextDecoration(node.parentNode); + if (ed.dom.getStyle(node, 'color') && textDecoration) { + ed.dom.setStyle(node, 'text-decoration', textDecoration); + } else if (ed.dom.getStyle(node, 'textdecoration') === textDecoration) { + ed.dom.setStyle(node, 'text-decoration', null); + } + } + }; + function apply(name, vars, node) { - var formatList = get(name), format = formatList[0], bookmark, rng, i; + var formatList = get(name), format = formatList[0], bookmark, rng, i, isCollapsed = selection.isCollapsed(); function moveStart(rng) { var container = rng.startContainer, @@ -12136,6 +14196,10 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { fmt = fmt || format; if (elm) { + if (fmt.onformat) { + fmt.onformat(elm, fmt, vars, node); + } + each(fmt.styles, function(value, name) { dom.setStyle(elm, name, replaceVars(value, vars)); }); @@ -12152,8 +14216,89 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }); } }; + function adjustSelectionToVisibleSelection() { + function findSelectionEnd(start, end) { + var walker = new TreeWalker(end); + for (node = walker.current(); node; node = walker.prev()) { + if (node.childNodes.length > 1 || node == start) { + return node; + } + } + }; + + // Adjust selection so that a end container with a end offset of zero is not included in the selection + // as this isn't visible to the user. + var rng = ed.selection.getRng(); + var start = rng.startContainer; + var end = rng.endContainer; + + if (start != end && rng.endOffset == 0) { + var newEnd = findSelectionEnd(start, end); + var endOffset = newEnd.nodeType == 3 ? newEnd.length : newEnd.childNodes.length; + + rng.setEnd(newEnd, endOffset); + } + + return rng; + } + + function applyStyleToList(node, bookmark, wrapElm, newWrappers, process){ + var nodes = [], listIndex = -1, list, startIndex = -1, endIndex = -1, currentWrapElm; + + // find the index of the first child list. + each(node.childNodes, function(n, index) { + if (n.nodeName === "UL" || n.nodeName === "OL") { + listIndex = index; + list = n; + return false; + } + }); + + // get the index of the bookmarks + each(node.childNodes, function(n, index) { + if (n.nodeName === "SPAN" && dom.getAttrib(n, "data-mce-type") == "bookmark") { + if (n.id == bookmark.id + "_start") { + startIndex = index; + } else if (n.id == bookmark.id + "_end") { + endIndex = index; + } + } + }); + + // if the selection spans across an embedded list, or there isn't an embedded list - handle processing normally + if (listIndex <= 0 || (startIndex < listIndex && endIndex > listIndex)) { + each(tinymce.grep(node.childNodes), process); + return 0; + } else { + currentWrapElm = wrapElm.cloneNode(FALSE); + + // create a list of the nodes on the same side of the list as the selection + each(tinymce.grep(node.childNodes), function(n, index) { + if ((startIndex < listIndex && index < listIndex) || (startIndex > listIndex && index > listIndex)) { + nodes.push(n); + n.parentNode.removeChild(n); + } + }); + + // insert the wrapping element either before or after the list. + if (startIndex < listIndex) { + node.insertBefore(currentWrapElm, list); + } else if (startIndex > listIndex) { + node.insertBefore(currentWrapElm, list.nextSibling); + } + + // add the new nodes to the list. + newWrappers.push(currentWrapElm); + + each(nodes, function(node) { + currentWrapElm.appendChild(node); + }); - function applyRngStyle(rng) { + return currentWrapElm; + } + }; + + function applyRngStyle(rng, bookmark, node_specific) { var newWrappers = [], wrapName, wrapElm; // Setup wrapper element @@ -12197,6 +14342,11 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (format.selector) { // Look for matching formats each(formatList, function(format) { + // Check collapsed state if it exists + if ('collapsed' in format && format.collapsed !== isCollapsed) { + return; + } + if (dom.is(node, format.selector) && !isCaretNode(node)) { setElementFormat(node, format); found = true; @@ -12211,7 +14361,8 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } // Is it valid to wrap this item - if (isValid(wrapName, nodeName) && isValid(parentName, wrapName)) { + if (isValid(wrapName, nodeName) && isValid(parentName, wrapName) && + !(!node_specific && node.nodeType === 3 && node.nodeValue.length === 1 && node.nodeValue.charCodeAt(0) === 65279) && node.id !== '_mce_caret') { // Start wrapping if (!currentWrapElm) { // Wrap the node @@ -12221,6 +14372,9 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } currentWrapElm.appendChild(node); + } else if (nodeName == 'li' && bookmark) { + // Start wrapping - if we are in a list node and have a bookmark, then we will always begin by wrapping in a new element. + currentWrapElm = applyStyleToList(node, bookmark, wrapElm, newWrappers, process); } else { // Start a new wrapper for possible children currentWrapElm = 0; @@ -12232,9 +14386,33 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } }; - // Process siblings from range - each(nodes, process); - }); + // Process siblings from range + each(nodes, process); + }); + + // Wrap links inside as well, for example color inside a link when the wrapper is around the link + if (format.wrap_links === false) { + each(newWrappers, function(node) { + function process(node) { + var i, currentWrapElm, children; + + if (node.nodeName === 'A') { + currentWrapElm = wrapElm.cloneNode(FALSE); + newWrappers.push(currentWrapElm); + + children = tinymce.grep(node.childNodes); + for (i = 0; i < children.length; i++) + currentWrapElm.appendChild(children[i]); + + node.appendChild(currentWrapElm); + } + + each(tinymce.grep(node.childNodes), process); + }; + + process(node); + }); + } // Cleanup each(newWrappers, function(node) { @@ -12275,8 +14453,9 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { childCount = getChildCount(node); - // Remove empty nodes - if (childCount === 0) { + // Remove empty nodes but only if there is multiple wrappers and they are not block + // elements so never remove single

    since that would remove the currrent empty block element where the caret is at + if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) { dom.remove(node, 1); return; } @@ -12292,6 +14471,19 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // this: text // will become: text each(dom.select(format.inline, node), function(child) { + var parent; + + // When wrap_links is set to false we don't want + // to remove the format on children within links + if (format.wrap_links === false) { + parent = child.parentNode; + + do { + if (parent.nodeName === 'A') + return; + } while (parent = parent.parentNode); + } + removeFormat(format, vars, child, format.exact ? child : null); }); }); @@ -12315,7 +14507,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } // Merge next and previous siblings if they are similar texttext becomes texttext - if (node) { + if (node && format.merge_siblings !== false) { node = mergeSiblings(getNonWhiteSpaceSibling(node), node); node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE)); } @@ -12325,17 +14517,29 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (format) { if (node) { - rng = dom.createRng(); - - rng.setStartBefore(node); - rng.setEndAfter(node); - - applyRngStyle(expandRng(rng, formatList)); + if (node.nodeType) { + rng = dom.createRng(); + rng.setStartBefore(node); + rng.setEndAfter(node); + applyRngStyle(expandRng(rng, formatList), null, true); + } else { + applyRngStyle(node, null, true); + } } else { - if (!selection.isCollapsed() || !format.inline) { + if (!isCollapsed || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) { + // Obtain selection node before selection is unselected by applyRngStyle() + var curSelNode = ed.selection.getNode(); + // Apply formatting to selection + ed.selection.setRng(adjustSelectionToVisibleSelection()); bookmark = selection.getBookmark(); - applyRngStyle(expandRng(selection.getRng(TRUE), formatList)); + applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark); + + // Colored nodes should be underlined so that the color of the underline matches the text color. + if (format.styles && (format.styles.color || format.styles.textDecoration)) { + tinymce.walk(curSelNode, processUnderlineAndColor, 'childNodes'); + processUnderlineAndColor(curSelNode); + } selection.moveToBookmark(bookmark); selection.setRng(moveStart(selection.getRng(TRUE))); @@ -12348,6 +14552,44 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { function remove(name, vars, node) { var formatList = get(name), format = formatList[0], bookmark, i, rng; + function moveStart(rng) { + var container = rng.startContainer, + offset = rng.startOffset, + walker, node, nodes, tmpNode; + + // Convert text node into index if possible + if (container.nodeType == 3 && offset >= container.nodeValue.length - 1) { + container = container.parentNode; + offset = nodeIndex(container) + 1; + } + + // Move startContainer/startOffset in to a suitable node + if (container.nodeType == 1) { + nodes = container.childNodes; + container = nodes[Math.min(offset, nodes.length - 1)]; + walker = new TreeWalker(container); + + // If offset is at end of the parent node walk to the next one + if (offset > nodes.length - 1) + walker.next(); + + for (node = walker.current(); node; node = walker.next()) { + if (node.nodeType == 3 && !isWhiteSpaceNode(node)) { + // IE has a "neat" feature where it moves the start node into the closest element + // we can avoid this by inserting an element before it and then remove it after we set the selection + tmpNode = dom.create('a', null, INVISIBLE_CHAR); + node.parentNode.insertBefore(tmpNode, node); + + // Set selection and remove tmpNode + rng.setStart(node, 0); + selection.setRng(rng); + dom.remove(tmpNode); + + return; + } + } + } + }; // Merges the styles for each node function process(node) { @@ -12461,8 +14703,8 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (startContainer != endContainer) { // Wrap start/end nodes in span element since these might be cloned/moved - startContainer = wrap(startContainer, 'span', {id : '_start', _mce_type : 'bookmark'}); - endContainer = wrap(endContainer, 'span', {id : '_end', _mce_type : 'bookmark'}); + startContainer = wrap(startContainer, 'span', {id : '_start', 'data-mce-type' : 'bookmark'}); + endContainer = wrap(endContainer, 'span', {id : '_end', 'data-mce-type' : 'bookmark'}); // Split start/end splitToFormatRoot(startContainer); @@ -12485,30 +14727,53 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { rangeUtils.walk(rng, function(nodes) { each(nodes, function(node) { process(node); + + // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined. + if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' && node.parentNode && getTextDecoration(node.parentNode) === 'underline') { + removeFormat({'deep': false, 'exact': true, 'inline': 'span', 'styles': {'textDecoration' : 'underline'}}, null, node); + } }); }); }; // Handle node if (node) { - rng = dom.createRng(); - rng.setStartBefore(node); - rng.setEndAfter(node); - removeRngStyle(rng); + if (node.nodeType) { + rng = dom.createRng(); + rng.setStartBefore(node); + rng.setEndAfter(node); + removeRngStyle(rng); + } else { + removeRngStyle(node); + } + return; } - if (!selection.isCollapsed() || !format.inline) { + if (!selection.isCollapsed() || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) { bookmark = selection.getBookmark(); removeRngStyle(selection.getRng(TRUE)); selection.moveToBookmark(bookmark); + + // Check if start element still has formatting then we are at: "text|text" and need to move the start into the next text node + if (format.inline && match(name, vars, selection.getStart())) { + moveStart(selection.getRng(true)); + } + ed.nodeChanged(); } else performCaretAction('remove', name, vars); + + // When you remove formatting from a table cell in WebKit (cell, not the contents of a cell) there is a rendering issue with column width + if (tinymce.isWebKit) { + ed.execCommand('mceCleanup'); + } }; function toggle(name, vars, node) { - if (match(name, vars, node)) + var fmt = get(name); + + if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0]['toggle'])) remove(name, vars, node); else apply(name, vars, node); @@ -12520,6 +14785,11 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { function matchItems(node, format, item_name) { var key, value, items = format[item_name], i; + // Custom match + if (format.onmatch) { + return format.onmatch(node, format, item_name); + } + // Check all items if (items) { // Non indexed object @@ -12572,7 +14842,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }; function match(name, vars, node) { - var startNode, i; + var startNode; function matchParents(node) { // Find first node with similar format settings @@ -12588,21 +14858,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (node) return matchParents(node); - // Check pending formats - if (selection.isCollapsed()) { - for (i = pendingFormats.apply.length - 1; i >= 0; i--) { - if (pendingFormats.apply[i].name == name) - return true; - } - - for (i = pendingFormats.remove.length - 1; i >= 0; i--) { - if (pendingFormats.remove[i].name == name) - return false; - } - - return matchParents(selection.getNode()); - } - // Check selected node node = selection.getNode(); if (matchParents(node)) @@ -12621,33 +14876,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { function matchAll(names, vars) { var startElement, matchedFormatNames = [], checkedMap = {}, i, ni, name; - // If the selection is collapsed then check pending formats - if (selection.isCollapsed()) { - for (ni = 0; ni < names.length; ni++) { - // If the name is to be removed, then stop it from being added - for (i = pendingFormats.remove.length - 1; i >= 0; i--) { - name = names[ni]; - - if (pendingFormats.remove[i].name == name) { - checkedMap[name] = true; - break; - } - } - } - - // If the format is to be applied - for (i = pendingFormats.apply.length - 1; i >= 0; i--) { - for (ni = 0; ni < names.length; ni++) { - name = names[ni]; - - if (!checkedMap[name] && pendingFormats.apply[i].name == name) { - checkedMap[name] = true; - matchedFormatNames.push(name); - } - } - } - } - // Check start of selection for formats startElement = selection.getStart(); dom.getParent(startElement, function(node) { @@ -12756,7 +14984,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }; function isWhiteSpaceNode(node) { - return node && node.nodeType === 3 && /^([\s\r\n]+|)$/.test(node.nodeValue); + return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue); }; function wrap(node, name, attrs) { @@ -12772,36 +15000,55 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { var startContainer = rng.startContainer, startOffset = rng.startOffset, endContainer = rng.endContainer, - endOffset = rng.endOffset, sibling, lastIdx; + endOffset = rng.endOffset, sibling, lastIdx, leaf, endPoint; // This function walks up the tree if there is no siblings before/after the node - function findParentContainer(container, child_name, sibling_name, root) { - var parent, child; + function findParentContainer(start) { + var container, parent, child, sibling, siblingName; - root = root || dom.getRoot(); + container = parent = start ? startContainer : endContainer; + siblingName = start ? 'previousSibling' : 'nextSibling'; + root = dom.getRoot(); - for (;;) { - // Check if we can move up are we at root level or body level - parent = container.parentNode; + // If it's a text node and the offset is inside the text + if (container.nodeType == 3 && !isWhiteSpaceNode(container)) { + if (start ? startOffset > 0 : endOffset < container.nodeValue.length) { + return container; + } + } + for (;;) { // Stop expanding on block elements or root depending on format if (parent == root || (!format[0].block_expand && isBlock(parent))) - return container; - - for (sibling = parent[child_name]; sibling && sibling != container; sibling = sibling[sibling_name]) { - if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) - return container; + return parent; - if (sibling.nodeType == 3 && !isWhiteSpaceNode(sibling)) - return container; + // Walk left/right + for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) { + if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling)) { + return parent; + } } - container = container.parentNode; + // Check if we can move up are we at root level or body level + parent = parent.parentNode; } return container; }; + // This function walks down the tree to find the leaf at the selection. + // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node. + function findLeaf(node, offset) { + if (offset === undefined) + offset = node.nodeType === 3 ? node.length : node.childNodes.length; + while (node && node.hasChildNodes()) { + node = node.childNodes[offset]; + if (node) + offset = node.nodeType === 3 ? node.length : node.childNodes.length; + } + return { node: node, offset: offset }; + } + // If index based start position then resolve it if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { lastIdx = startContainer.childNodes.length - 1; @@ -12821,31 +15068,141 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } // Exclude bookmark nodes if possible - if (isBookmarkNode(startContainer.parentNode)) - startContainer = startContainer.parentNode; - - if (isBookmarkNode(startContainer)) + if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) { + startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode; startContainer = startContainer.nextSibling || startContainer; - if (isBookmarkNode(endContainer.parentNode)) - endContainer = endContainer.parentNode; + if (startContainer.nodeType == 3) + startOffset = 0; + } - if (isBookmarkNode(endContainer)) + if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) { + endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode; endContainer = endContainer.previousSibling || endContainer; + if (endContainer.nodeType == 3) + endOffset = endContainer.length; + } + + if (format[0].inline) { + if (rng.collapsed) { + function findWordEndPoint(container, offset, start) { + var walker, node, pos, lastTextNode; + + function findSpace(node, offset) { + var pos, pos2, str = node.nodeValue; + + if (typeof(offset) == "undefined") { + offset = start ? str.length : 0; + } + + if (start) { + pos = str.lastIndexOf(' ', offset); + pos2 = str.lastIndexOf('\u00a0', offset); + pos = pos > pos2 ? pos : pos2; + + // Include the space on remove to avoid tag soup + if (pos !== -1 && !remove) { + pos++; + } + } else { + pos = str.indexOf(' ', offset); + pos2 = str.indexOf('\u00a0', offset); + pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2; + } + + return pos; + }; + + if (container.nodeType === 3) { + pos = findSpace(container, offset); + + if (pos !== -1) { + return {container : container, offset : pos}; + } + + lastTextNode = container; + } + + // Walk the nodes inside the block + walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody()); + while (node = walker[start ? 'prev' : 'next']()) { + if (node.nodeType === 3) { + lastTextNode = node; + pos = findSpace(node); + + if (pos !== -1) { + return {container : node, offset : pos}; + } + } else if (isBlock(node)) { + break; + } + } + + if (lastTextNode) { + if (start) { + offset = 0; + } else { + offset = lastTextNode.length; + } + + return {container: lastTextNode, offset: offset}; + } + } + + // Expand left to closest word boundery + endPoint = findWordEndPoint(startContainer, startOffset, true); + if (endPoint) { + startContainer = endPoint.container; + startOffset = endPoint.offset; + } + + // Expand right to closest word boundery + endPoint = findWordEndPoint(endContainer, endOffset); + if (endPoint) { + endContainer = endPoint.container; + endOffset = endPoint.offset; + } + } + + // Avoid applying formatting to a trailing space. + leaf = findLeaf(endContainer, endOffset); + if (leaf.node) { + while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) + leaf = findLeaf(leaf.node.previousSibling); + + if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 && + leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') { + + if (leaf.offset > 1) { + endContainer = leaf.node; + endContainer.splitText(leaf.offset - 1); + } else if (leaf.node.previousSibling) { + // TODO: Figure out why this is in here + //endContainer = leaf.node.previousSibling; + } + } + } + } + // Move start/end point up the tree if the leaves are sharp and if we are in different containers // Example * becomes !: !

    *texttext*

    ! // This will reduce the number of wrapper elements that needs to be created // Move start point up the tree if (format[0].inline || format[0].block_expand) { - startContainer = findParentContainer(startContainer, 'firstChild', 'nextSibling'); - endContainer = findParentContainer(endContainer, 'lastChild', 'previousSibling'); + if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) { + startContainer = findParentContainer(true); + } + + if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) { + endContainer = findParentContainer(); + } } // Expand start/end container to matching selector if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) { function findSelectorEndPoint(container, sibling_name) { - var parents, i, y; + var parents, i, y, curFormat; if (container.nodeType == 3 && container.nodeValue.length == 0 && container[sibling_name]) container = container[sibling_name]; @@ -12853,7 +15210,13 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { parents = getParents(container); for (i = 0; i < parents.length; i++) { for (y = 0; y < format.length; y++) { - if (dom.is(parents[i], format[y].selector)) + curFormat = format[y]; + + // If collapsed state is set then skip formats that doesn't match that + if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) + continue; + + if (dom.is(parents[i], curFormat.selector)) return parents[i]; } } @@ -12907,10 +15270,10 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Non block element then try to expand up the leaf if (format[0].block) { if (!isBlock(startContainer)) - startContainer = findParentContainer(startContainer, 'firstChild', 'nextSibling'); + startContainer = findParentContainer(true); if (!isBlock(endContainer)) - endContainer = findParentContainer(endContainer, 'lastChild', 'previousSibling'); + endContainer = findParentContainer(); } } @@ -12963,7 +15326,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Remove style attribute if it's empty if (stylesModified && dom.getAttrib(node, 'style') == '') { node.removeAttribute('style'); - node.removeAttribute('_mce_style'); + node.removeAttribute('data-mce-style'); } // Remove attributes @@ -13004,7 +15367,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Remove mce prefixed attributes if (MCE_ATTR_RE.test(name)) - node.removeAttribute('_mce_' + name); + node.removeAttribute('data-mce-' + name); node.removeAttribute(name); } @@ -13089,7 +15452,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }; function isBookmarkNode(node) { - return node && node.nodeType == 1 && node.getAttribute('_mce_type') == 'bookmark'; + return node && node.nodeType == 1 && node.getAttribute('data-mce-type') == 'bookmark'; }; function mergeSiblings(prev, next) { @@ -13160,7 +15523,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (prev && next) { function findElementSibling(node, sibling_name) { for (sibling = node; sibling; sibling = sibling[sibling_name]) { - if (sibling.nodeType == 3 && !isWhiteSpaceNode(sibling)) + if (sibling.nodeType == 3 && sibling.nodeValue.length !== 0) return node; if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) @@ -13203,7 +15566,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }; function getContainer(rng, start) { - var container, offset, lastIdx; + var container, offset, lastIdx, walker; container = rng[start ? 'startContainer' : 'endContainer']; offset = rng[start ? 'startOffset' : 'endOffset']; @@ -13217,103 +15580,267 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { container = container.childNodes[offset > lastIdx ? lastIdx : offset]; } + // If start text node is excluded then walk to the next node + if (container.nodeType === 3 && start && offset >= container.nodeValue.length) { + container = new TreeWalker(container, ed.getBody()).next() || container; + } + + // If end text node is excluded then walk to the previous node + if (container.nodeType === 3 && !start && offset == 0) { + container = new TreeWalker(container, ed.getBody()).prev() || container; + } + return container; }; function performCaretAction(type, name, vars) { - var i, currentPendingFormats = pendingFormats[type], - otherPendingFormats = pendingFormats[type == 'apply' ? 'remove' : 'apply']; + var invisibleChar, caretContainerId = '_mce_caret', debug = ed.settings.caret_debug; + + // Setup invisible character use zero width space on Gecko since it doesn't change the heigt of the container + invisibleChar = tinymce.isGecko ? '\u200B' : INVISIBLE_CHAR; + + // Creates a caret container bogus element + function createCaretContainer(fill) { + var caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true, style: debug ? 'color:red' : ''}); + + if (fill) { + caretContainer.appendChild(ed.getDoc().createTextNode(invisibleChar)); + } + + return caretContainer; + }; + + function isCaretContainerEmpty(node, nodes) { + while (node) { + if ((node.nodeType === 3 && node.nodeValue !== invisibleChar) || node.childNodes.length > 1) { + return false; + } + + // Collect nodes + if (nodes && node.nodeType === 1) { + nodes.push(node); + } - function hasPending() { - return pendingFormats.apply.length || pendingFormats.remove.length; + node = node.firstChild; + } + + return true; }; + + // Returns any parent caret container element + function getParentCaretContainer(node) { + while (node) { + if (node.id === caretContainerId) { + return node; + } - function resetPending() { - pendingFormats.apply = []; - pendingFormats.remove = []; + node = node.parentNode; + } }; - function perform(caret_node) { - // Apply pending formats - each(pendingFormats.apply.reverse(), function(item) { - apply(item.name, item.vars, caret_node); - }); + // Finds the first text node in the specified node + function findFirstTextNode(node) { + var walker; - // Remove pending formats - each(pendingFormats.remove.reverse(), function(item) { - remove(item.name, item.vars, caret_node); - }); + if (node) { + walker = new TreeWalker(node, node); + + for (node = walker.current(); node; node = walker.next()) { + if (node.nodeType === 3) { + return node; + } + } + } + }; + + // Removes the caret container for the specified node or all on the current document + function removeCaretContainer(node, move_caret) { + var child, rng; + + if (!node) { + node = getParentCaretContainer(selection.getStart()); + + if (!node) { + while (node = dom.get(caretContainerId)) { + removeCaretContainer(node, false); + } + } + } else { + rng = selection.getRng(true); + + if (isCaretContainerEmpty(node)) { + if (move_caret !== false) { + rng.setStartBefore(node); + rng.setEndBefore(node); + } + + dom.remove(node); + } else { + child = findFirstTextNode(node); + child = child.deleteData(0, 1); + dom.remove(node, 1); + } + + selection.setRng(rng); + } + }; + + // Applies formatting to the caret postion + function applyCaretFormat() { + var rng, caretContainer, textNode, offset, bookmark, container, text; + + rng = selection.getRng(true); + offset = rng.startOffset; + container = rng.startContainer; + text = container.nodeValue; + + caretContainer = getParentCaretContainer(selection.getStart()); + if (caretContainer) { + textNode = findFirstTextNode(caretContainer); + } + + // Expand to word is caret is in the middle of a text node and the char before/after is a alpha numeric character + if (text && offset > 0 && offset < text.length && /\w/.test(text.charAt(offset)) && /\w/.test(text.charAt(offset - 1))) { + // Get bookmark of caret position + bookmark = selection.getBookmark(); + + // Collapse bookmark range (WebKit) + rng.collapse(true); + + // Expand the range to the closest word and split it at those points + rng = expandRng(rng, get(name)); + rng = rangeUtils.split(rng); + + // Apply the format to the range + apply(name, vars, rng); + + // Move selection back to caret position + selection.moveToBookmark(bookmark); + } else { + if (!caretContainer || textNode.nodeValue !== invisibleChar) { + caretContainer = createCaretContainer(true); + textNode = caretContainer.firstChild; + + rng.insertNode(caretContainer); + offset = 1; + + apply(name, vars, caretContainer); + } else { + apply(name, vars, caretContainer); + } - dom.remove(caret_node, 1); - resetPending(); + // Move selection to text node + selection.setCursorLocation(textNode, offset); + } }; - // Check if it already exists then ignore it - for (i = currentPendingFormats.length - 1; i >= 0; i--) { - if (currentPendingFormats[i].name == name) + function removeCaretFormat() { + var rng = selection.getRng(true), container, offset, bookmark, + hasContentAfter, node, formatNode, parents = [], i, caretContainer; + + container = rng.startContainer; + offset = rng.startOffset; + node = container; + + if (container.nodeType == 3) { + if (offset != container.nodeValue.length || container.nodeValue === invisibleChar) { + hasContentAfter = true; + } + + node = node.parentNode; + } + + while (node) { + if (matchNode(node, name, vars)) { + formatNode = node; + break; + } + + if (node.nextSibling) { + hasContentAfter = true; + } + + parents.push(node); + node = node.parentNode; + } + + // Node doesn't have the specified format + if (!formatNode) { return; - } + } - currentPendingFormats.push({name : name, vars : vars}); + // Is there contents after the caret then remove the format on the element + if (hasContentAfter) { + // Get bookmark of caret position + bookmark = selection.getBookmark(); - // Check if it's in the other type, then remove it - for (i = otherPendingFormats.length - 1; i >= 0; i--) { - if (otherPendingFormats[i].name == name) - otherPendingFormats.splice(i, 1); - } + // Collapse bookmark range (WebKit) + rng.collapse(true); - // Pending apply or remove formats - if (hasPending()) { - ed.getDoc().execCommand('FontName', false, 'mceinline'); - pendingFormats.lastRng = selection.getRng(); + // Expand the range to the closest word and split it at those points + rng = expandRng(rng, get(name), true); + rng = rangeUtils.split(rng); - // IE will convert the current word - each(dom.select('font,span'), function(node) { - var bookmark; + // Remove the format from the range + remove(name, vars, rng); - if (isCaretNode(node)) { - bookmark = selection.getBookmark(); - perform(node); - selection.moveToBookmark(bookmark); - ed.nodeChanged(); + // Move selection back to caret position + selection.moveToBookmark(bookmark); + } else { + caretContainer = createCaretContainer(); + + node = caretContainer; + for (i = parents.length - 1; i >= 0; i--) { + node.appendChild(parents[i].cloneNode(false)); + node = node.firstChild; + } + + // Insert invisible character into inner most format element + node.appendChild(dom.doc.createTextNode(invisibleChar)); + node = node.firstChild; + + // Insert caret container after the formated node + dom.insertAfter(caretContainer, formatNode); + + // Move selection to text node + selection.setCursorLocation(node, 1); + } + }; + + // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements + ed.onBeforeGetContent.addToTop(function() { + var nodes = [], i; + + if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) { + // Mark children + i = nodes.length; + while (i--) { + dom.setAttrib(nodes[i], 'data-mce-bogus', '1'); } + } + }); + + // Remove caret container on mouse up and on key up + tinymce.each('onMouseUp onKeyUp'.split(' '), function(name) { + ed[name].addToTop(function() { + removeCaretContainer(); }); + }); - // Only register listeners once if we need to - if (!pendingFormats.isListening && hasPending()) { - pendingFormats.isListening = true; - - each('onKeyDown,onKeyUp,onKeyPress,onMouseUp'.split(','), function(event) { - ed[event].addToTop(function(ed, e) { - // Do we have pending formats and is the selection moved has moved - if (hasPending() && !tinymce.dom.RangeUtils.compareRanges(pendingFormats.lastRng, selection.getRng())) { - each(dom.select('font,span'), function(node) { - var textNode, rng; - - // Look for marker - if (isCaretNode(node)) { - textNode = node.firstChild; - - if (textNode) { - perform(node); - - rng = dom.createRng(); - rng.setStart(textNode, textNode.nodeValue.length); - rng.setEnd(textNode, textNode.nodeValue.length); - selection.setRng(rng); - ed.nodeChanged(); - } else - dom.remove(node); - } - }); + // Remove caret container on keydown and it's a backspace, enter or left/right arrow keys + ed.onKeyDown.addToTop(function(ed, e) { + var keyCode = e.keyCode; - // Always unbind and clear pending styles on keyup - if (e.type == 'keyup' || e.type == 'mouseup') - resetPending(); - } - }); - }); + if (keyCode == 8 || keyCode == 37 || keyCode == 39) { + removeCaretContainer(getParentCaretContainer(selection.getStart())); } + }); + + // Do apply or remove caret format + if (type == "apply") { + applyCaretFormat(); + } else { + removeCaretFormat(); } }; }; @@ -13323,12 +15850,15 @@ tinymce.onAddEditor.add(function(tinymce, ed) { var filters, fontSizes, dom, settings = ed.settings; if (settings.inline_styles) { - fontSizes = tinymce.explode(settings.font_size_style_values); + fontSizes = tinymce.explode(settings.font_size_legacy_values); function replaceWithSpan(node, styles) { - dom.replace(dom.create('span', { - style : styles - }), node, 1); + tinymce.each(styles, function(value, name) { + if (value) + dom.setStyle(node, name, value); + }); + + dom.rename(node, 'span'); }; filters = { @@ -13365,6 +15895,7 @@ tinymce.onAddEditor.add(function(tinymce, ed) { }; ed.onPreProcess.add(convert); + ed.onSetContent.add(convert); ed.onInit.add(function() { ed.selection.onSetContent.add(convert); diff --git a/js/tiny_mce/tiny_mce_popup.js b/js/tiny_mce/tiny_mce_popup.js index 3ef3acb1fd..f859d24e6a 100644 --- a/js/tiny_mce/tiny_mce_popup.js +++ b/js/tiny_mce/tiny_mce_popup.js @@ -2,4 +2,4 @@ // Uncomment and change this document.domain value if you are loading the script cross subdomains // document.domain = 'moxiecode.com'; -var tinymce=null,tinyMCEPopup,tinyMCE;tinyMCEPopup={init:function(){var b=this,a,c;a=b.getWin();tinymce=a.tinymce;tinyMCE=a.tinyMCE;b.editor=tinymce.EditorManager.activeEditor;b.params=b.editor.windowManager.params;b.features=b.editor.windowManager.features;b.dom=b.editor.windowManager.createInstance("tinymce.dom.DOMUtils",document);if(b.features.popup_css!==false){b.dom.loadCSS(b.features.popup_css||b.editor.settings.popup_css)}b.listeners=[];b.onInit={add:function(e,d){b.listeners.push({func:e,scope:d})}};b.isWindow=!b.getWindowArg("mce_inline");b.id=b.getWindowArg("mce_window_id");b.editor.windowManager.onOpen.dispatch(b.editor.windowManager,window)},getWin:function(){return(!window.frameElement&&window.dialogArguments)||opener||parent||top},getWindowArg:function(c,b){var a=this.params[c];return tinymce.is(a)?a:b},getParam:function(b,a){return this.editor.getParam(b,a)},getLang:function(b,a){return this.editor.getLang(b,a)},execCommand:function(d,c,e,b){b=b||{};b.skip_focus=1;this.restoreSelection();return this.editor.execCommand(d,c,e,b)},resizeToInnerSize:function(){var a=this;setTimeout(function(){var b=a.dom.getViewPort(window);a.editor.windowManager.resizeBy(a.getWindowArg("mce_width")-b.w,a.getWindowArg("mce_height")-b.h,a.id||window)},0)},executeOnLoad:function(s){this.onInit.add(function(){eval(s)})},storeSelection:function(){this.editor.windowManager.bookmark=tinyMCEPopup.editor.selection.getBookmark(1)},restoreSelection:function(){var a=tinyMCEPopup;if(!a.isWindow&&tinymce.isIE){a.editor.selection.moveToBookmark(a.editor.windowManager.bookmark)}},requireLangPack:function(){var b=this,a=b.getWindowArg("plugin_url")||b.getWindowArg("theme_url");if(a&&b.editor.settings.language&&b.features.translate_i18n!==false){a+="/langs/"+b.editor.settings.language+"_dlg.js";if(!tinymce.ScriptLoader.isDone(a)){document.write(''; + // Load the CSS by injecting them into the HTML this will reduce "flicker" + for (i = 0; i < t.contentCSS.length; i++) { + t.iframeHTML += ''; + } bi = s.body_id || 'tinymce'; if (bi.indexOf('=') != -1) { @@ -9594,33 +11610,35 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { bc = bc[t.id] || ''; } - t.iframeHTML += ''; + t.iframeHTML += '
    '; // Domain relaxing enabled, then set document domain - if (tinymce.relaxedDomain) { + if (tinymce.relaxedDomain && (isIE || (tinymce.isOpera && parseFloat(opera.version()) < 11))) { // We need to write the contents here in IE since multiple writes messes up refresh button and back button - if (isIE || (tinymce.isOpera && parseFloat(opera.version()) >= 9.5)) - u = 'javascript:(function(){document.open();document.domain="' + document.domain + '";var ed = window.parent.tinyMCE.get("' + t.id + '");document.write(ed.iframeHTML);document.close();ed.setupIframe();})()'; - else if (tinymce.isOpera) - u = 'javascript:(function(){document.open();document.domain="' + document.domain + '";document.close();ed.setupIframe();})()'; + u = 'javascript:(function(){document.open();document.domain="' + document.domain + '";var ed = window.parent.tinyMCE.get("' + t.id + '");document.write(ed.iframeHTML);document.close();ed.setupIframe();})()'; } // Create iframe - n = DOM.add(o.iframeContainer, 'iframe', { + // TODO: ACC add the appropriate description on this. + n = DOM.add(o.iframeContainer, 'iframe', { id : t.id + "_ifr", src : u || 'javascript:""', // Workaround for HTTPS warning in IE6/7 frameBorder : '0', + allowTransparency : "true", + title : s.aria_label, style : { width : '100%', - height : h + height : h, + display : 'block' // Important for Gecko to render the iframe correctly } }); t.contentAreaContainer = o.iframeContainer; DOM.get(o.editorContainer).style.display = t.orgDisplay; DOM.get(t.id).style.display = 'none'; + DOM.setAttrib(t.id, 'aria-hidden', true); - if (!isIE || !tinymce.relaxedDomain) + if (!tinymce.relaxedDomain || !u) t.setupIframe(); e = n = o = null; // Cleanup @@ -9634,30 +11652,21 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { d.open(); d.write(t.iframeHTML); d.close(); - } - // Design mode needs to be added here Ctrl+A will fail otherwise - if (!isIE) { - try { - if (!s.readonly) - d.designMode = 'On'; - } catch (ex) { - // Will fail on Gecko if the editor is placed in an hidden container element - // The design mode will be set ones the editor is focused - } + if (tinymce.relaxedDomain) + d.domain = tinymce.relaxedDomain; } - // IE needs to use contentEditable or it will display non secure items for HTTPS - if (isIE) { - // It will not steal focus if we hide it while setting contentEditable - b = t.getBody(); - DOM.hide(b); + // It will not steal focus while setting contentEditable + b = t.getBody(); + b.disabled = true; - if (!s.readonly) - b.contentEditable = true; + if (!s.readonly) + b.contentEditable = true; - DOM.show(b); - } + b.disabled = false; + + t.schema = new tinymce.html.Schema(s); t.dom = new tinymce.dom.DOMUtils(t.getDoc(), { keep_values : true, @@ -9667,16 +11676,85 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { class_filter : s.class_filter, update_styles : 1, fix_ie_paragraphs : 1, - valid_styles : s.valid_styles + schema : t.schema }); - t.schema = new tinymce.dom.Schema(); + t.parser = new tinymce.html.DomParser(s, t.schema); - t.serializer = new tinymce.dom.Serializer(extend(s, { - valid_elements : s.verify_html === false ? '*[*]' : s.valid_elements, - dom : t.dom, - schema : t.schema - })); + // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included. + if (!t.settings.allow_html_in_named_anchor) { + t.parser.addAttributeFilter('name', function(nodes, name) { + var i = nodes.length, sibling, prevSibling, parent, node; + + while (i--) { + node = nodes[i]; + if (node.name === 'a' && node.firstChild) { + parent = node.parent; + + // Move children after current node + sibling = node.lastChild; + do { + prevSibling = sibling.prev; + parent.insert(sibling, node); + sibling = prevSibling; + } while (sibling); + } + } + }); + } + + // Convert src and href into data-mce-src, data-mce-href and data-mce-style + t.parser.addAttributeFilter('src,href,style', function(nodes, name) { + var i = nodes.length, node, dom = t.dom, value, internalName; + + while (i--) { + node = nodes[i]; + value = node.attr(name); + internalName = 'data-mce-' + name; + + // Add internal attribute if we need to we don't on a refresh of the document + if (!node.attributes.map[internalName]) { + if (name === "style") + node.attr(internalName, dom.serializeStyle(dom.parseStyle(value), node.name)); + else + node.attr(internalName, t.convertURL(value, name, node.name)); + } + } + }); + + // Keep scripts from executing + t.parser.addNodeFilter('script', function(nodes, name) { + var i = nodes.length, node; + + while (i--) { + node = nodes[i]; + node.attr('type', 'mce-' + (node.attr('type') || 'text/javascript')); + } + }); + + t.parser.addNodeFilter('#cdata', function(nodes, name) { + var i = nodes.length, node; + + while (i--) { + node = nodes[i]; + node.type = 8; + node.name = '#comment'; + node.value = '[CDATA[' + node.value + ']]'; + } + }); + + t.parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', function(nodes, name) { + var i = nodes.length, node, nonEmptyElements = t.schema.getNonEmptyElements(); + + while (i--) { + node = nodes[i]; + + if (node.isEmpty(nonEmptyElements)) + node.empty().append(new tinymce.html.Node('br', 1)).shortEnded = true; + } + }); + + t.serializer = new tinymce.dom.Serializer(s, t.dom, t.schema); t.selection = new tinymce.dom.Selection(t.dom, t.getWin(), t.serializer); @@ -9686,18 +11764,18 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { t.formatter.register({ alignleft : [ {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'left'}}, - {selector : 'img,table', styles : {'float' : 'left'}} + {selector : 'img,table', collapsed : false, styles : {'float' : 'left'}} ], aligncenter : [ {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'center'}}, - {selector : 'img', styles : {display : 'block', marginLeft : 'auto', marginRight : 'auto'}}, - {selector : 'table', styles : {marginLeft : 'auto', marginRight : 'auto'}} + {selector : 'img', collapsed : false, styles : {display : 'block', marginLeft : 'auto', marginRight : 'auto'}}, + {selector : 'table', collapsed : false, styles : {marginLeft : 'auto', marginRight : 'auto'}} ], alignright : [ {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'right'}}, - {selector : 'img,table', styles : {'float' : 'right'}} + {selector : 'img,table', collapsed : false, styles : {'float' : 'right'}} ], alignfull : [ @@ -9705,33 +11783,47 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { ], bold : [ - {inline : 'strong'}, + {inline : 'strong', remove : 'all'}, {inline : 'span', styles : {fontWeight : 'bold'}}, - {inline : 'b'} + {inline : 'b', remove : 'all'} ], italic : [ - {inline : 'em'}, + {inline : 'em', remove : 'all'}, {inline : 'span', styles : {fontStyle : 'italic'}}, - {inline : 'i'} + {inline : 'i', remove : 'all'} ], underline : [ {inline : 'span', styles : {textDecoration : 'underline'}, exact : true}, - {inline : 'u'} + {inline : 'u', remove : 'all'} ], strikethrough : [ {inline : 'span', styles : {textDecoration : 'line-through'}, exact : true}, - {inline : 'u'} + {inline : 'strike', remove : 'all'} ], - forecolor : {inline : 'span', styles : {color : '%value'}}, - hilitecolor : {inline : 'span', styles : {backgroundColor : '%value'}}, + forecolor : {inline : 'span', styles : {color : '%value'}, wrap_links : false}, + hilitecolor : {inline : 'span', styles : {backgroundColor : '%value'}, wrap_links : false}, fontname : {inline : 'span', styles : {fontFamily : '%value'}}, fontsize : {inline : 'span', styles : {fontSize : '%value'}}, fontsize_class : {inline : 'span', attributes : {'class' : '%value'}}, blockquote : {block : 'blockquote', wrapper : 1, remove : 'all'}, + subscript : {inline : 'sub'}, + superscript : {inline : 'sup'}, + + link : {inline : 'a', selector : 'a', remove : 'all', split : true, deep : true, + onmatch : function(node) { + return true; + }, + + onformat : function(elm, fmt, vars) { + each(vars, function(value, key) { + t.dom.setAttrib(elm, key, value); + }); + } + }, removeformat : [ {selector : 'b,strong,em,i,font,u,strike', remove : 'all', split : true, expand : false, block_expand : true, deep : true}, @@ -9752,7 +11844,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Pass through t.undoManager.onAdd.add(function(um, l) { - if (!l.initial) + if (um.hasUndo()) return t.onChange.dispatch(t, l, um); }); @@ -9790,35 +11882,14 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { t.controlManager.onPostRender.dispatch(t, t.controlManager); t.onPostRender.dispatch(t); + t.quirks = new tinymce.util.Quirks(this); + if (s.directionality) t.getBody().dir = s.directionality; if (s.nowrap) t.getBody().style.whiteSpace = "nowrap"; - if (s.custom_elements) { - function handleCustom(ed, o) { - each(explode(s.custom_elements), function(v) { - var n; - - if (v.indexOf('~') === 0) { - v = v.substring(1); - n = 'span'; - } else - n = 'div'; - - o.content = o.content.replace(new RegExp('<(' + v + ')([^>]*)>', 'g'), '<' + n + ' _mce_name="$1"$2>'); - o.content = o.content.replace(new RegExp('', 'g'), ''); - }); - }; - - t.onBeforeSetContent.add(handleCustom); - t.onPostProcess.add(function(ed, o) { - if (o.set) - handleCustom(ed, o); - }); - } - if (s.handle_node_change_callback) { t.onNodeChange.add(function(ed, cm, n) { t.execCallback('handle_node_change_callback', t.id, n, -1, -1, true, t.selection.isCollapsed()); @@ -9840,16 +11911,22 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }); } - if (s.convert_newlines_to_brs) { + if (s.protect) { t.onBeforeSetContent.add(function(ed, o) { - if (o.initial) - o.content = o.content.replace(/\r?\n/g, '
    '); + if (s.protect) { + each(s.protect, function(pattern) { + o.content = o.content.replace(pattern, function(str) { + return ''; + }); + }); + } }); } - if (s.fix_nesting && isIE) { + if (s.convert_newlines_to_brs) { t.onBeforeSetContent.add(function(ed, o) { - o.content = t._fixNesting(o.content); + if (o.initial) + o.content = o.content.replace(/\r?\n/g, '
    '); }); } @@ -9946,7 +12023,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { var pn = n.parentNode; if (ed.dom.isBlock(pn) && pn.lastChild === n) - ed.dom.add(pn, 'br', {'_mce_bogus' : 1}); + ed.dom.add(pn, 'br', {'data-mce-bogus' : 1}); }); }; @@ -9956,72 +12033,62 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }); t.onSetContent.add(t.selection.onSetContent.add(fixLinks)); - - if (!s.readonly) { - try { - // Design mode must be set here once again to fix a bug where - // Ctrl+A/Delete/Backspace didn't work if the editor was added using mceAddControl then removed then added again - d.designMode = 'Off'; - d.designMode = 'On'; - } catch (ex) { - // Will fail on Gecko if the editor is placed in an hidden container element - // The design mode will be set ones the editor is focused - } - } } - // A small timeout was needed since firefox will remove. Bug: #1838304 - setTimeout(function () { - if (t.removed) - return; + t.load({initial : true, format : 'html'}); + t.startContent = t.getContent({format : 'raw'}); + t.undoManager.add(); + t.initialized = true; - t.load({initial : true, format : (s.cleanup_on_startup ? 'html' : 'raw')}); - t.startContent = t.getContent({format : 'raw'}); - t.initialized = true; + t.onInit.dispatch(t); + t.execCallback('setupcontent_callback', t.id, t.getBody(), t.getDoc()); + t.execCallback('init_instance_callback', t); + t.focus(true); + t.nodeChanged({initial : 1}); - t.onInit.dispatch(t); - t.execCallback('setupcontent_callback', t.id, t.getBody(), t.getDoc()); - t.execCallback('init_instance_callback', t); - t.focus(true); - t.nodeChanged({initial : 1}); + // Load specified content CSS last + each(t.contentCSS, function(u) { + t.dom.loadCSS(u); + }); - // Load specified content CSS last - if (s.content_css) { - tinymce.each(explode(s.content_css), function(u) { - t.dom.loadCSS(t.documentBaseURI.toAbsolute(u)); - }); - } + // Handle auto focus + if (s.auto_focus) { + setTimeout(function () { + var ed = tinymce.get(s.auto_focus); - // Handle auto focus - if (s.auto_focus) { - setTimeout(function () { - var ed = tinymce.get(s.auto_focus); + ed.selection.select(ed.getBody(), 1); + ed.selection.collapse(1); + ed.getBody().focus(); + ed.getWin().focus(); + }, 100); + } - ed.selection.select(ed.getBody(), 1); - ed.selection.collapse(1); - ed.getWin().focus(); - }, 100); - } - }, 1); - e = null; }, focus : function(sf) { - var oed, t = this, ce = t.settings.content_editable, ieRng, controlElm, doc = t.getDoc(); + var oed, t = this, selection = t.selection, ce = t.settings.content_editable, ieRng, controlElm, doc = t.getDoc(); if (!sf) { // Get selected control element - ieRng = t.selection.getRng(); + ieRng = selection.getRng(); if (ieRng.item) { controlElm = ieRng.item(0); } + t._refreshContentEditable(); + selection.normalize(); + // Is not content editable if (!ce) t.getWin().focus(); + // Focus the body as well since it's contentEditable + if (tinymce.isGecko) { + t.getBody().focus(); + } + // Restore selected control element // This is needed when for example an image is selected within a // layer a call to focus will then remove the control selection @@ -10106,7 +12173,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, nodeChanged : function(o) { - var t = this, s = t.selection, n = (isIE ? s.getNode() : s.getStart()) || t.getBody(); + var t = this, s = t.selection, n = s.getStart() || t.getBody(); // Fix for bug #1896577 it seems that this can not be fired while the editor is loading if (t.initialized) { @@ -10139,16 +12206,16 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { t.buttons[n] = s; }, - addCommand : function(n, f, s) { - this.execCommands[n] = {func : f, scope : s || this}; + addCommand : function(name, callback, scope) { + this.execCommands[name] = {func : callback, scope : scope || this}; }, - addQueryStateHandler : function(n, f, s) { - this.queryStateCommands[n] = {func : f, scope : s || this}; + addQueryStateHandler : function(name, callback, scope) { + this.queryStateCommands[name] = {func : callback, scope : scope || this}; }, - addQueryValueHandler : function(n, f, s) { - this.queryValueCommands[n] = {func : f, scope : s || this}; + addQueryValueHandler : function(name, callback, scope) { + this.queryValueCommands[name] = {func : callback, scope : scope || this}; }, addShortcut : function(pa, desc, cmd_func, sc) { @@ -10251,12 +12318,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { return true; } - // Execute global commands - if (tinymce.GlobalCommands.execCommand(t, cmd, ui, val)) { - t.onExecCommand.dispatch(t, cmd, ui, val, a); - return true; - } - // Editor commands if (t.editorCommands.execCommand(cmd, ui, val)) { t.onExecCommand.dispatch(t, cmd, ui, val, a); @@ -10388,7 +12449,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Add undo level will trigger onchange event if (!o.no_events) { - t.undoManager.typing = 0; + t.undoManager.typing = false; t.undoManager.add(); } @@ -10420,66 +12481,87 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { return h; }, - setContent : function(h, o) { - var t = this; + setContent : function(content, args) { + var self = this, rootNode, body = self.getBody(), forcedRootBlockName; - o = o || {}; - o.format = o.format || 'html'; - o.set = true; - o.content = h; + // Setup args object + args = args || {}; + args.format = args.format || 'html'; + args.set = true; + args.content = content; - if (!o.no_events) - t.onBeforeSetContent.dispatch(t, o); + // Do preprocessing + if (!args.no_events) + self.onBeforeSetContent.dispatch(self, args); + + content = args.content; // Padd empty content in Gecko and Safari. Commands will otherwise fail on the content // It will also be impossible to place the caret in the editor unless there is a BR element present - if (!tinymce.isIE && (h.length === 0 || /^\s+$/.test(h))) { - o.content = t.dom.setHTML(t.getBody(), '
    '); - o.format = 'raw'; - } + if (!tinymce.isIE && (content.length === 0 || /^\s+$/.test(content))) { + forcedRootBlockName = self.settings.forced_root_block; + if (forcedRootBlockName) + content = '<' + forcedRootBlockName + '>
    '; + else + content = '
    '; - o.content = t.dom.setHTML(t.getBody(), tinymce.trim(o.content)); + body.innerHTML = content; + self.selection.select(body, true); + self.selection.collapse(true); + return; + } - if (o.format != 'raw' && t.settings.cleanup) { - o.getInner = true; - o.content = t.dom.setHTML(t.getBody(), t.serializer.serialize(t.getBody(), o)); + // Parse and serialize the html + if (args.format !== 'raw') { + content = new tinymce.html.Serializer({}, self.schema).serialize( + self.parser.parse(content) + ); } - if (!o.no_events) - t.onSetContent.dispatch(t, o); + // Set the new cleaned contents to the editor + args.content = tinymce.trim(content); + self.dom.setHTML(body, args.content); + + // Do post processing + if (!args.no_events) + self.onSetContent.dispatch(self, args); - return o.content; + self.selection.normalize(); + + return args.content; }, - getContent : function(o) { - var t = this, h; + getContent : function(args) { + var self = this, content; - o = o || {}; - o.format = o.format || 'html'; - o.get = true; + // Setup args object + args = args || {}; + args.format = args.format || 'html'; + args.get = true; - if (!o.no_events) - t.onBeforeGetContent.dispatch(t, o); + // Do preprocessing + if (!args.no_events) + self.onBeforeGetContent.dispatch(self, args); - if (o.format != 'raw' && t.settings.cleanup) { - o.getInner = true; - h = t.serializer.serialize(t.getBody(), o); - } else - h = t.getBody().innerHTML; + // Get raw contents or by default the cleaned contents + if (args.format == 'raw') + content = self.getBody().innerHTML; + else + content = self.serializer.serialize(self.getBody(), args); - h = h.replace(/^\s*|\s*$/g, ''); - o.content = h; + args.content = tinymce.trim(content); - if (!o.no_events) - t.onGetContent.dispatch(t, o); + // Do post processing + if (!args.no_events) + self.onGetContent.dispatch(self, args); - return o.content; + return args.content; }, isDirty : function() { - var t = this; + var self = this; - return tinymce.trim(t.startContent) != tinymce.trim(t.getContent({format : 'raw', no_events : 1})) && !t.isNotDirty; + return tinymce.trim(self.startContent) != tinymce.trim(self.getContent({format : 'raw', no_events : 1})) && !self.isNotDirty; }, getContainer : function() { @@ -10657,7 +12739,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { _addEvents : function() { // 'focus', 'blur', 'dblclick', 'beforedeactivate', submit, reset - var t = this, i, s = t.settings, lo = { + var t = this, i, s = t.settings, dom = t.dom, lo = { mouseup : 'onMouseUp', mousedown : 'onMouseDown', click : 'onClick', @@ -10689,35 +12771,26 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { each(lo, function(v, k) { switch (k) { case 'contextmenu': - if (tinymce.isOpera) { - // Fake contextmenu on Opera - t.dom.bind(t.getBody(), 'mousedown', function(e) { - if (e.ctrlKey) { - e.fakeType = 'contextmenu'; - eventHandler(e); - } - }); - } else - t.dom.bind(t.getBody(), k, eventHandler); + dom.bind(t.getDoc(), k, eventHandler); break; case 'paste': - t.dom.bind(t.getBody(), k, function(e) { + dom.bind(t.getBody(), k, function(e) { eventHandler(e); }); break; case 'submit': case 'reset': - t.dom.bind(t.getElement().form || DOM.getParent(t.id, 'form'), k, eventHandler); + dom.bind(t.getElement().form || DOM.getParent(t.id, 'form'), k, eventHandler); break; default: - t.dom.bind(s.content_editable ? t.getBody() : t.getDoc(), k, eventHandler); + dom.bind(s.content_editable ? t.getBody() : t.getDoc(), k, eventHandler); } }); - t.dom.bind(s.content_editable ? t.getBody() : (isGecko ? t.getDoc() : t.getWin()), 'focus', function(e) { + dom.bind(s.content_editable ? t.getBody() : (isGecko ? t.getDoc() : t.getWin()), 'focus', function(e) { t.focus(true); }); @@ -10725,22 +12798,12 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Fixes bug where a specified document_base_uri could result in broken images // This will also fix drag drop of images in Gecko if (tinymce.isGecko) { - // Convert all images to absolute URLs -/* t.onSetContent.add(function(ed, o) { - each(ed.dom.select('img'), function(e) { - var v; - - if (v = e.getAttribute('_mce_src')) - e.src = t.documentBaseURI.toAbsolute(v); - }) - });*/ - - t.dom.bind(t.getDoc(), 'DOMNodeInserted', function(e) { + dom.bind(t.getDoc(), 'DOMNodeInserted', function(e) { var v; e = e.target; - if (e.nodeType === 1 && e.nodeName === 'IMG' && (v = e.getAttribute('_mce_src'))) + if (e.nodeType === 1 && e.nodeName === 'IMG' && (v = e.getAttribute('data-mce-src'))) e.src = t.documentBaseURI.toAbsolute(v); }); } @@ -10751,14 +12814,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { var t = this, d = t.getDoc(), s = t.settings; if (isGecko && !s.readonly) { - if (t._isHidden()) { - try { - if (!s.content_editable) - d.designMode = 'On'; - } catch (ex) { - // Fails if it's hidden - } - } + t._refreshContentEditable(); try { // Try new Gecko method @@ -10781,19 +12837,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { t.onMouseDown.add(setOpts); } - // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250 - // WebKit can't even do simple things like selecting an image - // This also fixes so it's possible to select mceItemAnchors - if (tinymce.isWebKit) { - t.onClick.add(function(ed, e) { - e = e.target; - - // Needs tobe the setBaseAndExtend or it will fail to select floated images - if (e.nodeName == 'IMG' || (e.nodeName == 'A' && t.dom.hasClass(e, 'mceItemAnchor'))) - t.selection.getSel().setBaseAndExtent(e, 0, e, 1); - }); - } - // Add node change handlers t.onMouseUp.add(t.nodeChanged); //t.onClick.add(t.nodeChanged); @@ -10804,6 +12847,35 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { t.nodeChanged(); }); + + // Add block quote deletion handler + t.onKeyDown.add(function(ed, e) { + // Was the BACKSPACE key pressed? + if (e.keyCode != 8) + return; + + var n = ed.selection.getRng().startContainer; + var offset = ed.selection.getRng().startOffset; + + while (n && n.nodeType && n.nodeType != 1 && n.parentNode) + n = n.parentNode; + + // Is the cursor at the beginning of a blockquote? + if (n && n.parentNode && n.parentNode.tagName === 'BLOCKQUOTE' && n.parentNode.firstChild == n && offset == 0) { + // Remove the blockquote + ed.formatter.toggle('blockquote', null, n.parentNode); + + // Move the caret to the beginning of n + var rng = ed.selection.getRng(); + rng.setStart(n, 0); + rng.setEnd(n, 0); + ed.selection.setRng(rng); + ed.selection.collapse(false); + } + }); + + + // Add reset handler t.onReset.add(function() { t.setContent(t.startContent, {format : 'raw'}); @@ -10825,9 +12897,9 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { for (i=1; i<=6; i++) t.addShortcut('ctrl+' + i, '', ['FormatBlock', false, 'h' + i]); - t.addShortcut('ctrl+7', '', ['FormatBlock', false, '

    ']); - t.addShortcut('ctrl+8', '', ['FormatBlock', false, '

    ']); - t.addShortcut('ctrl+9', '', ['FormatBlock', false, '
    ']); + t.addShortcut('ctrl+7', '', ['FormatBlock', false, 'p']); + t.addShortcut('ctrl+8', '', ['FormatBlock', false, 'div']); + t.addShortcut('ctrl+9', '', ['FormatBlock', false, 'address']); function find(e) { var v = null; @@ -10883,7 +12955,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (tinymce.isIE) { // Fix so resize will only update the width and height attributes not the styles of an image // It will also block mceItemNoResize items - t.dom.bind(t.getDoc(), 'controlselect', function(e) { + dom.bind(t.getDoc(), 'controlselect', function(e) { var re = t.resizeInfo, cb; e = e.target; @@ -10893,28 +12965,28 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { return; if (re) - t.dom.unbind(re.node, re.ev, re.cb); + dom.unbind(re.node, re.ev, re.cb); - if (!t.dom.hasClass(e, 'mceItemNoResize')) { + if (!dom.hasClass(e, 'mceItemNoResize')) { ev = 'resizeend'; - cb = t.dom.bind(e, ev, function(e) { + cb = dom.bind(e, ev, function(e) { var v; e = e.target; - if (v = t.dom.getStyle(e, 'width')) { - t.dom.setAttrib(e, 'width', v.replace(/[^0-9%]+/g, '')); - t.dom.setStyle(e, 'width', ''); + if (v = dom.getStyle(e, 'width')) { + dom.setAttrib(e, 'width', v.replace(/[^0-9%]+/g, '')); + dom.setStyle(e, 'width', ''); } - if (v = t.dom.getStyle(e, 'height')) { - t.dom.setAttrib(e, 'height', v.replace(/[^0-9%]+/g, '')); - t.dom.setStyle(e, 'height', ''); + if (v = dom.getStyle(e, 'height')) { + dom.setAttrib(e, 'height', v.replace(/[^0-9%]+/g, '')); + dom.setStyle(e, 'height', ''); } }); } else { ev = 'resizestart'; - cb = t.dom.bind(e, 'resizestart', Event.cancel, Event); + cb = dom.bind(e, 'resizestart', Event.cancel, Event); } re = t.resizeInfo = { @@ -10923,27 +12995,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { cb : cb }; }); - - t.onKeyDown.add(function(ed, e) { - switch (e.keyCode) { - case 8: - // Fix IE control + backspace browser bug - if (t.selection.getRng().item) { - ed.dom.remove(t.selection.getRng().item(0)); - return Event.cancel(e); - } - } - }); - - /*if (t.dom.boxModel) { - t.getBody().style.height = '100%'; - - Event.add(t.getWin(), 'resize', function(e) { - var docElm = t.getDoc().documentElement; - - docElm.style.height = (docElm.offsetHeight - 10) + 'px'; - }); - }*/ } if (tinymce.isOpera) { @@ -10955,81 +13006,61 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Add custom undo/redo handlers if (s.custom_undo_redo) { function addUndo() { - t.undoManager.typing = 0; + t.undoManager.typing = false; t.undoManager.add(); }; - t.dom.bind(t.getDoc(), 'focusout', function(e) { + dom.bind(t.getDoc(), 'focusout', function(e) { if (!t.removed && t.undoManager.typing) addUndo(); }); + // Add undo level when contents is drag/dropped within the editor + t.dom.bind(t.dom.getRoot(), 'dragend', function(e) { + addUndo(); + }); + t.onKeyUp.add(function(ed, e) { - if ((e.keyCode >= 33 && e.keyCode <= 36) || (e.keyCode >= 37 && e.keyCode <= 40) || e.keyCode == 13 || e.keyCode == 45 || e.ctrlKey) + var keyCode = e.keyCode; + + if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 13 || keyCode == 45 || e.ctrlKey) addUndo(); }); t.onKeyDown.add(function(ed, e) { - var rng, tmpRng, parent, offset; - - // IE has a really odd bug where the DOM might include an node that doesn't have - // a proper structure. If you try to access nodeValue it would throw an illegal value exception. - // This seems to only happen when you delete contents and it seems to be avoidable if you refresh the element - // after you delete contents from it. See: #3008923 - if (isIE && e.keyCode == 46) { - rng = t.selection.getRng(); - - if (rng.parentElement) { - parent = rng.parentElement(); - - // Get the current caret position within the element - tmpRng = rng.duplicate(); - tmpRng.moveToElementText(parent); - tmpRng.setEndPoint('EndToEnd', rng); - offset = tmpRng.text.length; - - // Select next word when ctrl key is used in combo with delete - if (e.ctrlKey) { - rng.moveEnd('word', 1); - rng.select(); - } + var keyCode = e.keyCode, sel; - // Delete contents - t.selection.getSel().clear(); + if (keyCode == 8) { + sel = t.getDoc().selection; - // Check if we are within the same parent - if (rng.parentElement() == parent) { - try { - // Update the HTML and hopefully it will remove the artifacts - parent.innerHTML = parent.innerHTML; - } catch (ex) { - // And since it's IE it can sometimes produce an unknown runtime error - } - - // Restore the caret position - tmpRng.moveToElementText(parent); - tmpRng.collapse(); - tmpRng.move('character', offset); - tmpRng.select(); - } + // Fix IE control + backspace browser bug + if (sel && sel.createRange && sel.createRange().item) { + t.undoManager.beforeChange(); + ed.dom.remove(sel.createRange().item(0)); + addUndo(); - // Block the default delete behavior since it might be broken - e.preventDefault(); - return; + return Event.cancel(e); } } - // Is caracter positon keys - if ((e.keyCode >= 33 && e.keyCode <= 36) || (e.keyCode >= 37 && e.keyCode <= 40) || e.keyCode == 13 || e.keyCode == 45) { + // Is caracter positon keys left,right,up,down,home,end,pgdown,pgup,enter + if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 13 || keyCode == 45) { + // Add position before enter key is pressed, used by IE since it still uses the default browser behavior + // Todo: Remove this once we normalize enter behavior on IE + if (tinymce.isIE && keyCode == 13) + t.undoManager.beforeChange(); + if (t.undoManager.typing) addUndo(); return; } - if (!t.undoManager.typing) { + // If key isn't shift,ctrl,alt,capslock,metakey + if ((keyCode < 16 || keyCode > 20) && keyCode != 224 && keyCode != 91 && !t.undoManager.typing) { + t.undoManager.beforeChange(); + t.undoManager.typing = true; t.undoManager.add(); - t.undoManager.typing = 1; } }); @@ -11038,68 +13069,83 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { addUndo(); }); } - }, - _isHidden : function() { - var s; + // Bug fix for FireFox keeping styles from end of selection instead of start. + if (tinymce.isGecko) { + function getAttributeApplyFunction() { + var template = t.dom.getAttribs(t.selection.getStart().cloneNode(false)); - if (!isGecko) - return 0; + return function() { + var target = t.selection.getStart(); - // Weird, wheres that cursor selection? - s = this.selection.getSel(); - return (!s || !s.rangeCount || s.rangeCount == 0); - }, + if (target !== t.getBody()) { + t.dom.setAttrib(target, "style", null); - // Fix for bug #1867292 - _fixNesting : function(s) { - var d = [], i; + each(template, function(attr) { + target.setAttributeNode(attr.cloneNode(true)); + }); + } + }; + } - s = s.replace(/<(\/)?([^\s>]+)[^>]*?>/g, function(a, b, c) { - var e; + function isSelectionAcrossElements() { + var s = t.selection; - // Handle end element - if (b === '/') { - if (!d.length) - return ''; + return !s.isCollapsed() && s.getStart() != s.getEnd(); + } - if (c !== d[d.length - 1].tag) { - for (i=d.length - 1; i>=0; i--) { - if (d[i].tag === c) { - d[i].close = 1; - break; - } - } + t.onKeyPress.add(function(ed, e) { + var applyAttributes; - return ''; - } else { - d.pop(); + if ((e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) { + applyAttributes = getAttributeApplyFunction(); + t.getDoc().execCommand('delete', false, null); + applyAttributes(); - if (d.length && d[d.length - 1].close) { - a = a + ''; - d.pop(); - } + return Event.cancel(e); } - } else { - // Ignore these - if (/^(br|hr|input|meta|img|link|param)$/i.test(c)) - return a; + }); - // Ignore closed ones - if (/\/>$/.test(a)) - return a; + t.dom.bind(t.getDoc(), 'cut', function(e) { + var applyAttributes; - d.push({tag : c}); // Push start element - } + if (isSelectionAcrossElements()) { + applyAttributes = getAttributeApplyFunction(); + t.onKeyUp.addToTop(Event.cancel, Event); - return a; - }); + setTimeout(function() { + applyAttributes(); + t.onKeyUp.remove(Event.cancel, Event); + }, 0); + } + }); + } + }, - // End all open tags - for (i=d.length - 1; i>=0; i--) - s += ''; + _refreshContentEditable : function() { + var self = this, body, parent; - return s; + // Check if the editor was hidden and the re-initalize contentEditable mode by removing and adding the body again + if (self._isHidden()) { + body = self.getBody(); + parent = body.parentNode; + + parent.removeChild(body); + parent.appendChild(body); + + body.focus(); + } + }, + + _isHidden : function() { + var s; + + if (!isGecko) + return 0; + + // Weird, wheres that cursor selection? + s = this.selection.getSel(); + return (!s || !s.rangeCount || s.rangeCount == 0); } }); })(tinymce); @@ -11113,6 +13159,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { selection = editor.selection, commands = {state: {}, exec : {}, value : {}}, settings = editor.settings, + formatter = editor.formatter, bookmark; function execCommand(command, ui, value) { @@ -11178,11 +13225,11 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }; function isFormatMatch(name) { - return editor.formatter.match(name); + return formatter.match(name); }; function toggleFormat(name, value) { - editor.formatter.toggle(name, value ? {value : value} : undefined); + formatter.toggle(name, value ? {value : value} : undefined); }; function storeSelection(type) { @@ -11242,10 +13289,11 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Remove all other alignments first each('left,center,right,full'.split(','), function(name) { if (align != name) - editor.formatter.remove('align' + name); + formatter.remove('align' + name); }); toggleFormat('align' + align); + execCommand('mceRepaint'); }, // Override list commands to fix WebKit bug @@ -11271,7 +13319,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, // Override commands to use the text formatter engine - 'Bold,Italic,Underline,Strikethrough' : function(command) { + 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) { toggleFormat(command); }, @@ -11298,7 +13346,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, RemoveFormat : function(command) { - editor.formatter.remove(command); + formatter.remove(command); }, mceBlockQuote : function(command) { @@ -11306,7 +13354,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, FormatBlock : function(command, ui, value) { - return toggleFormat(value); + return toggleFormat(value || 'p'); }, mceCleanup : function() { @@ -11331,25 +13379,151 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { mceSelectNodeDepth : function(command, ui, value) { var counter = 0; - dom.getParent(selection.getNode(), function(node) { - if (node.nodeType == 1 && counter++ == value) { - selection.select(node); - return FALSE; - } - }, editor.getBody()); - }, + dom.getParent(selection.getNode(), function(node) { + if (node.nodeType == 1 && counter++ == value) { + selection.select(node); + return FALSE; + } + }, editor.getBody()); + }, + + mceSelectNode : function(command, ui, value) { + selection.select(value); + }, + + mceInsertContent : function(command, ui, value) { + var parser, serializer, parentNode, rootNode, fragment, args, + marker, nodeRect, viewPortRect, rng, node, node2, bookmarkHtml, viewportBodyElement; + + // Setup parser and serializer + parser = editor.parser; + serializer = new tinymce.html.Serializer({}, editor.schema); + bookmarkHtml = '\uFEFF'; + + // Run beforeSetContent handlers on the HTML to be inserted + args = {content: value, format: 'html'}; + selection.onBeforeSetContent.dispatch(selection, args); + value = args.content; + + // Add caret at end of contents if it's missing + if (value.indexOf('{$caret}') == -1) + value += '{$caret}'; + + // Replace the caret marker with a span bookmark element + value = value.replace(/\{\$caret\}/, bookmarkHtml); + + // Insert node maker where we will insert the new HTML and get it's parent + if (!selection.isCollapsed()) + editor.getDoc().execCommand('Delete', false, null); + + parentNode = selection.getNode(); + + // Parse the fragment within the context of the parent node + args = {context : parentNode.nodeName.toLowerCase()}; + fragment = parser.parse(value, args); + + // Move the caret to a more suitable location + node = fragment.lastChild; + if (node.attr('id') == 'mce_marker') { + marker = node; + + for (node = node.prev; node; node = node.walk(true)) { + if (node.type == 3 || !dom.isBlock(node.name)) { + node.parent.insert(marker, node, node.name === 'br'); + break; + } + } + } + + // If parser says valid we can insert the contents into that parent + if (!args.invalid) { + value = serializer.serialize(fragment); + + // Check if parent is empty or only has one BR element then set the innerHTML of that parent + node = parentNode.firstChild; + node2 = parentNode.lastChild; + if (!node || (node === node2 && node.nodeName === 'BR')) + dom.setHTML(parentNode, value); + else + selection.setContent(value); + } else { + // If the fragment was invalid within that context then we need + // to parse and process the parent it's inserted into + + // Insert bookmark node and get the parent + selection.setContent(bookmarkHtml); + parentNode = editor.selection.getNode(); + rootNode = editor.getBody(); + + // Opera will return the document node when selection is in root + if (parentNode.nodeType == 9) + parentNode = node = rootNode; + else + node = parentNode; + + // Find the ancestor just before the root element + while (node !== rootNode) { + parentNode = node; + node = node.parentNode; + } + + // Get the outer/inner HTML depending on if we are in the root and parser and serialize that + value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode); + value = serializer.serialize( + parser.parse( + // Need to replace by using a function since $ in the contents would otherwise be a problem + value.replace(//i, function() { + return serializer.serialize(fragment); + }) + ) + ); + + // Set the inner/outer HTML depending on if we are in the root or not + if (parentNode == rootNode) + dom.setHTML(rootNode, value); + else + dom.setOuterHTML(parentNode, value); + } + + marker = dom.get('mce_marker'); + + // Scroll range into view scrollIntoView on element can't be used since it will scroll the main view port as well + nodeRect = dom.getRect(marker); + viewPortRect = dom.getViewPort(editor.getWin()); + + // Check if node is out side the viewport if it is then scroll to it + if ((nodeRect.y + nodeRect.h > viewPortRect.y + viewPortRect.h || nodeRect.y < viewPortRect.y) || + (nodeRect.x > viewPortRect.x + viewPortRect.w || nodeRect.x < viewPortRect.x)) { + viewportBodyElement = tinymce.isIE ? editor.getDoc().documentElement : editor.getBody(); + viewportBodyElement.scrollLeft = nodeRect.x; + viewportBodyElement.scrollTop = nodeRect.y - viewPortRect.h + 25; + } + + // Move selection before marker and remove it + rng = dom.createRng(); - mceSelectNode : function(command, ui, value) { - selection.select(value); - }, + // If previous sibling is a text node set the selection to the end of that node + node = marker.previousSibling; + if (node && node.nodeType == 3) { + rng.setStart(node, node.nodeValue.length); + } else { + // If the previous sibling isn't a text node or doesn't exist set the selection before the marker node + rng.setStartBefore(marker); + rng.setEndBefore(marker); + } - mceInsertContent : function(command, ui, value) { - selection.setContent(value); + // Remove the marker node and set the new range + dom.remove(marker); + selection.setRng(rng); + + // Dispatch after event and add any visual elements needed + selection.onSetContent.dispatch(selection, args); + editor.addVisual(); }, mceInsertRawHTML : function(command, ui, value) { selection.setContent('tiny_mce_marker'); - editor.setContent(editor.getContent().replace(/tiny_mce_marker/g, value)); + editor.setContent(editor.getContent().replace(/tiny_mce_marker/g, function() { return value })); }, mceSetContent : function(command, ui, value) { @@ -11395,11 +13569,11 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, mceToggleFormat : function(command, ui, value) { - editor.formatter.toggle(value); + formatter.toggle(value); }, InsertHorizontalRule : function() { - selection.setContent('
    '); + editor.execCommand('mceInsertContent', false, '
    '); }, mceToggleVisualAid : function() { @@ -11408,33 +13582,37 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, mceReplaceContent : function(command, ui, value) { - selection.setContent(value.replace(/\{\$selection\}/g, selection.getContent({format : 'text'}))); + editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({format : 'text'}))); }, mceInsertLink : function(command, ui, value) { - var link = dom.getParent(selection.getNode(), 'a'); + var anchor; - if (tinymce.is(value, 'string')) + if (typeof(value) == 'string') value = {href : value}; - if (!link) { - execNativeCommand('CreateLink', FALSE, 'javascript:mctmp(0);'); - each(dom.select('a[href=javascript:mctmp(0);]'), function(link) { - dom.setAttribs(link, value); - }); - } else { - if (value.href) - dom.setAttribs(link, value); - else - editor.dom.remove(link, TRUE); + anchor = dom.getParent(selection.getNode(), 'a'); + + // Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here. + value.href = value.href.replace(' ', '%20'); + + // Remove existing links if there could be child links or that the href isn't specified + if (!anchor || !value.href) { + formatter.remove('link'); + } + + // Apply new link to selection + if (value.href) { + formatter.apply('link', value, anchor); } }, - + selectAll : function() { - var root = dom.getRoot(); - var rng = dom.createRng(); + var root = dom.getRoot(), rng = dom.createRng(); + rng.setStart(root, 0); rng.setEnd(root, root.childNodes.length); + editor.selection.setRng(rng); } }); @@ -11446,7 +13624,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { return isFormatMatch('align' + command.substring(7)); }, - 'Bold,Italic,Underline,Strikethrough' : function(command) { + 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) { return isFormatMatch(command); }, @@ -11503,23 +13681,30 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } }; })(tinymce); + (function(tinymce) { var Dispatcher = tinymce.util.Dispatcher; tinymce.UndoManager = function(editor) { - var self, index = 0, data = []; + var self, index = 0, data = [], beforeBookmark; function getContent() { return tinymce.trim(editor.getContent({format : 'raw', no_events : 1})); }; return self = { - typing : 0, + typing : false, onAdd : new Dispatcher(self), + onUndo : new Dispatcher(self), + onRedo : new Dispatcher(self), + beforeChange : function() { + beforeBookmark = editor.selection.getBookmark(2, true); + }, + add : function(level) { var i, settings = editor.settings, lastLevel; @@ -11528,10 +13713,12 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Add undo level if needed lastLevel = data[index]; - if (lastLevel && lastLevel.content == level.content) { - if (index > 0 || data.length == 1) - return null; - } + if (lastLevel && lastLevel.content == level.content) + return null; + + // Set before bookmark on previous level + if (data[index]) + data[index].beforeBookmark = beforeBookmark; // Time to compress if (settings.custom_undo_redo_levels) { @@ -11548,13 +13735,8 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { level.bookmark = editor.selection.getBookmark(2, true); // Crop array if needed - if (index < data.length - 1) { - // Treat first level as initial - if (index == 0) - data = []; - else - data.length = index + 1; - } + if (index < data.length - 1) + data.length = index + 1; data.push(level); index = data.length - 1; @@ -11570,14 +13752,14 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (self.typing) { self.add(); - self.typing = 0; + self.typing = false; } if (index > 0) { level = data[--index]; editor.setContent(level.content, {format : 'raw'}); - editor.selection.moveToBookmark(level.bookmark); + editor.selection.moveToBookmark(level.beforeBookmark); self.onUndo.dispatch(self, level); } @@ -11602,15 +13784,16 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { clear : function() { data = []; - index = self.typing = 0; + index = 0; + self.typing = false; }, hasUndo : function() { - return index > 0 || self.typing; + return index > 0 || this.typing; }, hasRedo : function() { - return index < data.length - 1; + return index < data.length - 1 && !this.typing; } }; }; @@ -11659,24 +13842,15 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { return rng2.cloneContents().textContent.length == 0; }; - function isEmpty(n) { - n = n.innerHTML; - - n = n.replace(/<(img|hr|table|input|select|textarea)[ \>]/gi, '-'); // Keep these convert them to - chars - n = n.replace(/<[^>]+>/g, ''); // Remove all tags - - return n.replace(/[ \u00a0\t\r\n]+/g, '') == ''; - }; - function splitList(selection, dom, li) { var listBlock, block; - if (isEmpty(li)) { + if (dom.isEmpty(li)) { listBlock = dom.getParent(li, 'ul,ol'); if (!dom.getParent(listBlock.parentNode, 'ul,ol')) { dom.split(listBlock, li); - block = dom.create('p', 0, '
    '); + block = dom.create('p', 0, '
    '); dom.replace(block, li); selection.select(block, 1); } @@ -11697,45 +13871,94 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { s.element = elm.toUpperCase(); ed.onPreInit.add(t.setup, t); + }, - t.reOpera = new RegExp('(\\u00a0| | )<\/' + elm + '>', 'gi'); - t.rePadd = new RegExp(']+)><\\\/p>|]+)\\\/>|]+)>\\s+<\\\/p>|

    <\\\/p>||

    \\s+<\\\/p>'.replace(/p/g, elm), 'gi'); - t.reNbsp2BR1 = new RegExp(']+)>[\\s\\u00a0]+<\\\/p>|

    [\\s\\u00a0]+<\\\/p>'.replace(/p/g, elm), 'gi'); - t.reNbsp2BR2 = new RegExp('<%p()([^>]+)>( | )<\\\/%p>|<%p>( | )<\\\/%p>'.replace(/%p/g, elm), 'gi'); - t.reBR2Nbsp = new RegExp(']+)>\\s*
    \\s*<\\\/p>|

    \\s*
    \\s*<\\\/p>'.replace(/p/g, elm), 'gi'); + setup : function() { + var t = this, ed = t.editor, s = ed.settings, dom = ed.dom, selection = ed.selection, blockElements = ed.schema.getBlockElements(); - function padd(ed, o) { - if (isOpera) - o.content = o.content.replace(t.reOpera, ''); + // Force root blocks + if (s.forced_root_block) { + function addRootBlocks() { + var node = selection.getStart(), rootNode = ed.getBody(), rng, startContainer, startOffset, endContainer, endOffset, rootBlockNode, tempNode, offset = -0xFFFFFF; - o.content = o.content.replace(t.rePadd, '<' + elm + '$1$2$3$4$5$6>\u00a0'); + if (!node || node.nodeType !== 1) + return; - if (!isIE && !isOpera && o.set) { - // Use   instead of BR in padded paragraphs - o.content = o.content.replace(t.reNbsp2BR1, '<' + elm + '$1$2>
    '); - o.content = o.content.replace(t.reNbsp2BR2, '<' + elm + '$1$2>
    '); - } else - o.content = o.content.replace(t.reBR2Nbsp, '<' + elm + '$1$2>\u00a0'); - }; + // Check if node is wrapped in block + while (node != rootNode) { + if (blockElements[node.nodeName]) + return; - ed.onBeforeSetContent.add(padd); - ed.onPostProcess.add(padd); + node = node.parentNode; + } - if (s.forced_root_block) { - ed.onInit.add(t.forceRoots, t); - ed.onSetContent.add(t.forceRoots, t); - ed.onBeforeGetContent.add(t.forceRoots, t); - } - }, + // Get current selection + rng = selection.getRng(); + if (rng.setStart) { + startContainer = rng.startContainer; + startOffset = rng.startOffset; + endContainer = rng.endContainer; + endOffset = rng.endOffset; + } else { + // Force control range into text range + if (rng.item) { + rng = ed.getDoc().body.createTextRange(); + rng.moveToElementText(rng.item(0)); + } - setup : function() { - var t = this, ed = t.editor, s = ed.settings, dom = ed.dom, selection = ed.selection; + tmpRng = rng.duplicate(); + tmpRng.collapse(true); + startOffset = tmpRng.move('character', offset) * -1; - // Force root blocks when typing and when getting output - if (s.forced_root_block) { - ed.onBeforeExecCommand.add(t.forceRoots, t); - ed.onKeyUp.add(t.forceRoots, t); - ed.onPreProcess.add(t.forceRoots, t); + if (!tmpRng.collapsed) { + tmpRng = rng.duplicate(); + tmpRng.collapse(false); + endOffset = (tmpRng.move('character', offset) * -1) - startOffset; + } + } + + // Wrap non block elements and text nodes + for (node = rootNode.firstChild; node; node) { + if (node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName])) { + if (!rootBlockNode) { + rootBlockNode = dom.create(s.forced_root_block); + node.parentNode.insertBefore(rootBlockNode, node); + } + + tempNode = node; + node = node.nextSibling; + rootBlockNode.appendChild(tempNode); + } else { + rootBlockNode = null; + node = node.nextSibling; + } + } + + if (rng.setStart) { + rng.setStart(startContainer, startOffset); + rng.setEnd(endContainer, endOffset); + selection.setRng(rng); + } else { + try { + rng = ed.getDoc().body.createTextRange(); + rng.moveToElementText(rootNode); + rng.collapse(true); + rng.moveStart('character', startOffset); + + if (endOffset > 0) + rng.moveEnd('character', endOffset); + + rng.select(); + } catch (ex) { + // Ignore + } + } + + ed.nodeChanged(); + }; + + ed.onKeyUp.add(addRootBlocks); + ed.onClick.add(addRootBlocks); } if (s.force_br_newlines) { @@ -11785,12 +14008,12 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { var parent = ed.selection.getStart(), fmt = t._previousFormats; // Parent is an empty block - if (!parent.hasChildNodes()) { + if (!parent.hasChildNodes() && fmt) { parent = dom.getParent(parent, dom.isBlock); - if (parent) { + if (parent && parent.nodeName != 'LI') { parent.innerHTML = ''; - + if (t._previousFormats) { parent.appendChild(fmt.wrapper); fmt.inner.innerHTML = '\uFEFF'; @@ -11798,7 +14021,9 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { parent.innerHTML = '\uFEFF'; selection.select(parent, 1); + selection.collapse(true); ed.getDoc().execCommand('Delete', false, null); + t._previousFormats = 0; } } } @@ -11851,21 +14076,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }); } - // Padd empty inline elements within block elements - // For example:

    becomes

     

    - ed.onPreProcess.add(function(ed, o) { - each(dom.select('p,h1,h2,h3,h4,h5,h6,div', o.node), function(p) { - if (isEmpty(p)) { - each(dom.select('span,em,strong,b,i', o.node), function(n) { - if (!n.hasChildNodes()) { - n.appendChild(ed.getDoc().createTextNode('\u00a0')); - return FALSE; // Break the loop one padding is enough - } - }); - } - }); - }); - // IE specific fixes if (isIE) { // Replaces IE:s auto generated paragraphs with the specified element name @@ -11895,155 +14105,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } }, - find : function(n, t, s) { - var ed = this.editor, w = ed.getDoc().createTreeWalker(n, 4, null, FALSE), c = -1; - - while (n = w.nextNode()) { - c++; - - // Index by node - if (t == 0 && n == s) - return c; - - // Node by index - if (t == 1 && c == s) - return n; - } - - return -1; - }, - - forceRoots : function(ed, e) { - var t = this, ed = t.editor, b = ed.getBody(), d = ed.getDoc(), se = ed.selection, s = se.getSel(), r = se.getRng(), si = -2, ei, so, eo, tr, c = -0xFFFFFF; - var nx, bl, bp, sp, le, nl = b.childNodes, i, n, eid; - - // Fix for bug #1863847 - //if (e && e.keyCode == 13) - // return TRUE; - - // Wrap non blocks into blocks - for (i = nl.length - 1; i >= 0; i--) { - nx = nl[i]; - - // Ignore internal elements - if (nx.nodeType === 1 && nx.getAttribute('_mce_type')) { - bl = null; - continue; - } - - // Is text or non block element - if (nx.nodeType === 3 || (!t.dom.isBlock(nx) && nx.nodeType !== 8 && !/^(script|mce:script|style|mce:style)$/i.test(nx.nodeName))) { - if (!bl) { - // Create new block but ignore whitespace - if (nx.nodeType != 3 || /[^\s]/g.test(nx.nodeValue)) { - // Store selection - if (si == -2 && r) { - if (!isIE) { - // If selection is element then mark it - if (r.startContainer.nodeType == 1 && (n = r.startContainer.childNodes[r.startOffset]) && n.nodeType == 1) { - // Save the id of the selected element - eid = n.getAttribute("id"); - n.setAttribute("id", "__mce"); - } else { - // If element is inside body, might not be the case in contentEdiable mode - if (ed.dom.getParent(r.startContainer, function(e) {return e === b;})) { - so = r.startOffset; - eo = r.endOffset; - si = t.find(b, 0, r.startContainer); - ei = t.find(b, 0, r.endContainer); - } - } - } else { - // Force control range into text range - if (r.item) { - tr = d.body.createTextRange(); - tr.moveToElementText(r.item(0)); - r = tr; - } - - tr = d.body.createTextRange(); - tr.moveToElementText(b); - tr.collapse(1); - bp = tr.move('character', c) * -1; - - tr = r.duplicate(); - tr.collapse(1); - sp = tr.move('character', c) * -1; - - tr = r.duplicate(); - tr.collapse(0); - le = (tr.move('character', c) * -1) - sp; - - si = sp - bp; - ei = le; - } - } - - // Uses replaceChild instead of cloneNode since it removes selected attribute from option elements on IE - // See: http://support.microsoft.com/kb/829907 - bl = ed.dom.create(ed.settings.forced_root_block); - nx.parentNode.replaceChild(bl, nx); - bl.appendChild(nx); - } - } else { - if (bl.hasChildNodes()) - bl.insertBefore(nx, bl.firstChild); - else - bl.appendChild(nx); - } - } else - bl = null; // Time to create new block - } - - // Restore selection - if (si != -2) { - if (!isIE) { - bl = b.getElementsByTagName(ed.settings.element)[0]; - r = d.createRange(); - - // Select last location or generated block - if (si != -1) - r.setStart(t.find(b, 1, si), so); - else - r.setStart(bl, 0); - - // Select last location or generated block - if (ei != -1) - r.setEnd(t.find(b, 1, ei), eo); - else - r.setEnd(bl, 0); - - if (s) { - s.removeAllRanges(); - s.addRange(r); - } - } else { - try { - r = s.createRange(); - r.moveToElementText(b); - r.collapse(1); - r.moveStart('character', si); - r.moveEnd('character', ei); - r.select(); - } catch (ex) { - // Ignore - } - } - } else if (!isIE && (n = ed.dom.get('__mce'))) { - // Restore the id of the selected element - if (eid) - n.setAttribute('id', eid); - else - n.removeAttribute('id'); - - // Move caret before selected element - r = d.createRange(); - r.setStartBefore(n); - r.setEndBefore(n); - se.setRng(r); - } - }, - getParentBlock : function(n) { var d = this.dom; @@ -12054,6 +14115,8 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { var t = this, ed = t.editor, dom = ed.dom, d = ed.getDoc(), se = ed.settings, s = ed.selection.getSel(), r = s.getRangeAt(0), b = d.body; var rb, ra, dir, sn, so, en, eo, sb, eb, bn, bef, aft, sc, ec, n, vp = dom.getViewPort(ed.getWin()), y, ch, car; + ed.undoManager.beforeChange(); + // If root blocks are forced then use Operas default behavior since it's really good // Removed due to bug: #1853816 // if (se.forced_root_block && isOpera) @@ -12115,6 +14178,11 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { ra.setStart(en, 0); } + // If the body is totally empty add a BR element this might happen on webkit + if (!d.body.hasChildNodes()) { + d.body.appendChild(dom.create('br')); + } + // Never use body as start or end node sn = sn.nodeName == "HTML" ? d.body : sn; // Fix for Opera bug: https://bugs.opera.com/show_bug.cgi?id=273224&comments=yes sn = sn.nodeName == "BODY" ? sn.firstChild : sn; @@ -12229,10 +14297,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (aft.firstChild && aft.firstChild.nodeName == bn) aft.innerHTML = aft.firstChild.innerHTML; - // Padd empty blocks - if (isEmpty(bef)) - bef.innerHTML = '
    '; - function appendStyles(e, en) { var nl = [], nn, n, i; @@ -12257,14 +14321,18 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { nn = nn.appendChild(nl[i]); // Padd most inner style element - nl[0].innerHTML = isOpera ? ' ' : '
    '; // Extra space for Opera so that the caret can move there + nl[0].innerHTML = isOpera ? '\u00a0' : '
    '; // Extra space for Opera so that the caret can move there return nl[0]; // Move caret to most inner element } else - e.innerHTML = isOpera ? ' ' : '
    '; // Extra space for Opera so that the caret can move there + e.innerHTML = isOpera ? '\u00a0' : '
    '; // Extra space for Opera so that the caret can move there }; + + // Padd empty blocks + if (dom.isEmpty(bef)) + appendStyles(bef, sn); // Fill empty afterblook with current style - if (isEmpty(aft)) + if (dom.isEmpty(aft)) car = appendStyles(aft, en); // Opera needs this one backwards for older versions @@ -12280,27 +14348,26 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { aft.normalize(); bef.normalize(); - function first(n) { - return d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, FALSE).nextNode() || n; - }; - // Move cursor and scroll into view - r = d.createRange(); - r.selectNodeContents(isGecko ? first(car || aft) : car || aft); - r.collapse(1); - s.removeAllRanges(); - s.addRange(r); + ed.selection.select(aft, true); + ed.selection.collapse(true); // scrollIntoView seems to scroll the parent window in most browsers now including FF 3.0b4 so it's time to stop using it and do it our selfs y = ed.dom.getPos(aft).y; - ch = aft.clientHeight; + //ch = aft.clientHeight; // Is element within viewport - if (y < vp.y || y + ch > vp.y + vp.h) { + if (y < vp.y || y + 25 > vp.y + vp.h) { ed.getWin().scrollTo(0, y < vp.y ? y : y - vp.h + 25); // Needs to be hardcoded to roughly one line of text if a huge text block is broken into two blocks - //console.debug('SCROLL!', 'vp.y: ' + vp.y, 'y' + y, 'vp.h' + vp.h, 'clientHeight' + aft.clientHeight, 'yyy: ' + (y < vp.y ? y : y - vp.h + aft.clientHeight)); + + /*console.debug( + 'Element: y=' + y + ', h=' + ch + ', ' + + 'Viewport: y=' + vp.y + ", h=" + vp.h + ', bottom=' + (vp.y + vp.h) + );*/ } + ed.undoManager.add(); + return FALSE; }, @@ -12511,11 +14578,16 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { id = t.prefix + id; - if (ed.settings.use_native_selects) + + function useNativeListForAccessibility(ed) { + return ed.settings.use_accessible_selects && !tinymce.isGecko + } + + if (ed.settings.use_native_selects || useNativeListForAccessibility(ed)) c = new tinymce.ui.NativeListBox(id, s); else { cls = cc || t._cls.listbox || tinymce.ui.ListBox; - c = new cls(id, s); + c = new cls(id, s, ed); } t.controls[id] = c; @@ -12570,11 +14642,11 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (s.menu_button) { cls = cc || t._cls.menubutton || tinymce.ui.MenuButton; - c = new cls(id, s); + c = new cls(id, s, ed); ed.onMouseDown.add(c.hideMenu, c); } else { cls = t._cls.button || tinymce.ui.Button; - c = new cls(id, s); + c = new cls(id, s, ed); } return t.add(c); @@ -12617,7 +14689,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { id = t.prefix + id; cls = cc || t._cls.splitbutton || tinymce.ui.SplitButton; - c = t.add(new cls(id, s)); + c = t.add(new cls(id, s, ed)); ed.onMouseDown.add(c.hideMenu, c); return c; @@ -12657,7 +14729,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { id = t.prefix + id; cls = cc || t._cls.colorsplitbutton || tinymce.ui.ColorSplitButton; - c = new cls(id, s); + c = new cls(id, s, ed); ed.onMouseDown.add(c.hideMenu, c); // Remove the menu element when the editor is removed @@ -12689,13 +14761,25 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { id = t.prefix + id; cls = cc || t._cls.toolbar || tinymce.ui.Toolbar; - c = new cls(id, s); + c = new cls(id, s, t.editor); if (t.get(id)) return null; return t.add(c); }, + + createToolbarGroup : function(id, s, cc) { + var c, t = this, cls; + id = t.prefix + id; + cls = cc || this._cls.toolbarGroup || tinymce.ui.ToolbarGroup; + c = new cls(id, s, t.editor); + + if (t.get(id)) + return null; + + return t.add(c); + }, createSeparator : function(cc) { var cls = cc || this._cls.separator || tinymce.ui.Separator; @@ -12832,53 +14916,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } }); }(tinymce)); -(function(tinymce) { - function CommandManager() { - var execCommands = {}, queryStateCommands = {}, queryValueCommands = {}; - - function add(collection, cmd, func, scope) { - if (typeof(cmd) == 'string') - cmd = [cmd]; - - tinymce.each(cmd, function(cmd) { - collection[cmd.toLowerCase()] = {func : func, scope : scope}; - }); - }; - - tinymce.extend(this, { - add : function(cmd, func, scope) { - add(execCommands, cmd, func, scope); - }, - - addQueryStateHandler : function(cmd, func, scope) { - add(queryStateCommands, cmd, func, scope); - }, - - addQueryValueHandler : function(cmd, func, scope) { - add(queryValueCommands, cmd, func, scope); - }, - - execCommand : function(scope, cmd, ui, value, args) { - if (cmd = execCommands[cmd.toLowerCase()]) { - if (cmd.func.call(scope || cmd.scope, ui, value, args) !== false) - return true; - } - }, - - queryCommandValue : function() { - if (cmd = queryValueCommands[cmd.toLowerCase()]) - return cmd.func.call(scope || cmd.scope, ui, value, args); - }, - - queryCommandState : function() { - if (cmd = queryStateCommands[cmd.toLowerCase()]) - return cmd.func.call(scope || cmd.scope, ui, value, args); - } - }); - }; - - tinymce.GlobalCommands = new CommandManager(); -})(tinymce); (function(tinymce) { tinymce.Formatter = function(ed) { var formats = {}, @@ -12887,7 +14924,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { selection = ed.selection, TreeWalker = tinymce.dom.TreeWalker, rangeUtils = new tinymce.dom.RangeUtils(dom), - isValid = ed.schema.isValid, + isValid = ed.schema.isValidChild, isBlock = dom.isBlock, forcedRootBlock = ed.settings.forced_root_block, nodeIndex = dom.nodeIndex, @@ -12895,8 +14932,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { MCE_ATTR_RE = /^(src|href|style)$/, FALSE = false, TRUE = true, - undefined, - pendingFormats = {apply : [], remove : []}; + undefined; function isArray(obj) { return obj instanceof Array; @@ -12956,8 +14992,31 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } }; + var getTextDecoration = function(node) { + var decoration; + + ed.dom.getParent(node, function(n) { + decoration = ed.dom.getStyle(n, 'text-decoration'); + return decoration && decoration !== 'none'; + }); + + return decoration; + }; + + var processUnderlineAndColor = function(node) { + var textDecoration; + if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) { + textDecoration = getTextDecoration(node.parentNode); + if (ed.dom.getStyle(node, 'color') && textDecoration) { + ed.dom.setStyle(node, 'text-decoration', textDecoration); + } else if (ed.dom.getStyle(node, 'textdecoration') === textDecoration) { + ed.dom.setStyle(node, 'text-decoration', null); + } + } + }; + function apply(name, vars, node) { - var formatList = get(name), format = formatList[0], bookmark, rng, i; + var formatList = get(name), format = formatList[0], bookmark, rng, i, isCollapsed = selection.isCollapsed(); function moveStart(rng) { var container = rng.startContainer, @@ -12987,6 +15046,10 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { fmt = fmt || format; if (elm) { + if (fmt.onformat) { + fmt.onformat(elm, fmt, vars, node); + } + each(fmt.styles, function(value, name) { dom.setStyle(elm, name, replaceVars(value, vars)); }); @@ -13003,8 +15066,89 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }); } }; + function adjustSelectionToVisibleSelection() { + function findSelectionEnd(start, end) { + var walker = new TreeWalker(end); + for (node = walker.current(); node; node = walker.prev()) { + if (node.childNodes.length > 1 || node == start) { + return node; + } + } + }; + + // Adjust selection so that a end container with a end offset of zero is not included in the selection + // as this isn't visible to the user. + var rng = ed.selection.getRng(); + var start = rng.startContainer; + var end = rng.endContainer; + + if (start != end && rng.endOffset == 0) { + var newEnd = findSelectionEnd(start, end); + var endOffset = newEnd.nodeType == 3 ? newEnd.length : newEnd.childNodes.length; + + rng.setEnd(newEnd, endOffset); + } + + return rng; + } + + function applyStyleToList(node, bookmark, wrapElm, newWrappers, process){ + var nodes = [], listIndex = -1, list, startIndex = -1, endIndex = -1, currentWrapElm; + + // find the index of the first child list. + each(node.childNodes, function(n, index) { + if (n.nodeName === "UL" || n.nodeName === "OL") { + listIndex = index; + list = n; + return false; + } + }); + + // get the index of the bookmarks + each(node.childNodes, function(n, index) { + if (n.nodeName === "SPAN" && dom.getAttrib(n, "data-mce-type") == "bookmark") { + if (n.id == bookmark.id + "_start") { + startIndex = index; + } else if (n.id == bookmark.id + "_end") { + endIndex = index; + } + } + }); + + // if the selection spans across an embedded list, or there isn't an embedded list - handle processing normally + if (listIndex <= 0 || (startIndex < listIndex && endIndex > listIndex)) { + each(tinymce.grep(node.childNodes), process); + return 0; + } else { + currentWrapElm = wrapElm.cloneNode(FALSE); + + // create a list of the nodes on the same side of the list as the selection + each(tinymce.grep(node.childNodes), function(n, index) { + if ((startIndex < listIndex && index < listIndex) || (startIndex > listIndex && index > listIndex)) { + nodes.push(n); + n.parentNode.removeChild(n); + } + }); + + // insert the wrapping element either before or after the list. + if (startIndex < listIndex) { + node.insertBefore(currentWrapElm, list); + } else if (startIndex > listIndex) { + node.insertBefore(currentWrapElm, list.nextSibling); + } + + // add the new nodes to the list. + newWrappers.push(currentWrapElm); + + each(nodes, function(node) { + currentWrapElm.appendChild(node); + }); - function applyRngStyle(rng) { + return currentWrapElm; + } + }; + + function applyRngStyle(rng, bookmark, node_specific) { var newWrappers = [], wrapName, wrapElm; // Setup wrapper element @@ -13048,6 +15192,11 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (format.selector) { // Look for matching formats each(formatList, function(format) { + // Check collapsed state if it exists + if ('collapsed' in format && format.collapsed !== isCollapsed) { + return; + } + if (dom.is(node, format.selector) && !isCaretNode(node)) { setElementFormat(node, format); found = true; @@ -13062,7 +15211,8 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } // Is it valid to wrap this item - if (isValid(wrapName, nodeName) && isValid(parentName, wrapName)) { + if (isValid(wrapName, nodeName) && isValid(parentName, wrapName) && + !(!node_specific && node.nodeType === 3 && node.nodeValue.length === 1 && node.nodeValue.charCodeAt(0) === 65279) && node.id !== '_mce_caret') { // Start wrapping if (!currentWrapElm) { // Wrap the node @@ -13072,20 +15222,47 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } currentWrapElm.appendChild(node); + } else if (nodeName == 'li' && bookmark) { + // Start wrapping - if we are in a list node and have a bookmark, then we will always begin by wrapping in a new element. + currentWrapElm = applyStyleToList(node, bookmark, wrapElm, newWrappers, process); } else { // Start a new wrapper for possible children currentWrapElm = 0; each(tinymce.grep(node.childNodes), process); - // End the last wrapper - currentWrapElm = 0; - } - }; - - // Process siblings from range - each(nodes, process); - }); + // End the last wrapper + currentWrapElm = 0; + } + }; + + // Process siblings from range + each(nodes, process); + }); + + // Wrap links inside as well, for example color inside a link when the wrapper is around the link + if (format.wrap_links === false) { + each(newWrappers, function(node) { + function process(node) { + var i, currentWrapElm, children; + + if (node.nodeName === 'A') { + currentWrapElm = wrapElm.cloneNode(FALSE); + newWrappers.push(currentWrapElm); + + children = tinymce.grep(node.childNodes); + for (i = 0; i < children.length; i++) + currentWrapElm.appendChild(children[i]); + + node.appendChild(currentWrapElm); + } + + each(tinymce.grep(node.childNodes), process); + }; + + process(node); + }); + } // Cleanup each(newWrappers, function(node) { @@ -13126,8 +15303,9 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { childCount = getChildCount(node); - // Remove empty nodes - if (childCount === 0) { + // Remove empty nodes but only if there is multiple wrappers and they are not block + // elements so never remove single

    since that would remove the currrent empty block element where the caret is at + if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) { dom.remove(node, 1); return; } @@ -13143,6 +15321,19 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // this: text // will become: text each(dom.select(format.inline, node), function(child) { + var parent; + + // When wrap_links is set to false we don't want + // to remove the format on children within links + if (format.wrap_links === false) { + parent = child.parentNode; + + do { + if (parent.nodeName === 'A') + return; + } while (parent = parent.parentNode); + } + removeFormat(format, vars, child, format.exact ? child : null); }); }); @@ -13166,7 +15357,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } // Merge next and previous siblings if they are similar texttext becomes texttext - if (node) { + if (node && format.merge_siblings !== false) { node = mergeSiblings(getNonWhiteSpaceSibling(node), node); node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE)); } @@ -13176,17 +15367,29 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (format) { if (node) { - rng = dom.createRng(); - - rng.setStartBefore(node); - rng.setEndAfter(node); - - applyRngStyle(expandRng(rng, formatList)); + if (node.nodeType) { + rng = dom.createRng(); + rng.setStartBefore(node); + rng.setEndAfter(node); + applyRngStyle(expandRng(rng, formatList), null, true); + } else { + applyRngStyle(node, null, true); + } } else { - if (!selection.isCollapsed() || !format.inline) { + if (!isCollapsed || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) { + // Obtain selection node before selection is unselected by applyRngStyle() + var curSelNode = ed.selection.getNode(); + // Apply formatting to selection + ed.selection.setRng(adjustSelectionToVisibleSelection()); bookmark = selection.getBookmark(); - applyRngStyle(expandRng(selection.getRng(TRUE), formatList)); + applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark); + + // Colored nodes should be underlined so that the color of the underline matches the text color. + if (format.styles && (format.styles.color || format.styles.textDecoration)) { + tinymce.walk(curSelNode, processUnderlineAndColor, 'childNodes'); + processUnderlineAndColor(curSelNode); + } selection.moveToBookmark(bookmark); selection.setRng(moveStart(selection.getRng(TRUE))); @@ -13199,6 +15402,44 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { function remove(name, vars, node) { var formatList = get(name), format = formatList[0], bookmark, i, rng; + function moveStart(rng) { + var container = rng.startContainer, + offset = rng.startOffset, + walker, node, nodes, tmpNode; + + // Convert text node into index if possible + if (container.nodeType == 3 && offset >= container.nodeValue.length - 1) { + container = container.parentNode; + offset = nodeIndex(container) + 1; + } + + // Move startContainer/startOffset in to a suitable node + if (container.nodeType == 1) { + nodes = container.childNodes; + container = nodes[Math.min(offset, nodes.length - 1)]; + walker = new TreeWalker(container); + + // If offset is at end of the parent node walk to the next one + if (offset > nodes.length - 1) + walker.next(); + + for (node = walker.current(); node; node = walker.next()) { + if (node.nodeType == 3 && !isWhiteSpaceNode(node)) { + // IE has a "neat" feature where it moves the start node into the closest element + // we can avoid this by inserting an element before it and then remove it after we set the selection + tmpNode = dom.create('a', null, INVISIBLE_CHAR); + node.parentNode.insertBefore(tmpNode, node); + + // Set selection and remove tmpNode + rng.setStart(node, 0); + selection.setRng(rng); + dom.remove(tmpNode); + + return; + } + } + } + }; // Merges the styles for each node function process(node) { @@ -13312,8 +15553,8 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (startContainer != endContainer) { // Wrap start/end nodes in span element since these might be cloned/moved - startContainer = wrap(startContainer, 'span', {id : '_start', _mce_type : 'bookmark'}); - endContainer = wrap(endContainer, 'span', {id : '_end', _mce_type : 'bookmark'}); + startContainer = wrap(startContainer, 'span', {id : '_start', 'data-mce-type' : 'bookmark'}); + endContainer = wrap(endContainer, 'span', {id : '_end', 'data-mce-type' : 'bookmark'}); // Split start/end splitToFormatRoot(startContainer); @@ -13336,30 +15577,53 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { rangeUtils.walk(rng, function(nodes) { each(nodes, function(node) { process(node); + + // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined. + if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' && node.parentNode && getTextDecoration(node.parentNode) === 'underline') { + removeFormat({'deep': false, 'exact': true, 'inline': 'span', 'styles': {'textDecoration' : 'underline'}}, null, node); + } }); }); }; // Handle node if (node) { - rng = dom.createRng(); - rng.setStartBefore(node); - rng.setEndAfter(node); - removeRngStyle(rng); + if (node.nodeType) { + rng = dom.createRng(); + rng.setStartBefore(node); + rng.setEndAfter(node); + removeRngStyle(rng); + } else { + removeRngStyle(node); + } + return; } - if (!selection.isCollapsed() || !format.inline) { + if (!selection.isCollapsed() || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) { bookmark = selection.getBookmark(); removeRngStyle(selection.getRng(TRUE)); selection.moveToBookmark(bookmark); + + // Check if start element still has formatting then we are at: "text|text" and need to move the start into the next text node + if (format.inline && match(name, vars, selection.getStart())) { + moveStart(selection.getRng(true)); + } + ed.nodeChanged(); } else performCaretAction('remove', name, vars); + + // When you remove formatting from a table cell in WebKit (cell, not the contents of a cell) there is a rendering issue with column width + if (tinymce.isWebKit) { + ed.execCommand('mceCleanup'); + } }; function toggle(name, vars, node) { - if (match(name, vars, node)) + var fmt = get(name); + + if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0]['toggle'])) remove(name, vars, node); else apply(name, vars, node); @@ -13371,6 +15635,11 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { function matchItems(node, format, item_name) { var key, value, items = format[item_name], i; + // Custom match + if (format.onmatch) { + return format.onmatch(node, format, item_name); + } + // Check all items if (items) { // Non indexed object @@ -13423,7 +15692,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }; function match(name, vars, node) { - var startNode, i; + var startNode; function matchParents(node) { // Find first node with similar format settings @@ -13439,21 +15708,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (node) return matchParents(node); - // Check pending formats - if (selection.isCollapsed()) { - for (i = pendingFormats.apply.length - 1; i >= 0; i--) { - if (pendingFormats.apply[i].name == name) - return true; - } - - for (i = pendingFormats.remove.length - 1; i >= 0; i--) { - if (pendingFormats.remove[i].name == name) - return false; - } - - return matchParents(selection.getNode()); - } - // Check selected node node = selection.getNode(); if (matchParents(node)) @@ -13472,33 +15726,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { function matchAll(names, vars) { var startElement, matchedFormatNames = [], checkedMap = {}, i, ni, name; - // If the selection is collapsed then check pending formats - if (selection.isCollapsed()) { - for (ni = 0; ni < names.length; ni++) { - // If the name is to be removed, then stop it from being added - for (i = pendingFormats.remove.length - 1; i >= 0; i--) { - name = names[ni]; - - if (pendingFormats.remove[i].name == name) { - checkedMap[name] = true; - break; - } - } - } - - // If the format is to be applied - for (i = pendingFormats.apply.length - 1; i >= 0; i--) { - for (ni = 0; ni < names.length; ni++) { - name = names[ni]; - - if (!checkedMap[name] && pendingFormats.apply[i].name == name) { - checkedMap[name] = true; - matchedFormatNames.push(name); - } - } - } - } - // Check start of selection for formats startElement = selection.getStart(); dom.getParent(startElement, function(node) { @@ -13607,7 +15834,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }; function isWhiteSpaceNode(node) { - return node && node.nodeType === 3 && /^([\s\r\n]+|)$/.test(node.nodeValue); + return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue); }; function wrap(node, name, attrs) { @@ -13623,36 +15850,55 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { var startContainer = rng.startContainer, startOffset = rng.startOffset, endContainer = rng.endContainer, - endOffset = rng.endOffset, sibling, lastIdx; + endOffset = rng.endOffset, sibling, lastIdx, leaf, endPoint; // This function walks up the tree if there is no siblings before/after the node - function findParentContainer(container, child_name, sibling_name, root) { - var parent, child; + function findParentContainer(start) { + var container, parent, child, sibling, siblingName; - root = root || dom.getRoot(); + container = parent = start ? startContainer : endContainer; + siblingName = start ? 'previousSibling' : 'nextSibling'; + root = dom.getRoot(); - for (;;) { - // Check if we can move up are we at root level or body level - parent = container.parentNode; + // If it's a text node and the offset is inside the text + if (container.nodeType == 3 && !isWhiteSpaceNode(container)) { + if (start ? startOffset > 0 : endOffset < container.nodeValue.length) { + return container; + } + } + for (;;) { // Stop expanding on block elements or root depending on format if (parent == root || (!format[0].block_expand && isBlock(parent))) - return container; + return parent; - for (sibling = parent[child_name]; sibling && sibling != container; sibling = sibling[sibling_name]) { - if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) - return container; - - if (sibling.nodeType == 3 && !isWhiteSpaceNode(sibling)) - return container; + // Walk left/right + for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) { + if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling)) { + return parent; + } } - container = container.parentNode; + // Check if we can move up are we at root level or body level + parent = parent.parentNode; } return container; }; + // This function walks down the tree to find the leaf at the selection. + // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node. + function findLeaf(node, offset) { + if (offset === undefined) + offset = node.nodeType === 3 ? node.length : node.childNodes.length; + while (node && node.hasChildNodes()) { + node = node.childNodes[offset]; + if (node) + offset = node.nodeType === 3 ? node.length : node.childNodes.length; + } + return { node: node, offset: offset }; + } + // If index based start position then resolve it if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { lastIdx = startContainer.childNodes.length - 1; @@ -13672,31 +15918,141 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } // Exclude bookmark nodes if possible - if (isBookmarkNode(startContainer.parentNode)) - startContainer = startContainer.parentNode; - - if (isBookmarkNode(startContainer)) + if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) { + startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode; startContainer = startContainer.nextSibling || startContainer; - if (isBookmarkNode(endContainer.parentNode)) - endContainer = endContainer.parentNode; + if (startContainer.nodeType == 3) + startOffset = 0; + } - if (isBookmarkNode(endContainer)) + if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) { + endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode; endContainer = endContainer.previousSibling || endContainer; + if (endContainer.nodeType == 3) + endOffset = endContainer.length; + } + + if (format[0].inline) { + if (rng.collapsed) { + function findWordEndPoint(container, offset, start) { + var walker, node, pos, lastTextNode; + + function findSpace(node, offset) { + var pos, pos2, str = node.nodeValue; + + if (typeof(offset) == "undefined") { + offset = start ? str.length : 0; + } + + if (start) { + pos = str.lastIndexOf(' ', offset); + pos2 = str.lastIndexOf('\u00a0', offset); + pos = pos > pos2 ? pos : pos2; + + // Include the space on remove to avoid tag soup + if (pos !== -1 && !remove) { + pos++; + } + } else { + pos = str.indexOf(' ', offset); + pos2 = str.indexOf('\u00a0', offset); + pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2; + } + + return pos; + }; + + if (container.nodeType === 3) { + pos = findSpace(container, offset); + + if (pos !== -1) { + return {container : container, offset : pos}; + } + + lastTextNode = container; + } + + // Walk the nodes inside the block + walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody()); + while (node = walker[start ? 'prev' : 'next']()) { + if (node.nodeType === 3) { + lastTextNode = node; + pos = findSpace(node); + + if (pos !== -1) { + return {container : node, offset : pos}; + } + } else if (isBlock(node)) { + break; + } + } + + if (lastTextNode) { + if (start) { + offset = 0; + } else { + offset = lastTextNode.length; + } + + return {container: lastTextNode, offset: offset}; + } + } + + // Expand left to closest word boundery + endPoint = findWordEndPoint(startContainer, startOffset, true); + if (endPoint) { + startContainer = endPoint.container; + startOffset = endPoint.offset; + } + + // Expand right to closest word boundery + endPoint = findWordEndPoint(endContainer, endOffset); + if (endPoint) { + endContainer = endPoint.container; + endOffset = endPoint.offset; + } + } + + // Avoid applying formatting to a trailing space. + leaf = findLeaf(endContainer, endOffset); + if (leaf.node) { + while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) + leaf = findLeaf(leaf.node.previousSibling); + + if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 && + leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') { + + if (leaf.offset > 1) { + endContainer = leaf.node; + endContainer.splitText(leaf.offset - 1); + } else if (leaf.node.previousSibling) { + // TODO: Figure out why this is in here + //endContainer = leaf.node.previousSibling; + } + } + } + } + // Move start/end point up the tree if the leaves are sharp and if we are in different containers // Example * becomes !: !

    *texttext*

    ! // This will reduce the number of wrapper elements that needs to be created // Move start point up the tree if (format[0].inline || format[0].block_expand) { - startContainer = findParentContainer(startContainer, 'firstChild', 'nextSibling'); - endContainer = findParentContainer(endContainer, 'lastChild', 'previousSibling'); + if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) { + startContainer = findParentContainer(true); + } + + if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) { + endContainer = findParentContainer(); + } } // Expand start/end container to matching selector if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) { function findSelectorEndPoint(container, sibling_name) { - var parents, i, y; + var parents, i, y, curFormat; if (container.nodeType == 3 && container.nodeValue.length == 0 && container[sibling_name]) container = container[sibling_name]; @@ -13704,7 +16060,13 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { parents = getParents(container); for (i = 0; i < parents.length; i++) { for (y = 0; y < format.length; y++) { - if (dom.is(parents[i], format[y].selector)) + curFormat = format[y]; + + // If collapsed state is set then skip formats that doesn't match that + if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) + continue; + + if (dom.is(parents[i], curFormat.selector)) return parents[i]; } } @@ -13758,10 +16120,10 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Non block element then try to expand up the leaf if (format[0].block) { if (!isBlock(startContainer)) - startContainer = findParentContainer(startContainer, 'firstChild', 'nextSibling'); + startContainer = findParentContainer(true); if (!isBlock(endContainer)) - endContainer = findParentContainer(endContainer, 'lastChild', 'previousSibling'); + endContainer = findParentContainer(); } } @@ -13814,7 +16176,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Remove style attribute if it's empty if (stylesModified && dom.getAttrib(node, 'style') == '') { node.removeAttribute('style'); - node.removeAttribute('_mce_style'); + node.removeAttribute('data-mce-style'); } // Remove attributes @@ -13855,7 +16217,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Remove mce prefixed attributes if (MCE_ATTR_RE.test(name)) - node.removeAttribute('_mce_' + name); + node.removeAttribute('data-mce-' + name); node.removeAttribute(name); } @@ -13940,7 +16302,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }; function isBookmarkNode(node) { - return node && node.nodeType == 1 && node.getAttribute('_mce_type') == 'bookmark'; + return node && node.nodeType == 1 && node.getAttribute('data-mce-type') == 'bookmark'; }; function mergeSiblings(prev, next) { @@ -14011,7 +16373,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (prev && next) { function findElementSibling(node, sibling_name) { for (sibling = node; sibling; sibling = sibling[sibling_name]) { - if (sibling.nodeType == 3 && !isWhiteSpaceNode(sibling)) + if (sibling.nodeType == 3 && sibling.nodeValue.length !== 0) return node; if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) @@ -14054,7 +16416,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }; function getContainer(rng, start) { - var container, offset, lastIdx; + var container, offset, lastIdx, walker; container = rng[start ? 'startContainer' : 'endContainer']; offset = rng[start ? 'startOffset' : 'endOffset']; @@ -14068,103 +16430,267 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { container = container.childNodes[offset > lastIdx ? lastIdx : offset]; } + // If start text node is excluded then walk to the next node + if (container.nodeType === 3 && start && offset >= container.nodeValue.length) { + container = new TreeWalker(container, ed.getBody()).next() || container; + } + + // If end text node is excluded then walk to the previous node + if (container.nodeType === 3 && !start && offset == 0) { + container = new TreeWalker(container, ed.getBody()).prev() || container; + } + return container; }; function performCaretAction(type, name, vars) { - var i, currentPendingFormats = pendingFormats[type], - otherPendingFormats = pendingFormats[type == 'apply' ? 'remove' : 'apply']; + var invisibleChar, caretContainerId = '_mce_caret', debug = ed.settings.caret_debug; + + // Setup invisible character use zero width space on Gecko since it doesn't change the heigt of the container + invisibleChar = tinymce.isGecko ? '\u200B' : INVISIBLE_CHAR; + + // Creates a caret container bogus element + function createCaretContainer(fill) { + var caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true, style: debug ? 'color:red' : ''}); + + if (fill) { + caretContainer.appendChild(ed.getDoc().createTextNode(invisibleChar)); + } + + return caretContainer; + }; + + function isCaretContainerEmpty(node, nodes) { + while (node) { + if ((node.nodeType === 3 && node.nodeValue !== invisibleChar) || node.childNodes.length > 1) { + return false; + } + + // Collect nodes + if (nodes && node.nodeType === 1) { + nodes.push(node); + } + + node = node.firstChild; + } - function hasPending() { - return pendingFormats.apply.length || pendingFormats.remove.length; + return true; }; + + // Returns any parent caret container element + function getParentCaretContainer(node) { + while (node) { + if (node.id === caretContainerId) { + return node; + } - function resetPending() { - pendingFormats.apply = []; - pendingFormats.remove = []; + node = node.parentNode; + } }; - function perform(caret_node) { - // Apply pending formats - each(pendingFormats.apply.reverse(), function(item) { - apply(item.name, item.vars, caret_node); - }); + // Finds the first text node in the specified node + function findFirstTextNode(node) { + var walker; - // Remove pending formats - each(pendingFormats.remove.reverse(), function(item) { - remove(item.name, item.vars, caret_node); - }); + if (node) { + walker = new TreeWalker(node, node); + + for (node = walker.current(); node; node = walker.next()) { + if (node.nodeType === 3) { + return node; + } + } + } + }; + + // Removes the caret container for the specified node or all on the current document + function removeCaretContainer(node, move_caret) { + var child, rng; + + if (!node) { + node = getParentCaretContainer(selection.getStart()); + + if (!node) { + while (node = dom.get(caretContainerId)) { + removeCaretContainer(node, false); + } + } + } else { + rng = selection.getRng(true); + + if (isCaretContainerEmpty(node)) { + if (move_caret !== false) { + rng.setStartBefore(node); + rng.setEndBefore(node); + } + + dom.remove(node); + } else { + child = findFirstTextNode(node); + child = child.deleteData(0, 1); + dom.remove(node, 1); + } + + selection.setRng(rng); + } + }; + + // Applies formatting to the caret postion + function applyCaretFormat() { + var rng, caretContainer, textNode, offset, bookmark, container, text; + + rng = selection.getRng(true); + offset = rng.startOffset; + container = rng.startContainer; + text = container.nodeValue; + + caretContainer = getParentCaretContainer(selection.getStart()); + if (caretContainer) { + textNode = findFirstTextNode(caretContainer); + } + + // Expand to word is caret is in the middle of a text node and the char before/after is a alpha numeric character + if (text && offset > 0 && offset < text.length && /\w/.test(text.charAt(offset)) && /\w/.test(text.charAt(offset - 1))) { + // Get bookmark of caret position + bookmark = selection.getBookmark(); + + // Collapse bookmark range (WebKit) + rng.collapse(true); + + // Expand the range to the closest word and split it at those points + rng = expandRng(rng, get(name)); + rng = rangeUtils.split(rng); + + // Apply the format to the range + apply(name, vars, rng); + + // Move selection back to caret position + selection.moveToBookmark(bookmark); + } else { + if (!caretContainer || textNode.nodeValue !== invisibleChar) { + caretContainer = createCaretContainer(true); + textNode = caretContainer.firstChild; + + rng.insertNode(caretContainer); + offset = 1; + + apply(name, vars, caretContainer); + } else { + apply(name, vars, caretContainer); + } - dom.remove(caret_node, 1); - resetPending(); + // Move selection to text node + selection.setCursorLocation(textNode, offset); + } }; - // Check if it already exists then ignore it - for (i = currentPendingFormats.length - 1; i >= 0; i--) { - if (currentPendingFormats[i].name == name) + function removeCaretFormat() { + var rng = selection.getRng(true), container, offset, bookmark, + hasContentAfter, node, formatNode, parents = [], i, caretContainer; + + container = rng.startContainer; + offset = rng.startOffset; + node = container; + + if (container.nodeType == 3) { + if (offset != container.nodeValue.length || container.nodeValue === invisibleChar) { + hasContentAfter = true; + } + + node = node.parentNode; + } + + while (node) { + if (matchNode(node, name, vars)) { + formatNode = node; + break; + } + + if (node.nextSibling) { + hasContentAfter = true; + } + + parents.push(node); + node = node.parentNode; + } + + // Node doesn't have the specified format + if (!formatNode) { return; - } + } - currentPendingFormats.push({name : name, vars : vars}); + // Is there contents after the caret then remove the format on the element + if (hasContentAfter) { + // Get bookmark of caret position + bookmark = selection.getBookmark(); - // Check if it's in the other type, then remove it - for (i = otherPendingFormats.length - 1; i >= 0; i--) { - if (otherPendingFormats[i].name == name) - otherPendingFormats.splice(i, 1); - } + // Collapse bookmark range (WebKit) + rng.collapse(true); - // Pending apply or remove formats - if (hasPending()) { - ed.getDoc().execCommand('FontName', false, 'mceinline'); - pendingFormats.lastRng = selection.getRng(); + // Expand the range to the closest word and split it at those points + rng = expandRng(rng, get(name), true); + rng = rangeUtils.split(rng); - // IE will convert the current word - each(dom.select('font,span'), function(node) { - var bookmark; + // Remove the format from the range + remove(name, vars, rng); - if (isCaretNode(node)) { - bookmark = selection.getBookmark(); - perform(node); - selection.moveToBookmark(bookmark); - ed.nodeChanged(); + // Move selection back to caret position + selection.moveToBookmark(bookmark); + } else { + caretContainer = createCaretContainer(); + + node = caretContainer; + for (i = parents.length - 1; i >= 0; i--) { + node.appendChild(parents[i].cloneNode(false)); + node = node.firstChild; + } + + // Insert invisible character into inner most format element + node.appendChild(dom.doc.createTextNode(invisibleChar)); + node = node.firstChild; + + // Insert caret container after the formated node + dom.insertAfter(caretContainer, formatNode); + + // Move selection to text node + selection.setCursorLocation(node, 1); + } + }; + + // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements + ed.onBeforeGetContent.addToTop(function() { + var nodes = [], i; + + if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) { + // Mark children + i = nodes.length; + while (i--) { + dom.setAttrib(nodes[i], 'data-mce-bogus', '1'); } + } + }); + + // Remove caret container on mouse up and on key up + tinymce.each('onMouseUp onKeyUp'.split(' '), function(name) { + ed[name].addToTop(function() { + removeCaretContainer(); }); + }); - // Only register listeners once if we need to - if (!pendingFormats.isListening && hasPending()) { - pendingFormats.isListening = true; - - each('onKeyDown,onKeyUp,onKeyPress,onMouseUp'.split(','), function(event) { - ed[event].addToTop(function(ed, e) { - // Do we have pending formats and is the selection moved has moved - if (hasPending() && !tinymce.dom.RangeUtils.compareRanges(pendingFormats.lastRng, selection.getRng())) { - each(dom.select('font,span'), function(node) { - var textNode, rng; - - // Look for marker - if (isCaretNode(node)) { - textNode = node.firstChild; - - if (textNode) { - perform(node); - - rng = dom.createRng(); - rng.setStart(textNode, textNode.nodeValue.length); - rng.setEnd(textNode, textNode.nodeValue.length); - selection.setRng(rng); - ed.nodeChanged(); - } else - dom.remove(node); - } - }); + // Remove caret container on keydown and it's a backspace, enter or left/right arrow keys + ed.onKeyDown.addToTop(function(ed, e) { + var keyCode = e.keyCode; - // Always unbind and clear pending styles on keyup - if (e.type == 'keyup' || e.type == 'mouseup') - resetPending(); - } - }); - }); + if (keyCode == 8 || keyCode == 37 || keyCode == 39) { + removeCaretContainer(getParentCaretContainer(selection.getStart())); } + }); + + // Do apply or remove caret format + if (type == "apply") { + applyCaretFormat(); + } else { + removeCaretFormat(); } }; }; @@ -14174,12 +16700,15 @@ tinymce.onAddEditor.add(function(tinymce, ed) { var filters, fontSizes, dom, settings = ed.settings; if (settings.inline_styles) { - fontSizes = tinymce.explode(settings.font_size_style_values); + fontSizes = tinymce.explode(settings.font_size_legacy_values); function replaceWithSpan(node, styles) { - dom.replace(dom.create('span', { - style : styles - }), node, 1); + tinymce.each(styles, function(value, name) { + if (value) + dom.setStyle(node, name, value); + }); + + dom.rename(node, 'span'); }; filters = { @@ -14216,6 +16745,7 @@ tinymce.onAddEditor.add(function(tinymce, ed) { }; ed.onPreProcess.add(convert); + ed.onSetContent.add(convert); ed.onInit.add(function() { ed.selection.onSetContent.add(convert); diff --git a/js/tiny_mce/tiny_mce_src.js b/js/tiny_mce/tiny_mce_src.js index 9db8d18fe0..0866d617b6 100644 --- a/js/tiny_mce/tiny_mce_src.js +++ b/js/tiny_mce/tiny_mce_src.js @@ -1,13 +1,13 @@ (function(win) { var whiteSpaceRe = /^\s*|\s*$/g, - undefined; + undefined, isRegExpBroken = 'B'.replace(/A(.)|B/, '$1') === '$1'; var tinymce = { majorVersion : '3', - minorVersion : '3.7', + minorVersion : '4.7', - releaseDate : '2010-06-10', + releaseDate : '2011-11-03', _init : function() { var t = this, d = document, na = navigator, ua = na.userAgent, i, nl, n, base, p, v; @@ -20,6 +20,12 @@ t.isIE6 = t.isIE && /MSIE [56]/.test(ua); + t.isIE7 = t.isIE && /MSIE [7]/.test(ua); + + t.isIE8 = t.isIE && /MSIE [8]/.test(ua); + + t.isIE9 = t.isIE && /MSIE [9]/.test(ua); + t.isGecko = !t.isWebKit && /Gecko/.test(ua); t.isMac = ua.indexOf('Mac') != -1; @@ -27,6 +33,8 @@ t.isAir = /adobeair/i.test(ua); t.isIDevice = /(iPad|iPhone)/.test(ua); + + t.isIOS5 = t.isIDevice && ua.match(/AppleWebKit\/(\d*)/)[1]>=534; // TinyMCE .NET webcontrol might be setting the values for TinyMCE if (win.tinyMCEPreInit) { @@ -52,7 +60,7 @@ } function getBase(n) { - if (n.src && /tiny_mce(|_gzip|_jquery|_prototype)(_dev|_src)?.js/.test(n.src)) { + if (n.src && /tiny_mce(|_gzip|_jquery|_prototype|_full)(_dev|_src)?.js/.test(n.src)) { if (/_(src|dev)\.js/g.test(n.src)) t.suffix = '_src'; @@ -103,6 +111,24 @@ return typeof(o) == t; }, + makeMap : function(items, delim, map) { + var i; + + items = items || []; + delim = delim || ','; + + if (typeof(items) == "string") + items = items.split(delim); + + map = map || {}; + + i = items.length; + while (i--) + map[items[i]] = {}; + + return map; + }, + each : function(o, cb, s) { var n, l; @@ -185,7 +211,7 @@ return (s ? '' + s : '').replace(whiteSpaceRe, ''); }, - create : function(s, p) { + create : function(s, p, root) { var t = this, sp, ns, cn, scn, c, de = 0; // Parse : : @@ -193,7 +219,7 @@ cn = s[3].match(/(^|\.)(\w+)$/i)[2]; // Class name // Create namespace for new class - ns = t.createNS(s[3].replace(/\.\w+$/, '')); + ns = t.createNS(s[3].replace(/\.\w+$/, ''), root); // Class already exists if (ns[cn]) @@ -428,6 +454,29 @@ return u + v; return u.replace('#', v + '#'); + }, + + // Fix function for IE 9 where regexps isn't working correctly + // Todo: remove me once MS fixes the bug + _replace : function(find, replace, str) { + // On IE9 we have to fake $x replacement + if (isRegExpBroken) { + return str.replace(find, function() { + var val = replace, args = arguments, i; + + for (i = 0; i < args.length - 2; i++) { + if (args[i] === undefined) { + val = val.replace(new RegExp('\\$' + i, 'g'), ''); + } else { + val = val.replace(new RegExp('\\$' + i, 'g'), args[i]); + } + } + + return val; + }); + } + + return str.replace(find, replace); } }; @@ -437,7 +486,11 @@ // Expose tinymce namespace to the global namespace (window) win.tinymce = win.tinyMCE = tinymce; -})(window); + + // Describe the different namespaces + + })(window); + tinymce.create('tinymce.util.Dispatcher', { @@ -498,7 +551,7 @@ tinymce.create('tinymce.util.Dispatcher', { tinymce.create('tinymce.util.URI', { URI : function(u, s) { - var t = this, o, a, b; + var t = this, o, a, b, base_url; // Trim whitespace u = tinymce.trim(u); @@ -506,8 +559,9 @@ tinymce.create('tinymce.util.Dispatcher', { // Default settings s = t.settings = s || {}; - // Strange app protocol or local anchor - if (/^(mailto|tel|news|javascript|about|data):/i.test(u) || /^\s*#/.test(u)) { + // Strange app protocol that isn't http/https or local anchor + // For example: mailto,skype,tel etc. + if (/^([\w\-]+):([^\/]{2})/i.test(u) || /^\s*#/.test(u)) { t.source = u; return; } @@ -517,8 +571,10 @@ tinymce.create('tinymce.util.Dispatcher', { u = (s.base_uri ? s.base_uri.protocol || 'http' : 'http') + '://mce_host' + u; // Relative path http:// or protocol relative //path - if (!/^\w*:?\/\//.test(u)) - u = (s.base_uri.protocol || 'http') + '://mce_host' + t.toAbsPath(s.base_uri.path, u); + if (!/^[\w-]*:?\/\//.test(u)) { + base_url = s.base_uri ? s.base_uri.path : new tinymce.util.URI(location.href).directory; + u = ((s.base_uri && s.base_uri.protocol) || 'http') + '://mce_host' + t.toAbsPath(base_url, u); + } // Parse URL (Credits goes to Steave, http://blog.stevenlevithan.com/archives/parseuri) u = u.replace(/@@/g, '(mce_at)'); // Zope 3 workaround, they use @@something @@ -805,9 +861,11 @@ tinymce.create('tinymce.util.Dispatcher', { }); })(); -tinymce.create('static tinymce.util.JSON', { - serialize : function(o) { - var i, v, s = tinymce.util.JSON.serialize, t; +(function() { + function serialize(o, quote) { + var i, v, t; + + quote = quote || '"'; if (o == null) return 'null'; @@ -817,7 +875,11 @@ tinymce.create('static tinymce.util.JSON', { if (t == 'string') { v = '\bb\tt\nn\ff\rr\""\'\'\\\\'; - return '"' + o.replace(/([\u0080-\uFFFF\x00-\x1f\"])/g, function(a, b) { + return quote + o.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g, function(a, b) { + // Make sure single quotes never get encoded inside double quotes for JSON compatibility + if (quote === '"' && a === "'") + return a; + i = v.indexOf(b); if (i + 1) @@ -826,37 +888,44 @@ tinymce.create('static tinymce.util.JSON', { a = b.charCodeAt().toString(16); return '\\u' + '0000'.substring(a.length) + a; - }) + '"'; + }) + quote; } if (t == 'object') { if (o.hasOwnProperty && o instanceof Array) { for (i=0, v = '['; i 0 ? ',' : '') + s(o[i]); + v += (i > 0 ? ',' : '') + serialize(o[i], quote); return v + ']'; } v = '{'; - for (i in o) - v += typeof o[i] != 'function' ? (v.length > 1 ? ',"' : '"') + i + '":' + s(o[i]) : ''; + for (i in o) { + if (o.hasOwnProperty(i)) { + v += typeof o[i] != 'function' ? (v.length > 1 ? ',' + quote : quote) + i + quote +':' + serialize(o[i], quote) : ''; + } + } return v + '}'; } return '' + o; - }, + }; - parse : function(s) { - try { - return eval('(' + s + ')'); - } catch (ex) { - // Ignore + tinymce.util.JSON = { + serialize: serialize, + + parse: function(s) { + try { + return eval('(' + s + ')'); + } catch (ex) { + // Ignore + } } - } - }); + }; +})(); tinymce.create('static tinymce.util.XHR', { send : function(o) { @@ -948,7 +1017,8 @@ tinymce.create('static tinymce.util.XHR', { }; o.error = function(ty, x) { - ecb.call(o.error_scope || o.scope, ty, x); + if (ecb) + ecb.call(o.error_scope || o.scope, ty, x); }; o.data = JSON.serialize({ @@ -970,5862 +1040,7523 @@ tinymce.create('static tinymce.util.XHR', { } }); }()); +(function(tinymce){ + tinymce.VK = { + DELETE: 46, + BACKSPACE: 8, + ENTER: 13, + TAB: 9, + SPACEBAR: 32, + UP: 38, + DOWN: 40 + } +})(tinymce); + (function(tinymce) { - // Shorten names - var each = tinymce.each, - is = tinymce.is, - isWebKit = tinymce.isWebKit, - isIE = tinymce.isIE, - blockRe = /^(H[1-6R]|P|DIV|ADDRESS|PRE|FORM|T(ABLE|BODY|HEAD|FOOT|H|R|D)|LI|OL|UL|CAPTION|BLOCKQUOTE|CENTER|DL|DT|DD|DIR|FIELDSET|NOSCRIPT|MENU|ISINDEX|SAMP)$/, - boolAttrs = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected'), - mceAttribs = makeMap('src,href,style,coords,shape'), - encodedChars = {'&' : '&', '"' : '"', '<' : '<', '>' : '>'}, - encodeCharsRe = /[<>&\"]/g, - simpleSelectorRe = /^([a-z0-9],?)+$/i, - tagRegExp = /<(\w+)((?:\s+\w+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)(\s*\/?)>/g, - attrRegExp = /(\w+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; + var VK = tinymce.VK, BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE; - function makeMap(str) { - var map = {}, i; + function cleanupStylesWhenDeleting(ed) { + var dom = ed.dom, selection = ed.selection; - str = str.split(','); - for (i = str.length; i >= 0; i--) - map[str[i]] = 1; + ed.onKeyDown.add(function(ed, e) { + var rng, blockElm, node, clonedSpan, isDelete; - return map; - }; + isDelete = e.keyCode == DELETE; + if (isDelete || e.keyCode == BACKSPACE) { + e.preventDefault(); + rng = selection.getRng(); - tinymce.create('tinymce.dom.DOMUtils', { - doc : null, - root : null, - files : null, - pixelStyles : /^(top|left|bottom|right|width|height|borderWidth)$/, - props : { - "for" : "htmlFor", - "class" : "className", - className : "className", - checked : "checked", - disabled : "disabled", - maxlength : "maxLength", - readonly : "readOnly", - selected : "selected", - value : "value", - id : "id", - name : "name", - type : "type" - }, + // Find root block + blockElm = dom.getParent(rng.startContainer, dom.isBlock); - DOMUtils : function(d, s) { - var t = this, globalStyle; + // On delete clone the root span of the next block element + if (isDelete) + blockElm = dom.getNext(blockElm, dom.isBlock); - t.doc = d; - t.win = window; - t.files = {}; - t.cssFlicker = false; - t.counter = 0; - t.boxModel = !tinymce.isIE || d.compatMode == "CSS1Compat"; - t.stdMode = d.documentMode === 8; + // Locate root span element and clone it since it would otherwise get merged by the "apple-style-span" on delete/backspace + if (blockElm) { + node = blockElm.firstChild; - t.settings = s = tinymce.extend({ - keep_values : false, - hex_colors : 1, - process_html : 1 - }, s); + // Ignore empty text nodes + while (node && node.nodeType == 3 && node.nodeValue.length == 0) + node = node.nextSibling; - // Fix IE6SP2 flicker and check it failed for pre SP2 - if (tinymce.isIE6) { - try { - d.execCommand('BackgroundImageCache', false, true); - } catch (e) { - t.cssFlicker = true; + if (node && node.nodeName === 'SPAN') { + clonedSpan = node.cloneNode(false); + } } - } - // Build styles list - if (s.valid_styles) { - t._styles = {}; + // Do the backspace/delete actiopn + ed.getDoc().execCommand(isDelete ? 'ForwardDelete' : 'Delete', false, null); + + // Find all odd apple-style-spans + blockElm = dom.getParent(rng.startContainer, dom.isBlock); + tinymce.each(dom.select('span.Apple-style-span,font.Apple-style-span', blockElm), function(span) { + var bm = selection.getBookmark(); + + if (clonedSpan) { + dom.replace(clonedSpan.cloneNode(false), span, true); + } else { + dom.remove(span, true); + } - // Convert styles into a rule list - each(s.valid_styles, function(value, key) { - t._styles[key] = tinymce.explode(value); + // Restore the selection + selection.moveToBookmark(bm); }); } + }); + }; - tinymce.addUnload(t.destroy, t); - }, + function emptyEditorWhenDeleting(ed) { + ed.onKeyUp.add(function(ed, e) { + var keyCode = e.keyCode; - getRoot : function() { - var t = this, s = t.settings; + if (keyCode == DELETE || keyCode == BACKSPACE) { + if (ed.dom.isEmpty(ed.getBody())) { + ed.setContent('', {format : 'raw'}); + ed.nodeChanged(); + return; + } + } + }); + }; - return (s && t.get(s.root_element)) || t.doc.body; - }, + function inputMethodFocus(ed) { + ed.dom.bind(ed.getDoc(), 'focusin', function() { + ed.selection.setRng(ed.selection.getRng()); + }); + }; - getViewPort : function(w) { - var d, b; + function removeHrOnBackspace(ed) { + ed.onKeyDown.add(function(ed, e) { + if (e.keyCode === BACKSPACE) { + if (ed.selection.isCollapsed() && ed.selection.getRng(true).startOffset === 0) { + var node = ed.selection.getNode(); + var previousSibling = node.previousSibling; + if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "hr") { + ed.dom.remove(previousSibling); + tinymce.dom.Event.cancel(e); + } + } + } + }) + } - w = !w ? this.win : w; - d = w.document; - b = this.boxModel ? d.documentElement : d.body; + function focusBody(ed) { + // Fix for a focus bug in FF 3.x where the body element + // wouldn't get proper focus if the user clicked on the HTML element + if (!Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4 + ed.onMouseDown.add(function(ed, e) { + if (e.target.nodeName === "HTML") { + var body = ed.getBody(); + + // Blur the body it's focused but not correctly focused + body.blur(); + + // Refocus the body after a little while + setTimeout(function() { + body.focus(); + }, 0); + } + }); + } + }; - // Returns viewport size excluding scrollbars - return { - x : w.pageXOffset || b.scrollLeft, - y : w.pageYOffset || b.scrollTop, - w : w.innerWidth || b.clientWidth, - h : w.innerHeight || b.clientHeight - }; - }, + function selectControlElements(ed) { + ed.onClick.add(function(ed, e) { + e = e.target; - getRect : function(e) { - var p, t = this, sr; + // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250 + // WebKit can't even do simple things like selecting an image + // Needs tobe the setBaseAndExtend or it will fail to select floated images + if (/^(IMG|HR)$/.test(e.nodeName)) + ed.selection.getSel().setBaseAndExtent(e, 0, e, 1); - e = t.get(e); - p = t.getPos(e); - sr = t.getSize(e); + if (e.nodeName == 'A' && ed.dom.hasClass(e, 'mceItemAnchor')) + ed.selection.select(e); - return { - x : p.x, - y : p.y, - w : sr.w, - h : sr.h - }; - }, + ed.nodeChanged(); + }); + }; - getSize : function(e) { - var t = this, w, h; + function selectionChangeNodeChanged(ed) { + var lastRng, selectionTimer; - e = t.get(e); - w = t.getStyle(e, 'width'); - h = t.getStyle(e, 'height'); + ed.dom.bind(ed.getDoc(), 'selectionchange', function() { + if (selectionTimer) { + clearTimeout(selectionTimer); + selectionTimer = 0; + } - // Non pixel value, then force offset/clientWidth - if (w.indexOf('px') === -1) - w = 0; + selectionTimer = window.setTimeout(function() { + var rng = ed.selection.getRng(); - // Non pixel value, then force offset/clientWidth - if (h.indexOf('px') === -1) - h = 0; + // Compare the ranges to see if it was a real change or not + if (!lastRng || !tinymce.dom.RangeUtils.compareRanges(rng, lastRng)) { + ed.nodeChanged(); + lastRng = rng; + } + }, 50); + }); + } - return { - w : parseInt(w) || e.offsetWidth || e.clientWidth, - h : parseInt(h) || e.offsetHeight || e.clientHeight - }; - }, + function ensureBodyHasRoleApplication(ed) { + document.body.setAttribute("role", "application"); + } - getParent : function(n, f, r) { - return this.getParents(n, f, r, false); - }, + tinymce.create('tinymce.util.Quirks', { + Quirks: function(ed) { + // WebKit + if (tinymce.isWebKit) { + cleanupStylesWhenDeleting(ed); + emptyEditorWhenDeleting(ed); + inputMethodFocus(ed); + selectControlElements(ed); + + // iOS + if (tinymce.isIDevice) { + selectionChangeNodeChanged(ed); + } + } - getParents : function(n, f, r, c) { - var t = this, na, se = t.settings, o = []; + // IE + if (tinymce.isIE) { + removeHrOnBackspace(ed); + emptyEditorWhenDeleting(ed); + ensureBodyHasRoleApplication(ed); + } - n = t.get(n); - c = c === undefined; + // Gecko + if (tinymce.isGecko) { + removeHrOnBackspace(ed); + focusBody(ed); + } + } + }); +})(tinymce); - if (se.strict_root) - r = r || t.getRoot(); +(function(tinymce) { + var namedEntities, baseEntities, reverseEntities, + attrsCharsRegExp = /[&<>\"\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, + textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, + rawCharsRegExp = /[<>&\"\']/g, + entityRegExp = /&(#x|#)?([\w]+);/g, + asciiMap = { + 128 : "\u20AC", 130 : "\u201A", 131 : "\u0192", 132 : "\u201E", 133 : "\u2026", 134 : "\u2020", + 135 : "\u2021", 136 : "\u02C6", 137 : "\u2030", 138 : "\u0160", 139 : "\u2039", 140 : "\u0152", + 142 : "\u017D", 145 : "\u2018", 146 : "\u2019", 147 : "\u201C", 148 : "\u201D", 149 : "\u2022", + 150 : "\u2013", 151 : "\u2014", 152 : "\u02DC", 153 : "\u2122", 154 : "\u0161", 155 : "\u203A", + 156 : "\u0153", 158 : "\u017E", 159 : "\u0178" + }; - // Wrap node name as func - if (is(f, 'string')) { - na = f; + // Raw entities + baseEntities = { + '\"' : '"', // Needs to be escaped since the YUI compressor would otherwise break the code + "'" : ''', + '<' : '<', + '>' : '>', + '&' : '&' + }; - if (f === '*') { - f = function(n) {return n.nodeType == 1;}; - } else { - f = function(n) { - return t.is(n, na); - }; - } - } + // Reverse lookup table for raw entities + reverseEntities = { + '<' : '<', + '>' : '>', + '&' : '&', + '"' : '"', + ''' : "'" + }; - while (n) { - if (n == r || !n.nodeType || n.nodeType === 9) - break; + // Decodes text by using the browser + function nativeDecode(text) { + var elm; - if (!f || f(n)) { - if (c) - o.push(n); - else - return n; - } + elm = document.createElement("div"); + elm.innerHTML = text; - n = n.parentNode; - } + return elm.textContent || elm.innerText || text; + }; - return c ? o : null; - }, + // Build a two way lookup table for the entities + function buildEntitiesLookup(items, radix) { + var i, chr, entity, lookup = {}; - get : function(e) { - var n; + if (items) { + items = items.split(','); + radix = radix || 10; - if (e && this.doc && typeof(e) == 'string') { - n = e; - e = this.doc.getElementById(e); + // Build entities lookup table + for (i = 0; i < items.length; i += 2) { + chr = String.fromCharCode(parseInt(items[i], radix)); - // IE and Opera returns meta elements when they match the specified input ID, but getElementsByName seems to do the trick - if (e && e.id !== n) - return this.doc.getElementsByName(n)[1]; + // Only add non base entities + if (!baseEntities[chr]) { + entity = '&' + items[i + 1] + ';'; + lookup[chr] = entity; + lookup[entity] = chr; + } } - return e; - }, + return lookup; + } + }; - getNext : function(node, selector) { - return this._findSib(node, selector, 'nextSibling'); + // Unpack entities lookup where the numbers are in radix 32 to reduce the size + namedEntities = buildEntitiesLookup( + '50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' + + '5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' + + '5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' + + '5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' + + '68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' + + '6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' + + '6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' + + '75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' + + '7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' + + '7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' + + 'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' + + 'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' + + 't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' + + 'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' + + 'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' + + '81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' + + '8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' + + '8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' + + '8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' + + '8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' + + 'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' + + 'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' + + 'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' + + '80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' + + '811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro' + , 32); + + tinymce.html = tinymce.html || {}; + + tinymce.html.Entities = { + encodeRaw : function(text, attr) { + return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { + return baseEntities[chr] || chr; + }); }, - getPrev : function(node, selector) { - return this._findSib(node, selector, 'previousSibling'); + encodeAllRaw : function(text) { + return ('' + text).replace(rawCharsRegExp, function(chr) { + return baseEntities[chr] || chr; + }); }, + encodeNumeric : function(text, attr) { + return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { + // Multi byte sequence convert it to a single entity + if (chr.length > 1) + return '&#' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';'; - select : function(pa, s) { - var t = this; + return baseEntities[chr] || '&#' + chr.charCodeAt(0) + ';'; + }); + }, - return tinymce.dom.Sizzle(pa, t.get(s) || t.get(t.settings.root_element) || t.doc, []); + encodeNamed : function(text, attr, entities) { + entities = entities || namedEntities; + + return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { + return baseEntities[chr] || entities[chr] || chr; + }); }, - is : function(n, selector) { - var i; + getEncodeFunc : function(name, entities) { + var Entities = tinymce.html.Entities; - // If it isn't an array then try to do some simple selectors instead of Sizzle for to boost performance - if (n.length === undefined) { - // Simple all selector - if (selector === '*') - return n.nodeType == 1; + entities = buildEntitiesLookup(entities) || namedEntities; - // Simple selector just elements - if (simpleSelectorRe.test(selector)) { - selector = selector.toLowerCase().split(/,/); - n = n.nodeName.toLowerCase(); + function encodeNamedAndNumeric(text, attr) { + return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { + return baseEntities[chr] || entities[chr] || '&#' + chr.charCodeAt(0) + ';' || chr; + }); + }; - for (i = selector.length - 1; i >= 0; i--) { - if (selector[i] == n) - return true; - } - - return false; - } - } - - return tinymce.dom.Sizzle.matches(selector, n.nodeType ? [n] : n).length > 0; - }, + function encodeCustomNamed(text, attr) { + return Entities.encodeNamed(text, attr, entities); + }; + // Replace + with , to be compatible with previous TinyMCE versions + name = tinymce.makeMap(name.replace(/\+/g, ',')); - add : function(p, n, a, h, c) { - var t = this; + // Named and numeric encoder + if (name.named && name.numeric) + return encodeNamedAndNumeric; - return this.run(p, function(p) { - var e, k; + // Named encoder + if (name.named) { + // Custom names + if (entities) + return encodeCustomNamed; - e = is(n, 'string') ? t.doc.createElement(n) : n; - t.setAttribs(e, a); + return Entities.encodeNamed; + } - if (h) { - if (h.nodeType) - e.appendChild(h); - else - t.setHTML(e, h); - } + // Numeric + if (name.numeric) + return Entities.encodeNumeric; - return !c ? p.appendChild(e) : e; - }); + // Raw encoder + return Entities.encodeRaw; }, - create : function(n, a, h) { - return this.add(this.doc.createElement(n), n, a, h, 1); - }, + decode : function(text) { + return text.replace(entityRegExp, function(all, numeric, value) { + if (numeric) { + value = parseInt(value, numeric.length === 2 ? 16 : 10); - createHTML : function(n, a, h) { - var o = '', t = this, k; + // Support upper UTF + if (value > 0xFFFF) { + value -= 0x10000; - o += '<' + n; + return String.fromCharCode(0xD800 + (value >> 10), 0xDC00 + (value & 0x3FF)); + } else + return asciiMap[value] || String.fromCharCode(value); + } - for (k in a) { - if (a.hasOwnProperty(k)) - o += ' ' + k + '="' + t.encode(a[k]) + '"'; - } + return reverseEntities[all] || namedEntities[all] || nativeDecode(all); + }); + } + }; +})(tinymce); - if (tinymce.is(h)) - return o + '>' + h + ''; +tinymce.html.Styles = function(settings, schema) { + var rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi, + urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi, + styleRegExp = /\s*([^:]+):\s*([^;]+);?/g, + trimRightRegExp = /\s+$/, + urlColorRegExp = /rgb/, + undef, i, encodingLookup = {}, encodingItems; - return o + ' />'; - }, + settings = settings || {}; - remove : function(node, keep_children) { - return this.run(node, function(node) { - var parent, child; + encodingItems = '\\" \\\' \\; \\: ; : \uFEFF'.split(' '); + for (i = 0; i < encodingItems.length; i++) { + encodingLookup[encodingItems[i]] = '\uFEFF' + i; + encodingLookup['\uFEFF' + i] = encodingItems[i]; + } - parent = node.parentNode; + function toHex(match, r, g, b) { + function hex(val) { + val = parseInt(val).toString(16); - if (!parent) - return null; + return val.length > 1 ? val : '0' + val; // 0 -> 00 + }; - if (keep_children) { - while (child = node.firstChild) { - // IE 8 will crash if you don't remove completely empty text nodes - if (!tinymce.isIE || child.nodeType !== 3 || child.nodeValue) - parent.insertBefore(child, node); - else - node.removeChild(child); - } - } + return '#' + hex(r) + hex(g) + hex(b); + }; - return parent.removeChild(node); - }); + return { + toHex : function(color) { + return color.replace(rgbRegExp, toHex); }, - setStyle : function(n, na, v) { - var t = this; - - return t.run(n, function(e) { - var s, i; + parse : function(css) { + var styles = {}, matches, name, value, isEncoded, urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope || this; - s = e.style; + function compress(prefix, suffix) { + var top, right, bottom, left; - // Camelcase it, if needed - na = na.replace(/-(\D)/g, function(a, b){ - return b.toUpperCase(); - }); + // Get values and check it it needs compressing + top = styles[prefix + '-top' + suffix]; + if (!top) + return; - // Default px suffix on these - if (t.pixelStyles.test(na) && (tinymce.is(v, 'number') || /^[\-0-9\.]+$/.test(v))) - v += 'px'; + right = styles[prefix + '-right' + suffix]; + if (top != right) + return; - switch (na) { - case 'opacity': - // IE specific opacity - if (isIE) { - s.filter = v === '' ? '' : "alpha(opacity=" + (v * 100) + ")"; + bottom = styles[prefix + '-bottom' + suffix]; + if (right != bottom) + return; - if (!n.currentStyle || !n.currentStyle.hasLayout) - s.display = 'inline-block'; - } + left = styles[prefix + '-left' + suffix]; + if (bottom != left) + return; - // Fix for older browsers - s[na] = s['-moz-opacity'] = s['-khtml-opacity'] = v || ''; - break; + // Compress + styles[prefix + suffix] = left; + delete styles[prefix + '-top' + suffix]; + delete styles[prefix + '-right' + suffix]; + delete styles[prefix + '-bottom' + suffix]; + delete styles[prefix + '-left' + suffix]; + }; - case 'float': - isIE ? s.styleFloat = v : s.cssFloat = v; - break; - - default: - s[na] = v || ''; - } + function canCompress(key) { + var value = styles[key], i; - // Force update of the style data - if (t.settings.update_styles) - t.setAttrib(e, '_mce_style'); - }); - }, + if (!value || value.indexOf(' ') < 0) + return; - getStyle : function(n, na, c) { - n = this.get(n); + value = value.split(' '); + i = value.length; + while (i--) { + if (value[i] !== value[0]) + return false; + } - if (!n) - return false; + styles[key] = value[0]; - // Gecko - if (this.doc.defaultView && c) { - // Remove camelcase - na = na.replace(/[A-Z]/g, function(a){ - return '-' + a; - }); + return true; + }; - try { - return this.doc.defaultView.getComputedStyle(n, null).getPropertyValue(na); - } catch (ex) { - // Old safari might fail - return null; - } - } + function compress2(target, a, b, c) { + if (!canCompress(a)) + return; - // Camelcase it, if needed - na = na.replace(/-(\D)/g, function(a, b){ - return b.toUpperCase(); - }); + if (!canCompress(b)) + return; - if (na == 'float') - na = isIE ? 'styleFloat' : 'cssFloat'; + if (!canCompress(c)) + return; - // IE & Opera - if (n.currentStyle && c) - return n.currentStyle[na]; + // Compress + styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c]; + delete styles[a]; + delete styles[b]; + delete styles[c]; + }; - return n.style[na]; - }, + // Encodes the specified string by replacing all \" \' ; : with _ + function encode(str) { + isEncoded = true; - setStyles : function(e, o) { - var t = this, s = t.settings, ol; + return encodingLookup[str]; + }; - ol = s.update_styles; - s.update_styles = 0; + // Decodes the specified string by replacing all _ with it's original value \" \' etc + // It will also decode the \" \' if keep_slashes is set to fale or omitted + function decode(str, keep_slashes) { + if (isEncoded) { + str = str.replace(/\uFEFF[0-9]/g, function(str) { + return encodingLookup[str]; + }); + } - each(o, function(v, n) { - t.setStyle(e, n, v); - }); + if (!keep_slashes) + str = str.replace(/\\([\'\";:])/g, "$1"); - // Update style info - s.update_styles = ol; - if (s.update_styles) - t.setAttrib(e, s.cssText); - }, + return str; + } - setAttrib : function(e, n, v) { - var t = this; + if (css) { + // Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing + css = css.replace(/\\[\"\';:\uFEFF]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, function(str) { + return str.replace(/[;:]/g, encode); + }); - // Whats the point - if (!e || !n) - return; + // Parse styles + while (matches = styleRegExp.exec(css)) { + name = matches[1].replace(trimRightRegExp, '').toLowerCase(); + value = matches[2].replace(trimRightRegExp, ''); - // Strict XML mode - if (t.settings.strict) - n = n.toLowerCase(); + if (name && value.length > 0) { + // Opera will produce 700 instead of bold in their style values + if (name === 'font-weight' && value === '700') + value = 'bold'; + else if (name === 'color' || name === 'background-color') // Lowercase colors like RED + value = value.toLowerCase(); - return this.run(e, function(e) { - var s = t.settings; + // Convert RGB colors to HEX + value = value.replace(rgbRegExp, toHex); - switch (n) { - case "style": - if (!is(v, 'string')) { - each(v, function(v, n) { - t.setStyle(e, n, v); - }); + // Convert URLs and force them into url('value') format + value = value.replace(urlOrStrRegExp, function(match, url, url2, url3, str, str2) { + str = str || str2; - return; - } + if (str) { + str = decode(str); - // No mce_style for elements with these since they might get resized by the user - if (s.keep_values) { - if (v && !t._isRes(v)) - e.setAttribute('_mce_style', v, 2); - else - e.removeAttribute('_mce_style', 2); - } + // Force strings into single quote format + return "'" + str.replace(/\'/g, "\\'") + "'"; + } - e.style.cssText = v; - break; + url = decode(url || url2 || url3); - case "class": - e.className = v || ''; // Fix IE null bug - break; + // Convert the URL to relative/absolute depending on config + if (urlConverter) + url = urlConverter.call(urlConverterScope, url, 'style'); - case "src": - case "href": - if (s.keep_values) { - if (s.url_converter) - v = s.url_converter.call(s.url_converter_scope || t, v, n, e); + // Output new URL format + return "url('" + url.replace(/\'/g, "\\'") + "')"; + }); - t.setAttrib(e, '_mce_' + n, v, 2); - } + styles[name] = isEncoded ? decode(value, true) : value; + } - break; - - case "shape": - e.setAttribute('_mce_style', v); - break; + styleRegExp.lastIndex = matches.index + matches[0].length; } - if (is(v) && v !== null && v.length !== 0) - e.setAttribute(n, '' + v, 2); - else - e.removeAttribute(n, 2); - }); - }, + // Compress the styles to reduce it's size for example IE will expand styles + compress("border", ""); + compress("border", "-width"); + compress("border", "-color"); + compress("border", "-style"); + compress("padding", ""); + compress("margin", ""); + compress2('border', 'border-width', 'border-style', 'border-color'); - setAttribs : function(e, o) { - var t = this; + // Remove pointless border, IE produces these + if (styles.border === 'medium none') + delete styles.border; + } - return this.run(e, function(e) { - each(o, function(v, n) { - t.setAttrib(e, n, v); - }); - }); + return styles; }, - getAttrib : function(e, n, dv) { - var v, t = this; + serialize : function(styles, element_name) { + var css = '', name, value; - e = t.get(e); + function serializeStyles(name) { + var styleList, i, l, value; - if (!e || e.nodeType !== 1) - return false; + styleList = schema.styles[name]; + if (styleList) { + for (i = 0, l = styleList.length; i < l; i++) { + name = styleList[i]; + value = styles[name]; - if (!is(dv)) - dv = ''; + if (value !== undef && value.length > 0) + css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; + } + } + }; - // Try the mce variant for these - if (/^(src|href|style|coords|shape)$/.test(n)) { - v = e.getAttribute("_mce_" + n); + // Serialize styles according to schema + if (element_name && schema && schema.styles) { + // Serialize global styles and element specific styles + serializeStyles('*'); + serializeStyles(element_name); + } else { + // Output the styles in the order they are inside the object + for (name in styles) { + value = styles[name]; - if (v) - return v; + if (value !== undef && value.length > 0) + css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; + } } - if (isIE && t.props[n]) { - v = e[t.props[n]]; - v = v && v.nodeValue ? v.nodeValue : v; - } + return css; + } + }; +}; - if (!v) - v = e.getAttribute(n, 2); +(function(tinymce) { + var transitional = {}, boolAttrMap, blockElementsMap, shortEndedElementsMap, nonEmptyElementsMap, customElementsMap = {}, + defaultWhiteSpaceElementsMap, selfClosingElementsMap, makeMap = tinymce.makeMap, each = tinymce.each; - // Check boolean attribs - if (/^(checked|compact|declare|defer|disabled|ismap|multiple|nohref|noshade|nowrap|readonly|selected)$/.test(n)) { - if (e[t.props[n]] === true && v === '') - return n; + function split(str, delim) { + return str.split(delim || ','); + }; - return v ? n : ''; - } + function unpack(lookup, data) { + var key, elements = {}; - // Inner input elements will override attributes on form elements - if (e.nodeName === "FORM" && e.getAttributeNode(n)) - return e.getAttributeNode(n).nodeValue; + function replace(value) { + return value.replace(/[A-Z]+/g, function(key) { + return replace(lookup[key]); + }); + }; - if (n === 'style') { - v = v || e.style.cssText; + // Unpack lookup + for (key in lookup) { + if (lookup.hasOwnProperty(key)) + lookup[key] = replace(lookup[key]); + } - if (v) { - v = t.serializeStyle(t.parseStyle(v), e.nodeName); + // Unpack and parse data into object map + replace(data).replace(/#/g, '#text').replace(/(\w+)\[([^\]]+)\]\[([^\]]*)\]/g, function(str, name, attributes, children) { + attributes = split(attributes, '|'); - if (t.settings.keep_values && !t._isRes(v)) - e.setAttribute('_mce_style', v); - } + elements[name] = { + attributes : makeMap(attributes), + attributesOrder : attributes, + children : makeMap(children, '|', {'#comment' : {}}) } + }); - // Remove Apple and WebKit stuff - if (isWebKit && n === "class" && v) - v = v.replace(/(apple|webkit)\-[a-z\-]+/gi, ''); + return elements; + }; - // Handle IE issues - if (isIE) { - switch (n) { - case 'rowspan': - case 'colspan': - // IE returns 1 as default value - if (v === 1) - v = ''; + // Build a lookup table for block elements both lowercase and uppercase + blockElementsMap = 'h1,h2,h3,h4,h5,h6,hr,p,div,address,pre,form,table,tbody,thead,tfoot,' + + 'th,tr,td,li,ol,ul,caption,blockquote,center,dl,dt,dd,dir,fieldset,' + + 'noscript,menu,isindex,samp,header,footer,article,section,hgroup'; + blockElementsMap = makeMap(blockElementsMap, ',', makeMap(blockElementsMap.toUpperCase())); + + // This is the XHTML 1.0 transitional elements with it's attributes and children packed to reduce it's size + transitional = unpack({ + Z : 'H|K|N|O|P', + Y : 'X|form|R|Q', + ZG : 'E|span|width|align|char|charoff|valign', + X : 'p|T|div|U|W|isindex|fieldset|table', + ZF : 'E|align|char|charoff|valign', + W : 'pre|hr|blockquote|address|center|noframes', + ZE : 'abbr|axis|headers|scope|rowspan|colspan|align|char|charoff|valign|nowrap|bgcolor|width|height', + ZD : '[E][S]', + U : 'ul|ol|dl|menu|dir', + ZC : 'p|Y|div|U|W|table|br|span|bdo|object|applet|img|map|K|N|Q', + T : 'h1|h2|h3|h4|h5|h6', + ZB : 'X|S|Q', + S : 'R|P', + ZA : 'a|G|J|M|O|P', + R : 'a|H|K|N|O', + Q : 'noscript|P', + P : 'ins|del|script', + O : 'input|select|textarea|label|button', + N : 'M|L', + M : 'em|strong|dfn|code|q|samp|kbd|var|cite|abbr|acronym', + L : 'sub|sup', + K : 'J|I', + J : 'tt|i|b|u|s|strike', + I : 'big|small|font|basefont', + H : 'G|F', + G : 'br|span|bdo', + F : 'object|applet|img|map|iframe', + E : 'A|B|C', + D : 'accesskey|tabindex|onfocus|onblur', + C : 'onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup', + B : 'lang|xml:lang|dir', + A : 'id|class|style|title' + }, 'script[id|charset|type|language|src|defer|xml:space][]' + + 'style[B|id|type|media|title|xml:space][]' + + 'object[E|declare|classid|codebase|data|type|codetype|archive|standby|width|height|usemap|name|tabindex|align|border|hspace|vspace][#|param|Y]' + + 'param[id|name|value|valuetype|type][]' + + 'p[E|align][#|S]' + + 'a[E|D|charset|type|name|href|hreflang|rel|rev|shape|coords|target][#|Z]' + + 'br[A|clear][]' + + 'span[E][#|S]' + + 'bdo[A|C|B][#|S]' + + 'applet[A|codebase|archive|code|object|alt|name|width|height|align|hspace|vspace][#|param|Y]' + + 'h1[E|align][#|S]' + + 'img[E|src|alt|name|longdesc|width|height|usemap|ismap|align|border|hspace|vspace][]' + + 'map[B|C|A|name][X|form|Q|area]' + + 'h2[E|align][#|S]' + + 'iframe[A|longdesc|name|src|frameborder|marginwidth|marginheight|scrolling|align|width|height][#|Y]' + + 'h3[E|align][#|S]' + + 'tt[E][#|S]' + + 'i[E][#|S]' + + 'b[E][#|S]' + + 'u[E][#|S]' + + 's[E][#|S]' + + 'strike[E][#|S]' + + 'big[E][#|S]' + + 'small[E][#|S]' + + 'font[A|B|size|color|face][#|S]' + + 'basefont[id|size|color|face][]' + + 'em[E][#|S]' + + 'strong[E][#|S]' + + 'dfn[E][#|S]' + + 'code[E][#|S]' + + 'q[E|cite][#|S]' + + 'samp[E][#|S]' + + 'kbd[E][#|S]' + + 'var[E][#|S]' + + 'cite[E][#|S]' + + 'abbr[E][#|S]' + + 'acronym[E][#|S]' + + 'sub[E][#|S]' + + 'sup[E][#|S]' + + 'input[E|D|type|name|value|checked|disabled|readonly|size|maxlength|src|alt|usemap|onselect|onchange|accept|align][]' + + 'select[E|name|size|multiple|disabled|tabindex|onfocus|onblur|onchange][optgroup|option]' + + 'optgroup[E|disabled|label][option]' + + 'option[E|selected|disabled|label|value][]' + + 'textarea[E|D|name|rows|cols|disabled|readonly|onselect|onchange][]' + + 'label[E|for|accesskey|onfocus|onblur][#|S]' + + 'button[E|D|name|value|type|disabled][#|p|T|div|U|W|table|G|object|applet|img|map|K|N|Q]' + + 'h4[E|align][#|S]' + + 'ins[E|cite|datetime][#|Y]' + + 'h5[E|align][#|S]' + + 'del[E|cite|datetime][#|Y]' + + 'h6[E|align][#|S]' + + 'div[E|align][#|Y]' + + 'ul[E|type|compact][li]' + + 'li[E|type|value][#|Y]' + + 'ol[E|type|compact|start][li]' + + 'dl[E|compact][dt|dd]' + + 'dt[E][#|S]' + + 'dd[E][#|Y]' + + 'menu[E|compact][li]' + + 'dir[E|compact][li]' + + 'pre[E|width|xml:space][#|ZA]' + + 'hr[E|align|noshade|size|width][]' + + 'blockquote[E|cite][#|Y]' + + 'address[E][#|S|p]' + + 'center[E][#|Y]' + + 'noframes[E][#|Y]' + + 'isindex[A|B|prompt][]' + + 'fieldset[E][#|legend|Y]' + + 'legend[E|accesskey|align][#|S]' + + 'table[E|summary|width|border|frame|rules|cellspacing|cellpadding|align|bgcolor][caption|col|colgroup|thead|tfoot|tbody|tr]' + + 'caption[E|align][#|S]' + + 'col[ZG][]' + + 'colgroup[ZG][col]' + + 'thead[ZF][tr]' + + 'tr[ZF|bgcolor][th|td]' + + 'th[E|ZE][#|Y]' + + 'form[E|action|method|name|enctype|onsubmit|onreset|accept|accept-charset|target][#|X|R|Q]' + + 'noscript[E][#|Y]' + + 'td[E|ZE][#|Y]' + + 'tfoot[ZF][tr]' + + 'tbody[ZF][tr]' + + 'area[E|D|shape|coords|href|nohref|alt|target][]' + + 'base[id|href|target][]' + + 'body[E|onload|onunload|background|bgcolor|text|link|vlink|alink][#|Y]' + ); - break; + boolAttrMap = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected,autoplay,loop,controls'); + shortEndedElementsMap = makeMap('area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed,source'); + nonEmptyElementsMap = tinymce.extend(makeMap('td,th,iframe,video,audio,object'), shortEndedElementsMap); + defaultWhiteSpaceElementsMap = makeMap('pre,script,style,textarea'); + selfClosingElementsMap = makeMap('colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr'); - case 'size': - // IE returns +0 as default value for size - if (v === '+0' || v === 20 || v === 0) - v = ''; + tinymce.html.Schema = function(settings) { + var self = this, elements = {}, children = {}, patternElements = [], validStyles, whiteSpaceElementsMap; - break; + settings = settings || {}; - case 'width': - case 'height': - case 'vspace': - case 'checked': - case 'disabled': - case 'readonly': - if (v === 0) - v = ''; + // Allow all elements and attributes if verify_html is set to false + if (settings.verify_html === false) + settings.valid_elements = '*[*]'; - break; + // Build styles list + if (settings.valid_styles) { + validStyles = {}; - case 'hspace': - // IE returns -1 as default value - if (v === -1) - v = ''; + // Convert styles into a rule list + each(settings.valid_styles, function(value, key) { + validStyles[key] = tinymce.explode(value); + }); + } - break; + whiteSpaceElementsMap = settings.whitespace_elements ? makeMap(settings.whitespace_elements) : defaultWhiteSpaceElementsMap; - case 'maxlength': - case 'tabindex': - // IE returns default value - if (v === 32768 || v === 2147483647 || v === '32768') - v = ''; + // Converts a wildcard expression string to a regexp for example *a will become /.*a/. + function patternToRegExp(str) { + return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$'); + }; - break; + // Parses the specified valid_elements string and adds to the current rules + // This function is a bit hard to read since it's heavily optimized for speed + function addValidElements(valid_elements) { + var ei, el, ai, al, yl, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder, + prefix, outputName, globalAttributes, globalAttributesOrder, transElement, key, childKey, value, + elementRuleRegExp = /^([#+-])?([^\[\/]+)(?:\/([^\[]+))?(?:\[([^\]]+)\])?$/, + attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/, + hasPatternsRegExp = /[*?+]/; + + if (valid_elements) { + // Split valid elements into an array with rules + valid_elements = split(valid_elements); + + if (elements['@']) { + globalAttributes = elements['@'].attributes; + globalAttributesOrder = elements['@'].attributesOrder; + } - case 'multiple': - case 'compact': - case 'noshade': - case 'nowrap': - if (v === 65535) - return n; + // Loop all rules + for (ei = 0, el = valid_elements.length; ei < el; ei++) { + // Parse element rule + matches = elementRuleRegExp.exec(valid_elements[ei]); + if (matches) { + // Setup local names for matches + prefix = matches[1]; + elementName = matches[2]; + outputName = matches[3]; + attrData = matches[4]; + + // Create new attributes and attributesOrder + attributes = {}; + attributesOrder = []; + + // Create the new element + element = { + attributes : attributes, + attributesOrder : attributesOrder + }; - return dv; + // Padd empty elements prefix + if (prefix === '#') + element.paddEmpty = true; - case 'shape': - v = v.toLowerCase(); - break; + // Remove empty elements prefix + if (prefix === '-') + element.removeEmpty = true; - default: - // IE has odd anonymous function for event attributes - if (n.indexOf('on') === 0 && v) - v = ('' + v).replace(/^function\s+\w+\(\)\s+\{\s+(.*)\s+\}$/, '$1'); - } - } + // Copy attributes from global rule into current rule + if (globalAttributes) { + for (key in globalAttributes) + attributes[key] = globalAttributes[key]; - return (v !== undefined && v !== null && v !== '') ? '' + v : dv; - }, + attributesOrder.push.apply(attributesOrder, globalAttributesOrder); + } - getPos : function(n, ro) { - var t = this, x = 0, y = 0, e, d = t.doc, r; + // Attributes defined + if (attrData) { + attrData = split(attrData, '|'); + for (ai = 0, al = attrData.length; ai < al; ai++) { + matches = attrRuleRegExp.exec(attrData[ai]); + if (matches) { + attr = {}; + attrType = matches[1]; + attrName = matches[2].replace(/::/g, ':'); + prefix = matches[3]; + value = matches[4]; + + // Required + if (attrType === '!') { + element.attributesRequired = element.attributesRequired || []; + element.attributesRequired.push(attrName); + attr.required = true; + } - n = t.get(n); - ro = ro || d.body; + // Denied from global + if (attrType === '-') { + delete attributes[attrName]; + attributesOrder.splice(tinymce.inArray(attributesOrder, attrName), 1); + continue; + } - if (n) { - // Use getBoundingClientRect on IE, Opera has it but it's not perfect - if (isIE && !t.stdMode) { - n = n.getBoundingClientRect(); - e = t.boxModel ? d.documentElement : d.body; - x = t.getStyle(t.select('html')[0], 'borderWidth'); // Remove border - x = (x == 'medium' || t.boxModel && !t.isIE6) && 2 || x; - n.top += t.win.self != t.win.top ? 2 : 0; // IE adds some strange extra cord if used in a frameset + // Default value + if (prefix) { + // Default value + if (prefix === '=') { + element.attributesDefault = element.attributesDefault || []; + element.attributesDefault.push({name: attrName, value: value}); + attr.defaultValue = value; + } - return {x : n.left + e.scrollLeft - x, y : n.top + e.scrollTop - x}; - } + // Forced value + if (prefix === ':') { + element.attributesForced = element.attributesForced || []; + element.attributesForced.push({name: attrName, value: value}); + attr.forcedValue = value; + } - r = n; - while (r && r != ro && r.nodeType) { - x += r.offsetLeft || 0; - y += r.offsetTop || 0; - r = r.offsetParent; - } + // Required values + if (prefix === '<') + attr.validValues = makeMap(value, '?'); + } - r = n.parentNode; - while (r && r != ro && r.nodeType) { - x -= r.scrollLeft || 0; - y -= r.scrollTop || 0; - r = r.parentNode; - } - } + // Check for attribute patterns + if (hasPatternsRegExp.test(attrName)) { + element.attributePatterns = element.attributePatterns || []; + attr.pattern = patternToRegExp(attrName); + element.attributePatterns.push(attr); + } else { + // Add attribute to order list if it doesn't already exist + if (!attributes[attrName]) + attributesOrder.push(attrName); - return {x : x, y : y}; - }, + attributes[attrName] = attr; + } + } + } + } - parseStyle : function(st) { - var t = this, s = t.settings, o = {}; + // Global rule, store away these for later usage + if (!globalAttributes && elementName == '@') { + globalAttributes = attributes; + globalAttributesOrder = attributesOrder; + } - if (!st) - return o; + // Handle substitute elements such as b/strong + if (outputName) { + element.outputName = elementName; + elements[outputName] = element; + } - function compress(p, s, ot) { - var t, r, b, l; + // Add pattern or exact element + if (hasPatternsRegExp.test(elementName)) { + element.pattern = patternToRegExp(elementName); + patternElements.push(element); + } else + elements[elementName] = element; + } + } + } + }; - // Get values and check it it needs compressing - t = o[p + '-top' + s]; - if (!t) - return; + function setValidElements(valid_elements) { + elements = {}; + patternElements = []; - r = o[p + '-right' + s]; - if (t != r) - return; + addValidElements(valid_elements); - b = o[p + '-bottom' + s]; - if (r != b) - return; + each(transitional, function(element, name) { + children[name] = element.children; + }); + }; - l = o[p + '-left' + s]; - if (b != l) - return; + // Adds custom non HTML elements to the schema + function addCustomElements(custom_elements) { + var customElementRegExp = /^(~)?(.+)$/; - // Compress - o[ot] = l; - delete o[p + '-top' + s]; - delete o[p + '-right' + s]; - delete o[p + '-bottom' + s]; - delete o[p + '-left' + s]; - }; + if (custom_elements) { + each(split(custom_elements), function(rule) { + var matches = customElementRegExp.exec(rule), + inline = matches[1] === '~', + cloneName = inline ? 'span' : 'div', + name = matches[2]; - function compress2(ta, a, b, c) { - var t; + children[name] = children[cloneName]; + customElementsMap[name] = cloneName; - t = o[a]; - if (!t) - return; + // If it's not marked as inline then add it to valid block elements + if (!inline) + blockElementsMap[name] = {}; - t = o[b]; - if (!t) - return; + // Add custom elements at span/div positions + each(children, function(element, child) { + if (element[cloneName]) + element[name] = element[cloneName]; + }); + }); + } + }; - t = o[c]; - if (!t) - return; + // Adds valid children to the schema object + function addValidChildren(valid_children) { + var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/; - // Compress - o[ta] = o[a] + ' ' + o[b] + ' ' + o[c]; - delete o[a]; - delete o[b]; - delete o[c]; - }; + if (valid_children) { + each(split(valid_children), function(rule) { + var matches = childRuleRegExp.exec(rule), parent, prefix; - st = st.replace(/&(#?[a-z0-9]+);/g, '&$1_MCE_SEMI_'); // Protect entities + if (matches) { + prefix = matches[1]; - each(st.split(';'), function(v) { - var sv, ur = []; + // Add/remove items from default + if (prefix) + parent = children[matches[2]]; + else + parent = children[matches[2]] = {'#comment' : {}}; - if (v) { - v = v.replace(/_MCE_SEMI_/g, ';'); // Restore entities - v = v.replace(/url\([^\)]+\)/g, function(v) {ur.push(v);return 'url(' + ur.length + ')';}); - v = v.split(':'); - sv = tinymce.trim(v[1]); - sv = sv.replace(/url\(([^\)]+)\)/g, function(a, b) {return ur[parseInt(b) - 1];}); - - sv = sv.replace(/rgb\([^\)]+\)/g, function(v) { - return t.toHex(v); - }); + parent = children[matches[2]]; - if (s.url_converter) { - sv = sv.replace(/url\([\'\"]?([^\)\'\"]+)[\'\"]?\)/g, function(x, c) { - return 'url(' + s.url_converter.call(s.url_converter_scope || t, t.decode(c), 'style', null) + ')'; + each(split(matches[3], '|'), function(child) { + if (prefix === '-') + delete parent[child]; + else + parent[child] = {}; }); } + }); + } + }; - o[tinymce.trim(v[0]).toLowerCase()] = sv; - } - }); + function getElementRule(name) { + var element = elements[name], i; - compress("border", "", "border"); - compress("border", "-width", "border-width"); - compress("border", "-color", "border-color"); - compress("border", "-style", "border-style"); - compress("padding", "", "padding"); - compress("margin", "", "margin"); - compress2('border', 'border-width', 'border-style', 'border-color'); + // Exact match found + if (element) + return element; - if (isIE) { - // Remove pointless border - if (o.border == 'medium none') - o.border = ''; + // No exact match then try the patterns + i = patternElements.length; + while (i--) { + element = patternElements[i]; + + if (element.pattern.test(name)) + return element; } + }; - return o; - }, + if (!settings.valid_elements) { + // No valid elements defined then clone the elements from the transitional spec + each(transitional, function(element, name) { + elements[name] = { + attributes : element.attributes, + attributesOrder : element.attributesOrder + }; - serializeStyle : function(o, name) { - var t = this, s = ''; + children[name] = element.children; + }); - function add(v, k) { - if (k && v) { - // Remove browser specific styles like -moz- or -webkit- - if (k.indexOf('-') === 0) - return; + // Switch these + each(split('strong/b,em/i'), function(item) { + item = split(item, '/'); + elements[item[1]].outputName = item[0]; + }); - switch (k) { - case 'font-weight': - // Opera will output bold as 700 - if (v == 700) - v = 'bold'; + // Add default alt attribute for images + elements.img.attributesDefault = [{name: 'alt', value: ''}]; - break; + // Remove these if they are empty by default + each(split('ol,ul,sub,sup,blockquote,span,font,a,table,tbody,tr'), function(name) { + elements[name].removeEmpty = true; + }); - case 'color': - case 'background-color': - v = v.toLowerCase(); - break; - } + // Padd these by default + each(split('p,h1,h2,h3,h4,h5,h6,th,td,pre,div,address,caption'), function(name) { + elements[name].paddEmpty = true; + }); + } else + setValidElements(settings.valid_elements); - s += (s ? ' ' : '') + k + ': ' + v + ';'; - } - }; + addCustomElements(settings.custom_elements); + addValidChildren(settings.valid_children); + addValidElements(settings.extended_valid_elements); - // Validate style output - if (name && t._styles) { - each(t._styles['*'], function(name) { - add(o[name], name); - }); + // Todo: Remove this when we fix list handling to be valid + addValidChildren('+ol[ul|ol],+ul[ul|ol]'); - each(t._styles[name.toLowerCase()], function(name) { - add(o[name], name); - }); - } else - each(o, add); + // If the user didn't allow span only allow internal spans + if (!getElementRule('span')) + addValidElements('span[!data-mce-type|*]'); - return s; - }, + // Delete invalid elements + if (settings.invalid_elements) { + tinymce.each(tinymce.explode(settings.invalid_elements), function(item) { + if (elements[item]) + delete elements[item]; + }); + } - loadCSS : function(u) { - var t = this, d = t.doc, head; + self.children = children; - if (!u) - u = ''; + self.styles = validStyles; - head = t.select('head')[0]; + self.getBoolAttrs = function() { + return boolAttrMap; + }; - each(u.split(','), function(u) { - var link; + self.getBlockElements = function() { + return blockElementsMap; + }; - if (t.files[u]) - return; + self.getShortEndedElements = function() { + return shortEndedElementsMap; + }; - t.files[u] = true; - link = t.create('link', {rel : 'stylesheet', href : tinymce._addVer(u)}); + self.getSelfClosingElements = function() { + return selfClosingElementsMap; + }; - // IE 8 has a bug where dynamically loading stylesheets would produce a 1 item remaining bug - // This fix seems to resolve that issue by realcing the document ones a stylesheet finishes loading - // It's ugly but it seems to work fine. - if (isIE && d.documentMode) { - link.onload = function() { - d.recalc(); - link.onload = null; - }; - } + self.getNonEmptyElements = function() { + return nonEmptyElementsMap; + }; - head.appendChild(link); - }); - }, + self.getWhiteSpaceElements = function() { + return whiteSpaceElementsMap; + }; - addClass : function(e, c) { - return this.run(e, function(e) { - var o; + self.isValidChild = function(name, child) { + var parent = children[name]; - if (!c) - return 0; + return !!(parent && parent[child]); + }; - if (this.hasClass(e, c)) - return e.className; + self.getElementRule = getElementRule; - o = this.removeClass(e, c); + self.getCustomElements = function() { + return customElementsMap; + }; - return e.className = (o != '' ? (o + ' ') : '') + c; - }); - }, + self.addValidElements = addValidElements; - removeClass : function(e, c) { - var t = this, re; + self.setValidElements = setValidElements; - return t.run(e, function(e) { - var v; + self.addCustomElements = addCustomElements; - if (t.hasClass(e, c)) { - if (!re) - re = new RegExp("(^|\\s+)" + c + "(\\s+|$)", "g"); + self.addValidChildren = addValidChildren; + }; - v = e.className.replace(re, ' '); - v = tinymce.trim(v != ' ' ? v : ''); + // Expose boolMap and blockElementMap as static properties for usage in DOMUtils + tinymce.html.Schema.boolAttrMap = boolAttrMap; + tinymce.html.Schema.blockElementsMap = blockElementsMap; +})(tinymce); - e.className = v; +(function(tinymce) { + tinymce.html.SaxParser = function(settings, schema) { + var self = this, noop = function() {}; - // Empty class attr - if (!v) { - e.removeAttribute('class'); - e.removeAttribute('className'); - } + settings = settings || {}; + self.schema = schema = schema || new tinymce.html.Schema(); - return v; + if (settings.fix_self_closing !== false) + settings.fix_self_closing = true; + + // Add handler functions from settings and setup default handlers + tinymce.each('comment cdata text start end pi doctype'.split(' '), function(name) { + if (name) + self[name] = settings[name] || noop; + }); + + self.parse = function(html) { + var self = this, matches, index = 0, value, endRegExp, stack = [], attrList, i, text, name, isInternalElement, removeInternalElements, + shortEndedElements, fillAttrsMap, isShortEnded, validate, elementRule, isValidElement, attr, attribsValue, invalidPrefixRegExp, + validAttributesMap, validAttributePatterns, attributesRequired, attributesDefault, attributesForced, selfClosing, + tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0, decode = tinymce.html.Entities.decode, fixSelfClosing, isIE; + + function processEndTag(name) { + var pos, i; + + // Find position of parent of the same type + pos = stack.length; + while (pos--) { + if (stack[pos].name === name) + break; } - return e.className; - }); - }, + // Found parent + if (pos >= 0) { + // Close all the open elements + for (i = stack.length - 1; i >= pos; i--) { + name = stack[i]; - hasClass : function(n, c) { - n = this.get(n); + if (name.valid) + self.end(name.name); + } - if (!n || !c) - return false; + // Remove the open elements from the stack + stack.length = pos; + } + }; - return (' ' + n.className + ' ').indexOf(' ' + c + ' ') !== -1; - }, + // Precompile RegExps and map objects + tokenRegExp = new RegExp('<(?:' + + '(?:!--([\\w\\W]*?)-->)|' + // Comment + '(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|' + // CDATA + '(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE + '(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI + '(?:\\/([^>]+)>)|' + // End element + '(?:([^\\s\\/<>]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/)>)' + // Start element + ')', 'g'); + + attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:\\.|[^\"])*)\")|(?:\'((?:\\.|[^\'])*)\')|([^>\s]+)))?/g; + specialElements = { + 'script' : /<\/script[^>]*>/gi, + 'style' : /<\/style[^>]*>/gi, + 'noscript' : /<\/noscript[^>]*>/gi + }; - show : function(e) { - return this.setStyle(e, 'display', 'block'); - }, + // Setup lookup tables for empty elements and boolean attributes + shortEndedElements = schema.getShortEndedElements(); + selfClosing = schema.getSelfClosingElements(); + fillAttrsMap = schema.getBoolAttrs(); + validate = settings.validate; + removeInternalElements = settings.remove_internals; + fixSelfClosing = settings.fix_self_closing; + isIE = tinymce.isIE; + invalidPrefixRegExp = /^:/; + + while (matches = tokenRegExp.exec(html)) { + // Text + if (index < matches.index) + self.text(decode(html.substr(index, matches.index - index))); + + if (value = matches[6]) { // End element + value = value.toLowerCase(); + + // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements + if (isIE && invalidPrefixRegExp.test(value)) + value = value.substr(1); + + processEndTag(value); + } else if (value = matches[7]) { // Start element + value = value.toLowerCase(); + + // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements + if (isIE && invalidPrefixRegExp.test(value)) + value = value.substr(1); + + isShortEnded = value in shortEndedElements; + + // Is self closing tag for example an
  • after an open
  • + if (fixSelfClosing && selfClosing[value] && stack.length > 0 && stack[stack.length - 1].name === value) + processEndTag(value); + + // Validate element + if (!validate || (elementRule = schema.getElementRule(value))) { + isValidElement = true; + + // Grab attributes map and patters when validation is enabled + if (validate) { + validAttributesMap = elementRule.attributes; + validAttributePatterns = elementRule.attributePatterns; + } - hide : function(e) { - return this.setStyle(e, 'display', 'none'); - }, + // Parse attributes + if (attribsValue = matches[8]) { + isInternalElement = attribsValue.indexOf('data-mce-type') !== -1; // Check if the element is an internal element - isHidden : function(e) { - e = this.get(e); + // If the element has internal attributes then remove it if we are told to do so + if (isInternalElement && removeInternalElements) + isValidElement = false; - return !e || e.style.display == 'none' || this.getStyle(e, 'display') == 'none'; - }, + attrList = []; + attrList.map = {}; - uniqueId : function(p) { - return (!p ? 'mce_' : p) + (this.counter++); - }, + attribsValue.replace(attrRegExp, function(match, name, value, val2, val3) { + var attrRule, i; - setHTML : function(e, h) { - var t = this; + name = name.toLowerCase(); + value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute - return this.run(e, function(e) { - var x, i, nl, n, p, x; + // Validate name and value + if (validate && !isInternalElement && name.indexOf('data-') !== 0) { + attrRule = validAttributesMap[name]; - h = t.processHTML(h); + // Find rule by pattern matching + if (!attrRule && validAttributePatterns) { + i = validAttributePatterns.length; + while (i--) { + attrRule = validAttributePatterns[i]; + if (attrRule.pattern.test(name)) + break; + } - if (isIE) { - function set() { - // Remove all child nodes - while (e.firstChild) - e.firstChild.removeNode(); + // No rule matched + if (i === -1) + attrRule = null; + } - try { - // IE will remove comments from the beginning - // unless you padd the contents with something - e.innerHTML = '
    ' + h; - e.removeChild(e.firstChild); - } catch (ex) { - // IE sometimes produces an unknown runtime error on innerHTML if it's an block element within a block element for example a div inside a p - // This seems to fix this problem - - // Create new div with HTML contents and a BR infront to keep comments - x = t.create('div'); - x.innerHTML = '
    ' + h; - - // Add all children from div to target - each (x.childNodes, function(n, i) { - // Skip br element - if (i) - e.appendChild(n); + // No attribute rule found + if (!attrRule) + return; + + // Validate value + if (attrRule.validValues && !(value in attrRule.validValues)) + return; + } + + // Add attribute to list and map + attrList.map[name] = value; + attrList.push({ + name: name, + value: value + }); }); + } else { + attrList = []; + attrList.map = {}; } - }; - // IE has a serious bug when it comes to paragraphs it can produce an invalid - // DOM tree if contents like this

    • Item 1

    is inserted - // It seems to be that IE doesn't like a root block element placed inside another root block element - if (t.settings.fix_ie_paragraphs) - h = h.replace(/

    <\/p>|]+)><\/p>|/gi, ' 

    '); + // Process attributes if validation is enabled + if (validate && !isInternalElement) { + attributesRequired = elementRule.attributesRequired; + attributesDefault = elementRule.attributesDefault; + attributesForced = elementRule.attributesForced; + + // Handle forced attributes + if (attributesForced) { + i = attributesForced.length; + while (i--) { + attr = attributesForced[i]; + name = attr.name; + attrValue = attr.value; + + if (attrValue === '{$uid}') + attrValue = 'mce_' + idCount++; + + attrList.map[name] = attrValue; + attrList.push({name: name, value: attrValue}); + } + } + + // Handle default attributes + if (attributesDefault) { + i = attributesDefault.length; + while (i--) { + attr = attributesDefault[i]; + name = attr.name; - set(); + if (!(name in attrList.map)) { + attrValue = attr.value; - if (t.settings.fix_ie_paragraphs) { - // Check for odd paragraphs this is a sign of a broken DOM - nl = e.getElementsByTagName("p"); - for (i = nl.length - 1, x = 0; i >= 0; i--) { - n = nl[i]; + if (attrValue === '{$uid}') + attrValue = 'mce_' + idCount++; - if (!n.hasChildNodes()) { - if (!n._mce_keep) { - x = 1; // Is broken - break; + attrList.map[name] = attrValue; + attrList.push({name: name, value: attrValue}); + } } - - n.removeAttribute('_mce_keep'); } - } - } - // Time to fix the madness IE left us - if (x) { - // So if we replace the p elements with divs and mark them and then replace them back to paragraphs - // after we use innerHTML we can fix the DOM tree - h = h.replace(/

    ]+)>|

    /ig, '

    '); - h = h.replace(/<\/p>/gi, '
    '); + // Handle required attributes + if (attributesRequired) { + i = attributesRequired.length; + while (i--) { + if (attributesRequired[i] in attrList.map) + break; + } + + // None of the required attributes where found + if (i === -1) + isValidElement = false; + } - // Set the new HTML with DIVs - set(); + // Invalidate element if it's marked as bogus + if (attrList.map['data-mce-bogus']) + isValidElement = false; + } - // Replace all DIV elements with the _mce_tmp attibute back to paragraphs - // This is needed since IE has a annoying bug see above for details - // This is a slow process but it has to be done. :( - if (t.settings.fix_ie_paragraphs) { - nl = e.getElementsByTagName("DIV"); - for (i = nl.length - 1; i >= 0; i--) { - n = nl[i]; + if (isValidElement) + self.start(value, attrList, isShortEnded); + } else + isValidElement = false; - // Is it a temp div - if (n._mce_tmp) { - // Create new paragraph - p = t.doc.createElement('p'); + // Treat script, noscript and style a bit different since they may include code that looks like elements + if (endRegExp = specialElements[value]) { + endRegExp.lastIndex = index = matches.index + matches[0].length; - // Copy all attributes - n.cloneNode(false).outerHTML.replace(/([a-z0-9\-_]+)=/gi, function(a, b) { - var v; + if (matches = endRegExp.exec(html)) { + if (isValidElement) + text = html.substr(index, matches.index - index); - if (b !== '_mce_tmp') { - v = n.getAttribute(b); + index = matches.index + matches[0].length; + } else { + text = html.substr(index); + index = html.length; + } - if (!v && b === 'class') - v = n.className; + if (isValidElement && text.length > 0) + self.text(text, true); - p.setAttribute(b, v); - } - }); + if (isValidElement) + self.end(value); - // Append all children to new paragraph - for (x = 0; x= 0; i--) { + value = stack[i]; - if (isIE) { - h = h.replace(/'/g, '''); // IE can't handle apos - h = h.replace(/\s+(disabled|checked|readonly|selected)\s*=\s*[\"\']?(false|0)[\"\']?/gi, ''); // IE doesn't handle default values correct + if (value.valid) + self.end(value.name); } + }; + } +})(tinymce); - // Fix some issues - h = h.replace(/]+)\/>|/gi, ''); // Force open +(function(tinymce) { + var whiteSpaceRegExp = /^[ \t\r\n]*$/, typeLookup = { + '#text' : 3, + '#comment' : 8, + '#cdata' : 4, + '#pi' : 7, + '#doctype' : 10, + '#document-fragment' : 11 + }; - // Store away src and href in _mce_src and mce_href since browsers mess them up - if (s.keep_values) { - // Wrap scripts and styles in comments for serialization purposes - if (/)/g, '\n'); - s = s.replace(/^[\r\n]*|[\r\n]*$/g, ''); - s = s.replace(/^\s*(\/\/\s*|\]\]>|-->|\]\]-->)\s*$/g, ''); + // Walks the tree left/right + function walk(node, root_node, prev) { + var sibling, parent, startName = prev ? 'lastChild' : 'firstChild', siblingName = prev ? 'prev' : 'next'; - return s; - }; + // Walk into nodes if it has a start + if (node[startName]) + return node[startName]; - // Wrap the script contents in CDATA and keep them from executing - h = h.replace(/]+|)>([\s\S]*?)<\/script>/gi, function(v, attribs, text) { - // Force type attribute - if (!attribs) - attribs = ' type="text/javascript"'; + // Return the sibling if it has one + if (node !== root_node) { + sibling = node[siblingName]; - // Convert the src attribute of the scripts - attribs = attribs.replace(/src=\"([^\"]+)\"?/i, function(a, url) { - if (s.url_converter) - url = t.encode(s.url_converter.call(s.url_converter_scope || t, t.decode(url), 'src', 'script')); + if (sibling) + return sibling; - return '_mce_src="' + url + '"'; - }); + // Walk up the parents to look for siblings + for (parent = node.parent; parent && parent !== root_node; parent = parent.parent) { + sibling = parent[siblingName]; - // Wrap text contents - if (tinymce.trim(text)) { - codeBlocks.push(trim(text)); - text = ''; - } + if (sibling) + return sibling; + } + } + }; - return '' + text + ''; - }); + function Node(name, type) { + this.name = name; + this.type = type; - // Wrap style elements - h = h.replace(/]+|)>([\s\S]*?)<\/style>/gi, function(v, attribs, text) { - // Wrap text contents - if (text) { - codeBlocks.push(trim(text)); - text = ''; - } + if (type === 1) { + this.attributes = []; + this.attributes.map = {}; + } + } - return '' + text + ''; - }); + tinymce.extend(Node.prototype, { + replace : function(node) { + var self = this; - // Wrap noscript elements - h = h.replace(/]+|)>([\s\S]*?)<\/noscript>/g, function(v, attribs, text) { - return ''; - }); - } + if (node.parent) + node.remove(); - h = h.replace(//g, ''); + self.insert(node, self); + self.remove(); - // This function processes the attributes in the HTML string to force boolean - // attributes to the attr="attr" format and convert style, src and href to _mce_ versions - function processTags(html) { - return html.replace(tagRegExp, function(match, elm_name, attrs, end) { - return '<' + elm_name + attrs.replace(attrRegExp, function(match, name, value, val2, val3) { - var mceValue; + return self; + }, - name = name.toLowerCase(); - value = value || val2 || val3 || ""; + attr : function(name, value) { + var self = this, attrs, i, undef; - // Treat boolean attributes - if (boolAttrs[name]) { - // false or 0 is treated as a missing attribute - if (value === 'false' || value === '0') - return; + if (typeof name !== "string") { + for (i in name) + self.attr(i, name[i]); - return name + '="' + name + '"'; - } + return self; + } - // Is attribute one that needs special treatment - if (mceAttribs[name] && attrs.indexOf('_mce_' + name) == -1) { - mceValue = t.decode(value); + if (attrs = self.attributes) { + if (value !== undef) { + // Remove attribute + if (value === null) { + if (name in attrs.map) { + delete attrs.map[name]; - // Convert URLs to relative/absolute ones - if (s.url_converter && (name == "src" || name == "href")) - mceValue = s.url_converter.call(s.url_converter_scope || t, mceValue, name, elm_name); + i = attrs.length; + while (i--) { + if (attrs[i].name === name) { + attrs = attrs.splice(i, 1); + return self; + } + } + } - // Process styles lowercases them and compresses them - if (name == 'style') - mceValue = t.serializeStyle(t.parseStyle(mceValue), name); + return self; + } - return name + '="' + value + '"' + ' _mce_' + name + '="' + t.encode(mceValue) + '"'; + // Set attribute + if (name in attrs.map) { + // Set attribute + i = attrs.length; + while (i--) { + if (attrs[i].name === name) { + attrs[i].value = value; + break; } + } + } else + attrs.push({name: name, value: value}); - return match; - }) + end + '>'; - }); - }; - - h = processTags(h); + attrs.map[name] = value; - // Restore script blocks - h = h.replace(/MCE_SCRIPT:([0-9]+)/g, function(val, idx) { - return codeBlocks[idx]; - }); + return self; + } else { + return attrs.map[name]; + } } - - return h; }, - getOuterHTML : function(e) { - var d; + clone : function() { + var self = this, clone = new Node(self.name, self.type), i, l, selfAttrs, selfAttr, cloneAttrs; - e = this.get(e); + // Clone element attributes + if (selfAttrs = self.attributes) { + cloneAttrs = []; + cloneAttrs.map = {}; - if (!e) - return null; + for (i = 0, l = selfAttrs.length; i < l; i++) { + selfAttr = selfAttrs[i]; + + // Clone everything except id + if (selfAttr.name !== 'id') { + cloneAttrs[cloneAttrs.length] = {name: selfAttr.name, value: selfAttr.value}; + cloneAttrs.map[selfAttr.name] = selfAttr.value; + } + } - if (e.outerHTML !== undefined) - return e.outerHTML; + clone.attributes = cloneAttrs; + } - d = (e.ownerDocument || this.doc).createElement("body"); - d.appendChild(e.cloneNode(true)); + clone.value = self.value; + clone.shortEnded = self.shortEnded; - return d.innerHTML; + return clone; }, - setOuterHTML : function(e, h, d) { - var t = this; + wrap : function(wrapper) { + var self = this; - function setHTML(e, h, d) { - var n, tp; + self.parent.insert(wrapper, self); + wrapper.append(self); - tp = d.createElement("body"); - tp.innerHTML = h; + return self; + }, - n = tp.lastChild; - while (n) { - t.insertAfter(n.cloneNode(true), e); - n = n.previousSibling; - } + unwrap : function() { + var self = this, node, next; - t.remove(e); - }; + for (node = self.firstChild; node; ) { + next = node.next; + self.insert(node, self, true); + node = next; + } - return this.run(e, function(e) { - e = t.get(e); + self.remove(); + }, - // Only set HTML on elements - if (e.nodeType == 1) { - d = d || e.ownerDocument || t.doc; + remove : function() { + var self = this, parent = self.parent, next = self.next, prev = self.prev; - if (isIE) { - try { - // Try outerHTML for IE it sometimes produces an unknown runtime error - if (isIE && e.nodeType == 1) - e.outerHTML = h; - else - setHTML(e, h, d); - } catch (ex) { - // Fix for unknown runtime error - setHTML(e, h, d); - } - } else - setHTML(e, h, d); - } - }); - }, + if (parent) { + if (parent.firstChild === self) { + parent.firstChild = next; - decode : function(s) { - var e, n, v; + if (next) + next.prev = null; + } else { + prev.next = next; + } - // Look for entities to decode - if (/&[\w#]+;/.test(s)) { - // Decode the entities using a div element not super efficient but less code - e = this.doc.createElement("div"); - e.innerHTML = s; - n = e.firstChild; - v = ''; + if (parent.lastChild === self) { + parent.lastChild = prev; - if (n) { - do { - v += n.nodeValue; - } while (n = n.nextSibling); + if (prev) + prev.next = null; + } else { + next.prev = prev; } - return v || s; + self.parent = self.next = self.prev = null; } - return s; + return self; }, - encode : function(str) { - return ('' + str).replace(encodeCharsRe, function(chr) { - return encodedChars[chr]; - }); + append : function(node) { + var self = this, last; + + if (node.parent) + node.remove(); + + last = self.lastChild; + if (last) { + last.next = node; + node.prev = last; + self.lastChild = node; + } else + self.lastChild = self.firstChild = node; + + node.parent = self; + + return node; }, - insertAfter : function(node, reference_node) { - reference_node = this.get(reference_node); + insert : function(node, ref_node, before) { + var parent; - return this.run(node, function(node) { - var parent, nextSibling; + if (node.parent) + node.remove(); - parent = reference_node.parentNode; - nextSibling = reference_node.nextSibling; + parent = ref_node.parent || this; - if (nextSibling) - parent.insertBefore(node, nextSibling); + if (before) { + if (ref_node === parent.firstChild) + parent.firstChild = node; else - parent.appendChild(node); + ref_node.prev.next = node; - return node; - }); - }, + node.prev = ref_node.prev; + node.next = ref_node; + ref_node.prev = node; + } else { + if (ref_node === parent.lastChild) + parent.lastChild = node; + else + ref_node.next.prev = node; - isBlock : function(n) { - if (n.nodeType && n.nodeType !== 1) - return false; + node.next = ref_node.next; + node.prev = ref_node; + ref_node.next = node; + } - n = n.nodeName || n; + node.parent = parent; - return blockRe.test(n); + return node; }, - replace : function(n, o, k) { - var t = this; - - if (is(o, 'array')) - n = n.cloneNode(true); + getAll : function(name) { + var self = this, node, collection = []; - return t.run(o, function(o) { - if (k) { - each(tinymce.grep(o.childNodes), function(c) { - n.appendChild(c); - }); - } + for (node = self.firstChild; node; node = walk(node, self)) { + if (node.name === name) + collection.push(node); + } - return o.parentNode.replaceChild(n, o); - }); + return collection; }, - rename : function(elm, name) { - var t = this, newElm; + empty : function() { + var self = this, nodes, i, node; - if (elm.nodeName != name.toUpperCase()) { - // Rename block element - newElm = t.create(name); + // Remove all children + if (self.firstChild) { + nodes = []; - // Copy attribs to new block - each(t.getAttribs(elm), function(attr_node) { - t.setAttrib(newElm, attr_node.nodeName, t.getAttrib(elm, attr_node.nodeName)); - }); + // Collect the children + for (node = self.firstChild; node; node = walk(node, self)) + nodes.push(node); - // Replace block - t.replace(newElm, elm, 1); + // Remove the children + i = nodes.length; + while (i--) { + node = nodes[i]; + node.parent = node.firstChild = node.lastChild = node.next = node.prev = null; + } } - return newElm || elm; + self.firstChild = self.lastChild = null; + + return self; }, - findCommonAncestor : function(a, b) { - var ps = a, pe; + isEmpty : function(elements) { + var self = this, node = self.firstChild, i, name; - while (ps) { - pe = b; + if (node) { + do { + if (node.type === 1) { + // Ignore bogus elements + if (node.attributes.map['data-mce-bogus']) + continue; - while (pe && ps != pe) - pe = pe.parentNode; + // Keep empty elements like + if (elements[node.name]) + return false; - if (ps == pe) - break; + // Keep elements with data attributes or name attribute like + i = node.attributes.length; + while (i--) { + name = node.attributes[i].name; + if (name === "name" || name.indexOf('data-') === 0) + return false; + } + } - ps = ps.parentNode; + // Keep non whitespace text nodes + if ((node.type === 3 && !whiteSpaceRegExp.test(node.value))) + return false; + } while (node = walk(node, self)); } - if (!ps && a.ownerDocument) - return a.ownerDocument.documentElement; - - return ps; + return true; }, - toHex : function(s) { - var c = /^\s*rgb\s*?\(\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?\)\s*$/i.exec(s); - - function hex(s) { - s = parseInt(s).toString(16); + walk : function(prev) { + return walk(this, null, prev); + } + }); - return s.length > 1 ? s : '0' + s; // 0 -> 00 - }; + tinymce.extend(Node, { + create : function(name, attrs) { + var node, attrName; - if (c) { - s = '#' + hex(c[1]) + hex(c[2]) + hex(c[3]); + // Create node + node = new Node(name, typeLookup[name] || 1); - return s; + // Add attributes if needed + if (attrs) { + for (attrName in attrs) + node.attr(attrName, attrs[attrName]); } - return s; - }, + return node; + } + }); - getClasses : function() { - var t = this, cl = [], i, lo = {}, f = t.settings.class_filter, ov; + tinymce.html.Node = Node; +})(tinymce); - if (t.classes) - return t.classes; +(function(tinymce) { + var Node = tinymce.html.Node; - function addClasses(s) { - // IE style imports - each(s.imports, function(r) { - addClasses(r); - }); + tinymce.html.DomParser = function(settings, schema) { + var self = this, nodeFilters = {}, attributeFilters = [], matchedNodes = {}, matchedAttributes = {}; - each(s.cssRules || s.rules, function(r) { - // Real type or fake it on IE - switch (r.type || 1) { - // Rule - case 1: - if (r.selectorText) { - each(r.selectorText.split(','), function(v) { - v = v.replace(/^\s*|\s*$|^\s\./g, ""); + settings = settings || {}; + settings.validate = "validate" in settings ? settings.validate : true; + settings.root_name = settings.root_name || 'body'; + self.schema = schema = schema || new tinymce.html.Schema(); - // Is internal or it doesn't contain a class - if (/\.mce/.test(v) || !/\.[\w\-]+$/.test(v)) - return; + function fixInvalidChildren(nodes) { + var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i, + childClone, nonEmptyElements, nonSplitableElements, sibling, nextNode; - // Remove everything but class name - ov = v; - v = v.replace(/.*\.([a-z0-9_\-]+).*/i, '$1'); + nonSplitableElements = tinymce.makeMap('tr,td,th,tbody,thead,tfoot,table'); + nonEmptyElements = schema.getNonEmptyElements(); - // Filter classes - if (f && !(v = f(v, ov))) - return; + for (ni = 0; ni < nodes.length; ni++) { + node = nodes[ni]; - if (!lo[v]) { - cl.push({'class' : v}); - lo[v] = 1; - } - }); - } - break; + // Already removed + if (!node.parent) + continue; - // Import - case 3: - addClasses(r.styleSheet); - break; - } - }); - }; + // Get list of all parent nodes until we find a valid parent to stick the child into + parents = [node]; + for (parent = node.parent; parent && !schema.isValidChild(parent.name, node.name) && !nonSplitableElements[parent.name]; parent = parent.parent) + parents.push(parent); - try { - each(t.doc.styleSheets, addClasses); - } catch (ex) { - // Ignore - } + // Found a suitable parent + if (parent && parents.length > 1) { + // Reverse the array since it makes looping easier + parents.reverse(); - if (cl.length > 0) - t.classes = cl; + // Clone the related parent and insert that after the moved node + newParent = currentNode = self.filterNode(parents[0].clone()); - return cl; - }, + // Start cloning and moving children on the left side of the target node + for (i = 0; i < parents.length - 1; i++) { + if (schema.isValidChild(currentNode.name, parents[i].name)) { + tempNode = self.filterNode(parents[i].clone()); + currentNode.append(tempNode); + } else + tempNode = currentNode; - run : function(e, f, s) { - var t = this, o; + for (childNode = parents[i].firstChild; childNode && childNode != parents[i + 1]; ) { + nextNode = childNode.next; + tempNode.append(childNode); + childNode = nextNode; + } - if (t.doc && typeof(e) === 'string') - e = t.get(e); + currentNode = tempNode; + } - if (!e) - return false; + if (!newParent.isEmpty(nonEmptyElements)) { + parent.insert(newParent, parents[0], true); + parent.insert(node, newParent); + } else { + parent.insert(node, parents[0], true); + } - s = s || this; - if (!e.nodeType && (e.length || e.length === 0)) { - o = []; + // Check if the element is empty by looking through it's contents and special treatment for


    + parent = parents[0]; + if (parent.isEmpty(nonEmptyElements) || parent.firstChild === parent.lastChild && parent.firstChild.name === 'br') { + parent.empty().remove(); + } + } else if (node.parent) { + // If it's an LI try to find a UL/OL for it or wrap it + if (node.name === 'li') { + sibling = node.prev; + if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) { + sibling.append(node); + continue; + } - each(e, function(e, i) { - if (e) { - if (typeof(e) == 'string') - e = t.doc.getElementById(e); + sibling = node.next; + if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) { + sibling.insert(node, sibling.firstChild, true); + continue; + } - o.push(f.call(s, e, i)); + node.wrap(self.filterNode(new Node('ul', 1))); + continue; } - }); - return o; + // Try wrapping the element in a DIV + if (schema.isValidChild(node.parent.name, 'div') && schema.isValidChild('div', node.name)) { + node.wrap(self.filterNode(new Node('div', 1))); + } else { + // We failed wrapping it, then remove or unwrap it + if (node.name === 'style' || node.name === 'script') + node.empty().remove(); + else + node.unwrap(); + } + } } + }; - return f.call(s, e); - }, + self.filterNode = function(node) { + var i, name, list; - getAttribs : function(n) { - var o; + // Run element filters + if (name in nodeFilters) { + list = matchedNodes[name]; - n = this.get(n); + if (list) + list.push(node); + else + matchedNodes[name] = [node]; + } - if (!n) - return []; + // Run attribute filters + i = attributeFilters.length; + while (i--) { + name = attributeFilters[i].name; - if (isIE) { - o = []; + if (name in node.attributes.map) { + list = matchedAttributes[name]; - // Object will throw exception in IE - if (n.nodeName == 'OBJECT') - return n.attributes; + if (list) + list.push(node); + else + matchedAttributes[name] = [node]; + } + } - // IE doesn't keep the selected attribute if you clone option elements - if (n.nodeName === 'OPTION' && this.getAttrib(n, 'selected')) - o.push({specified : 1, nodeName : 'selected'}); + return node; + }; - // It's crazy that this is faster in IE but it's because it returns all attributes all the time - n.cloneNode(false).outerHTML.replace(/<\/?[\w:\-]+ ?|=[\"][^\"]+\"|=\'[^\']+\'|=[\w\-]+|>/gi, '').replace(/[\w:\-]+/gi, function(a) { - o.push({specified : 1, nodeName : a}); - }); + self.addNodeFilter = function(name, callback) { + tinymce.each(tinymce.explode(name), function(name) { + var list = nodeFilters[name]; - return o; - } + if (!list) + nodeFilters[name] = list = []; - return n.attributes; - }, + list.push(callback); + }); + }; - destroy : function(s) { - var t = this; + self.addAttributeFilter = function(name, callback) { + tinymce.each(tinymce.explode(name), function(name) { + var i; - if (t.events) - t.events.destroy(); + for (i = 0; i < attributeFilters.length; i++) { + if (attributeFilters[i].name === name) { + attributeFilters[i].callbacks.push(callback); + return; + } + } - t.win = t.doc = t.root = t.events = null; + attributeFilters.push({name: name, callbacks: [callback]}); + }); + }; - // Manual destroy then remove unload handler - if (!s) - tinymce.removeUnload(t.destroy); - }, + self.parse = function(html, args) { + var parser, rootNode, node, nodes, i, l, fi, fl, list, name, validate, + blockElements, startWhiteSpaceRegExp, invalidChildren = [], + endWhiteSpaceRegExp, allWhiteSpaceRegExp, whiteSpaceElements, children, nonEmptyElements, rootBlockName; + + args = args || {}; + matchedNodes = {}; + matchedAttributes = {}; + blockElements = tinymce.extend(tinymce.makeMap('script,style,head,html,body,title,meta,param'), schema.getBlockElements()); + nonEmptyElements = schema.getNonEmptyElements(); + children = schema.children; + validate = settings.validate; + rootBlockName = "forced_root_block" in args ? args.forced_root_block : settings.forced_root_block; + + whiteSpaceElements = schema.getWhiteSpaceElements(); + startWhiteSpaceRegExp = /^[ \t\r\n]+/; + endWhiteSpaceRegExp = /[ \t\r\n]+$/; + allWhiteSpaceRegExp = /[ \t\r\n]+/g; + + function addRootBlocks() { + var node = rootNode.firstChild, next, rootBlockNode; + + while (node) { + next = node.next; + + if (node.type == 3 || (node.type == 1 && node.name !== 'p' && !blockElements[node.name] && !node.attr('data-mce-type'))) { + if (!rootBlockNode) { + // Create a new root block element + rootBlockNode = createNode(rootBlockName, 1); + rootNode.insert(rootBlockNode, node); + rootBlockNode.append(node); + } else + rootBlockNode.append(node); + } else { + rootBlockNode = null; + } - createRng : function() { - var d = this.doc; + node = next; + }; + }; - return d.createRange ? d.createRange() : new tinymce.dom.Range(this); - }, + function createNode(name, type) { + var node = new Node(name, type), list; - nodeIndex : function(node, normalized) { - var idx = 0, lastNodeType, lastNode, nodeType; + if (name in nodeFilters) { + list = matchedNodes[name]; - if (node) { - for (lastNodeType = node.nodeType, node = node.previousSibling, lastNode = node; node; node = node.previousSibling) { - nodeType = node.nodeType; + if (list) + list.push(node); + else + matchedNodes[name] = [node]; + } - // Normalize text nodes - if (normalized && nodeType == 3) { - if (nodeType == lastNodeType || !node.nodeValue.length) - continue; - } + return node; + }; - idx++; - lastNodeType = nodeType; - } - } + function removeWhitespaceBefore(node) { + var textNode, textVal, sibling; - return idx; - }, + for (textNode = node.prev; textNode && textNode.type === 3; ) { + textVal = textNode.value.replace(endWhiteSpaceRegExp, ''); - split : function(pe, e, re) { - var t = this, r = t.createRng(), bef, aft, pa; + if (textVal.length > 0) { + textNode.value = textVal; + textNode = textNode.prev; + } else { + sibling = textNode.prev; + textNode.remove(); + textNode = sibling; + } + } + }; - // W3C valid browsers tend to leave empty nodes to the left/right side of the contents, this makes sense - // but we don't want that in our code since it serves no purpose for the end user - // For example if this is chopped: - //

    text 1CHOPtext 2

    - // would produce: - //

    text 1

    CHOP

    text 2

    - // this function will then trim of empty edges and produce: - //

    text 1

    CHOP

    text 2

    - function trim(node) { - var i, children = node.childNodes; + parser = new tinymce.html.SaxParser({ + validate : validate, + fix_self_closing : !validate, // Let the DOM parser handle
  • in
  • or

    in

    for better results - if (node.nodeType == 1 && node.getAttribute('_mce_type') == 'bookmark') - return; + cdata: function(text) { + node.append(createNode('#cdata', 4)).value = text; + }, - for (i = children.length - 1; i >= 0; i--) - trim(children[i]); + text: function(text, raw) { + var textNode; - if (node.nodeType != 9) { - // Keep non whitespace text nodes - if (node.nodeType == 3 && node.nodeValue.length > 0) - return; + // Trim all redundant whitespace on non white space elements + if (!whiteSpaceElements[node.name]) { + text = text.replace(allWhiteSpaceRegExp, ' '); - if (node.nodeType == 1) { - // If the only child is a bookmark then move it up - children = node.childNodes; - if (children.length == 1 && children[0] && children[0].nodeType == 1 && children[0].getAttribute('_mce_type') == 'bookmark') - node.parentNode.insertBefore(children[0], node); + if (node.lastChild && blockElements[node.lastChild.name]) + text = text.replace(startWhiteSpaceRegExp, ''); + } - // Keep non empty elements or img, hr etc - if (children.length || /^(br|hr|input|img)$/i.test(node.nodeName)) - return; + // Do we need to create the node + if (text.length !== 0) { + textNode = createNode('#text', 3); + textNode.raw = !!raw; + node.append(textNode).value = text; } + }, - t.remove(node); - } + comment: function(text) { + node.append(createNode('#comment', 8)).value = text; + }, - return node; - }; + pi: function(name, text) { + node.append(createNode(name, 7)).value = text; + removeWhitespaceBefore(node); + }, - if (pe && e) { - // Get before chunk - r.setStart(pe.parentNode, t.nodeIndex(pe)); - r.setEnd(e.parentNode, t.nodeIndex(e)); - bef = r.extractContents(); + doctype: function(text) { + var newNode; + + newNode = node.append(createNode('#doctype', 10)); + newNode.value = text; + removeWhitespaceBefore(node); + }, - // Get after chunk - r = t.createRng(); - r.setStart(e.parentNode, t.nodeIndex(e) + 1); - r.setEnd(pe.parentNode, t.nodeIndex(pe) + 1); - aft = r.extractContents(); + start: function(name, attrs, empty) { + var newNode, attrFiltersLen, elementRule, textNode, attrName, text, sibling, parent; - // Insert before chunk - pa = pe.parentNode; - pa.insertBefore(trim(bef), pe); + elementRule = validate ? schema.getElementRule(name) : {}; + if (elementRule) { + newNode = createNode(elementRule.outputName || name, 1); + newNode.attributes = attrs; + newNode.shortEnded = empty; - // Insert middle chunk - if (re) - pa.replaceChild(re, e); - else - pa.insertBefore(e, pe); + node.append(newNode); - // Insert after chunk - pa.insertBefore(trim(aft), pe); - t.remove(pe); + // Check if node is valid child of the parent node is the child is + // unknown we don't collect it since it's probably a custom element + parent = children[node.name]; + if (parent && children[newNode.name] && !parent[newNode.name]) + invalidChildren.push(newNode); - return re || e; - } - }, + attrFiltersLen = attributeFilters.length; + while (attrFiltersLen--) { + attrName = attributeFilters[attrFiltersLen].name; - bind : function(target, name, func, scope) { - var t = this; + if (attrName in attrs.map) { + list = matchedAttributes[attrName]; - if (!t.events) - t.events = new tinymce.dom.EventUtils(); + if (list) + list.push(newNode); + else + matchedAttributes[attrName] = [newNode]; + } + } - return t.events.add(target, name, func, scope || this); - }, + // Trim whitespace before block + if (blockElements[name]) + removeWhitespaceBefore(newNode); - unbind : function(target, name, func) { - var t = this; + // Change current node if the element wasn't empty i.e not
    or + if (!empty) + node = newNode; + } + }, - if (!t.events) - t.events = new tinymce.dom.EventUtils(); + end: function(name) { + var textNode, elementRule, text, sibling, tempNode; - return t.events.remove(target, name, func); - }, + elementRule = validate ? schema.getElementRule(name) : {}; + if (elementRule) { + if (blockElements[name]) { + if (!whiteSpaceElements[node.name]) { + // Trim whitespace at beginning of block + for (textNode = node.firstChild; textNode && textNode.type === 3; ) { + text = textNode.value.replace(startWhiteSpaceRegExp, ''); + if (text.length > 0) { + textNode.value = text; + textNode = textNode.next; + } else { + sibling = textNode.next; + textNode.remove(); + textNode = sibling; + } + } - _findSib : function(node, selector, name) { - var t = this, f = selector; + // Trim whitespace at end of block + for (textNode = node.lastChild; textNode && textNode.type === 3; ) { + text = textNode.value.replace(endWhiteSpaceRegExp, ''); - if (node) { - // If expression make a function of it using is - if (is(f, 'string')) { - f = function(node) { - return t.is(node, selector); - }; - } + if (text.length > 0) { + textNode.value = text; + textNode = textNode.prev; + } else { + sibling = textNode.prev; + textNode.remove(); + textNode = sibling; + } + } + } - // Loop all siblings - for (node = node[name]; node; node = node[name]) { - if (f(node)) - return node; + // Trim start white space + textNode = node.prev; + if (textNode && textNode.type === 3) { + text = textNode.value.replace(startWhiteSpaceRegExp, ''); + + if (text.length > 0) + textNode.value = text; + else + textNode.remove(); + } + } + + // Handle empty nodes + if (elementRule.removeEmpty || elementRule.paddEmpty) { + if (node.isEmpty(nonEmptyElements)) { + if (elementRule.paddEmpty) + node.empty().append(new Node('#text', '3')).value = '\u00a0'; + else { + // Leave nodes that have a name like + if (!node.attributes.map.name) { + tempNode = node.parent; + node.empty().remove(); + node = tempNode; + return; + } + } + } + } + + node = node.parent; + } } - } + }, schema); - return null; - }, + rootNode = node = new Node(args.context || settings.root_name, 11); - _isRes : function(c) { - // Is live resizble element - return /^(top|left|bottom|right|width|height)/i.test(c) || /;\s*(top|left|bottom|right|width|height)/i.test(c); - } + parser.parse(html); - /* - walk : function(n, f, s) { - var d = this.doc, w; + // Fix invalid children or report invalid children in a contextual parsing + if (validate && invalidChildren.length) { + if (!args.context) + fixInvalidChildren(invalidChildren); + else + args.invalid = true; + } - if (d.createTreeWalker) { - w = d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, false); + // Wrap nodes in the root into block elements if the root is body + if (rootBlockName && rootNode.name == 'body') + addRootBlocks(); - while ((n = w.nextNode()) != null) - f.call(s || this, n); - } else - tinymce.walk(n, f, 'childNodes', s); - } - */ + // Run filters only when the contents is valid + if (!args.invalid) { + // Run node filters + for (name in matchedNodes) { + list = nodeFilters[name]; + nodes = matchedNodes[name]; - /* - toRGB : function(s) { - var c = /^\s*?#([0-9A-F]{2})([0-9A-F]{1,2})([0-9A-F]{2})?\s*?$/.exec(s); + // Remove already removed children + fi = nodes.length; + while (fi--) { + if (!nodes[fi].parent) + nodes.splice(fi, 1); + } - if (c) { - // #FFF -> #FFFFFF - if (!is(c[3])) - c[3] = c[2] = c[1]; + for (i = 0, l = list.length; i < l; i++) + list[i](nodes, name, args); + } - return "rgb(" + parseInt(c[1], 16) + "," + parseInt(c[2], 16) + "," + parseInt(c[3], 16) + ")"; - } + // Run attribute filters + for (i = 0, l = attributeFilters.length; i < l; i++) { + list = attributeFilters[i]; - return s; - } - */ - }); + if (list.name in matchedAttributes) { + nodes = matchedAttributes[list.name]; - tinymce.DOM = new tinymce.dom.DOMUtils(document, {process_html : 0}); -})(tinymce); + // Remove already removed children + fi = nodes.length; + while (fi--) { + if (!nodes[fi].parent) + nodes.splice(fi, 1); + } -(function(ns) { - // Range constructor - function Range(dom) { - var t = this, - doc = dom.doc, - EXTRACT = 0, - CLONE = 1, - DELETE = 2, - TRUE = true, - FALSE = false, - START_OFFSET = 'startOffset', - START_CONTAINER = 'startContainer', - END_CONTAINER = 'endContainer', - END_OFFSET = 'endOffset', - extend = tinymce.extend, - nodeIndex = dom.nodeIndex; + for (fi = 0, fl = list.callbacks.length; fi < fl; fi++) + list.callbacks[fi](nodes, list.name, args); + } + } + } - extend(t, { - // Inital states - startContainer : doc, - startOffset : 0, - endContainer : doc, - endOffset : 0, - collapsed : TRUE, - commonAncestorContainer : doc, + return rootNode; + }; - // Range constants - START_TO_START : 0, - START_TO_END : 1, - END_TO_END : 2, - END_TO_START : 3, + // Remove
    at end of block elements Gecko and WebKit injects BR elements to + // make it possible to place the caret inside empty blocks. This logic tries to remove + // these elements and keep br elements that where intended to be there intact + if (settings.remove_trailing_brs) { + self.addNodeFilter('br', function(nodes, name) { + var i, l = nodes.length, node, blockElements = schema.getBlockElements(), + nonEmptyElements = schema.getNonEmptyElements(), parent, prev, prevName; + + // Remove brs from body element as well + blockElements.body = 1; + + // Must loop forwards since it will otherwise remove all brs in

    a


    + for (i = 0; i < l; i++) { + node = nodes[i]; + parent = node.parent; + + if (blockElements[node.parent.name] && node === parent.lastChild) { + // Loop all nodes to the right of the current node and check for other BR elements + // excluding bookmarks since they are invisible + prev = node.prev; + while (prev) { + prevName = prev.name; + + // Ignore bookmarks + if (prevName !== "span" || prev.attr('data-mce-type') !== 'bookmark') { + // Found a non BR element + if (prevName !== "br") + break; + + // Found another br it's a

    structure then don't remove anything + if (prevName === 'br') { + node = null; + break; + } + } - // Public methods - setStart : setStart, - setEnd : setEnd, - setStartBefore : setStartBefore, - setStartAfter : setStartAfter, - setEndBefore : setEndBefore, - setEndAfter : setEndAfter, - collapse : collapse, - selectNode : selectNode, - selectNodeContents : selectNodeContents, - compareBoundaryPoints : compareBoundaryPoints, - deleteContents : deleteContents, - extractContents : extractContents, - cloneContents : cloneContents, - insertNode : insertNode, - surroundContents : surroundContents, - cloneRange : cloneRange - }); + prev = prev.prev; + } - function setStart(n, o) { - _setEndPoint(TRUE, n, o); - }; + if (node) { + node.remove(); + + // Is the parent to be considered empty after we removed the BR + if (parent.isEmpty(nonEmptyElements)) { + elementRule = schema.getElementRule(parent.name); + + // Remove or padd the element depending on schema rule + if (elementRule) { + if (elementRule.removeEmpty) + parent.remove(); + else if (elementRule.paddEmpty) + parent.empty().append(new tinymce.html.Node('#text', 3)).value = '\u00a0'; + } + } + } + } + } + }); + } + } +})(tinymce); - function setEnd(n, o) { - _setEndPoint(FALSE, n, o); - }; +tinymce.html.Writer = function(settings) { + var html = [], indent, indentBefore, indentAfter, encode, htmlOutput; - function setStartBefore(n) { - setStart(n.parentNode, nodeIndex(n)); - }; + settings = settings || {}; + indent = settings.indent; + indentBefore = tinymce.makeMap(settings.indent_before || ''); + indentAfter = tinymce.makeMap(settings.indent_after || ''); + encode = tinymce.html.Entities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities); + htmlOutput = settings.element_format == "html"; - function setStartAfter(n) { - setStart(n.parentNode, nodeIndex(n) + 1); - }; + return { + start: function(name, attrs, empty) { + var i, l, attr, value; - function setEndBefore(n) { - setEnd(n.parentNode, nodeIndex(n)); - }; + if (indent && indentBefore[name] && html.length > 0) { + value = html[html.length - 1]; - function setEndAfter(n) { - setEnd(n.parentNode, nodeIndex(n) + 1); - }; + if (value.length > 0 && value !== '\n') + html.push('\n'); + } - function collapse(ts) { - if (ts) { - t[END_CONTAINER] = t[START_CONTAINER]; - t[END_OFFSET] = t[START_OFFSET]; - } else { - t[START_CONTAINER] = t[END_CONTAINER]; - t[START_OFFSET] = t[END_OFFSET]; + html.push('<', name); + + if (attrs) { + for (i = 0, l = attrs.length; i < l; i++) { + attr = attrs[i]; + html.push(' ', attr.name, '="', encode(attr.value, true), '"'); + } } - t.collapsed = TRUE; - }; + if (!empty || htmlOutput) + html[html.length] = '>'; + else + html[html.length] = ' />'; - function selectNode(n) { - setStartBefore(n); - setEndAfter(n); - }; + if (empty && indent && indentAfter[name] && html.length > 0) { + value = html[html.length - 1]; - function selectNodeContents(n) { - setStart(n, 0); - setEnd(n, n.nodeType === 1 ? n.childNodes.length : n.nodeValue.length); - }; + if (value.length > 0 && value !== '\n') + html.push('\n'); + } + }, - function compareBoundaryPoints(h, r) { - var sc = t[START_CONTAINER], so = t[START_OFFSET], ec = t[END_CONTAINER], eo = t[END_OFFSET]; + end: function(name) { + var value; - // Check START_TO_START - if (h === 0) - return _compareBoundaryPoints(sc, so, sc, so); + /*if (indent && indentBefore[name] && html.length > 0) { + value = html[html.length - 1]; - // Check START_TO_END - if (h === 1) - return _compareBoundaryPoints(sc, so, ec, eo); + if (value.length > 0 && value !== '\n') + html.push('\n'); + }*/ - // Check END_TO_END - if (h === 2) - return _compareBoundaryPoints(ec, eo, ec, eo); + html.push(''); - // Check END_TO_START - if (h === 3) - return _compareBoundaryPoints(ec, eo, sc, so); - }; + if (indent && indentAfter[name] && html.length > 0) { + value = html[html.length - 1]; - function deleteContents() { - _traverse(DELETE); - }; + if (value.length > 0 && value !== '\n') + html.push('\n'); + } + }, - function extractContents() { - return _traverse(EXTRACT); - }; + text: function(text, raw) { + if (text.length > 0) + html[html.length] = raw ? text : encode(text); + }, - function cloneContents() { - return _traverse(CLONE); - }; + cdata: function(text) { + html.push(''); + }, - function insertNode(n) { - var startContainer = this[START_CONTAINER], - startOffset = this[START_OFFSET], nn, o; + comment: function(text) { + html.push(''); + }, - // Node is TEXT_NODE or CDATA - if ((startContainer.nodeType === 3 || startContainer.nodeType === 4) && startContainer.nodeValue) { - if (!startOffset) { - // At the start of text - startContainer.parentNode.insertBefore(n, startContainer); - } else if (startOffset >= startContainer.nodeValue.length) { - // At the end of text - dom.insertAfter(n, startContainer); - } else { - // Middle, need to split - nn = startContainer.splitText(startOffset); - startContainer.parentNode.insertBefore(n, nn); - } - } else { - // Insert element node - if (startContainer.childNodes.length > 0) - o = startContainer.childNodes[startOffset]; + pi: function(name, text) { + if (text) + html.push(''); + else + html.push(''); - if (o) - startContainer.insertBefore(n, o); - else - startContainer.appendChild(n); - } - }; + if (indent) + html.push('\n'); + }, - function surroundContents(n) { - var f = t.extractContents(); + doctype: function(text) { + html.push('', indent ? '\n' : ''); + }, - t.insertNode(n); - n.appendChild(f); - t.selectNode(n); - }; + reset: function() { + html.length = 0; + }, - function cloneRange() { - return extend(new Range(dom), { - startContainer : t[START_CONTAINER], - startOffset : t[START_OFFSET], - endContainer : t[END_CONTAINER], - endOffset : t[END_OFFSET], - collapsed : t.collapsed, - commonAncestorContainer : t.commonAncestorContainer - }); - }; + getContent: function() { + return html.join('').replace(/\n$/, ''); + } + }; +}; - // Private methods +(function(tinymce) { + tinymce.html.Serializer = function(settings, schema) { + var self = this, writer = new tinymce.html.Writer(settings); - function _getSelectedNode(container, offset) { - var child; + settings = settings || {}; + settings.validate = "validate" in settings ? settings.validate : true; - if (container.nodeType == 3 /* TEXT_NODE */) - return container; + self.schema = schema = schema || new tinymce.html.Schema(); + self.writer = writer; - if (offset < 0) - return container; + self.serialize = function(node) { + var handlers, validate; - child = container.firstChild; - while (child && offset > 0) { - --offset; - child = child.nextSibling; - } + validate = settings.validate; - if (child) - return child; + handlers = { + // #text + 3: function(node, raw) { + writer.text(node.value, node.raw); + }, - return container; - }; + // #comment + 8: function(node) { + writer.comment(node.value); + }, - function _isCollapsed() { - return (t[START_CONTAINER] == t[END_CONTAINER] && t[START_OFFSET] == t[END_OFFSET]); - }; + // Processing instruction + 7: function(node) { + writer.pi(node.name, node.value); + }, - function _compareBoundaryPoints(containerA, offsetA, containerB, offsetB) { - var c, offsetC, n, cmnRoot, childA, childB; + // Doctype + 10: function(node) { + writer.doctype(node.value); + }, - // In the first case the boundary-points have the same container. A is before B - // if its offset is less than the offset of B, A is equal to B if its offset is - // equal to the offset of B, and A is after B if its offset is greater than the - // offset of B. - if (containerA == containerB) { - if (offsetA == offsetB) - return 0; // equal + // CDATA + 4: function(node) { + writer.cdata(node.value); + }, - if (offsetA < offsetB) - return -1; // before + // Document fragment + 11: function(node) { + if ((node = node.firstChild)) { + do { + walk(node); + } while (node = node.next); + } + } + }; - return 1; // after - } + writer.reset(); - // In the second case a child node C of the container of A is an ancestor - // container of B. In this case, A is before B if the offset of A is less than or - // equal to the index of the child node C and A is after B otherwise. - c = containerB; - while (c && c.parentNode != containerA) - c = c.parentNode; + function walk(node) { + var handler = handlers[node.type], name, isEmpty, attrs, attrName, attrValue, sortedAttrs, i, l, elementRule; - if (c) { - offsetC = 0; - n = containerA.firstChild; + if (!handler) { + name = node.name; + isEmpty = node.shortEnded; + attrs = node.attributes; - while (n != c && offsetC < offsetA) { - offsetC++; - n = n.nextSibling; - } + // Sort attributes + if (validate && attrs && attrs.length > 1) { + sortedAttrs = []; + sortedAttrs.map = {}; - if (offsetA <= offsetC) - return -1; // before + elementRule = schema.getElementRule(node.name); + for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) { + attrName = elementRule.attributesOrder[i]; - return 1; // after - } + if (attrName in attrs.map) { + attrValue = attrs.map[attrName]; + sortedAttrs.map[attrName] = attrValue; + sortedAttrs.push({name: attrName, value: attrValue}); + } + } - // In the third case a child node C of the container of B is an ancestor container - // of A. In this case, A is before B if the index of the child node C is less than - // the offset of B and A is after B otherwise. - c = containerA; - while (c && c.parentNode != containerB) { - c = c.parentNode; - } + for (i = 0, l = attrs.length; i < l; i++) { + attrName = attrs[i].name; - if (c) { - offsetC = 0; - n = containerB.firstChild; + if (!(attrName in sortedAttrs.map)) { + attrValue = attrs.map[attrName]; + sortedAttrs.map[attrName] = attrValue; + sortedAttrs.push({name: attrName, value: attrValue}); + } + } - while (n != c && offsetC < offsetB) { - offsetC++; - n = n.nextSibling; - } + attrs = sortedAttrs; + } - if (offsetC < offsetB) - return -1; // before + writer.start(node.name, attrs, isEmpty); - return 1; // after - } + if (!isEmpty) { + if ((node = node.firstChild)) { + do { + walk(node); + } while (node = node.next); + } - // In the fourth case, none of three other cases hold: the containers of A and B - // are siblings or descendants of sibling nodes. In this case, A is before B if - // the container of A is before the container of B in a pre-order traversal of the - // Ranges' context tree and A is after B otherwise. - cmnRoot = dom.findCommonAncestor(containerA, containerB); - childA = containerA; + writer.end(name); + } + } else + handler(node); + } - while (childA && childA.parentNode != cmnRoot) - childA = childA.parentNode; + // Serialize element and treat all non elements as fragments + if (node.type == 1 && !settings.inner) + walk(node); + else + handlers[11](node); - if (!childA) - childA = cmnRoot; + return writer.getContent(); + }; + } +})(tinymce); - childB = containerB; - while (childB && childB.parentNode != cmnRoot) - childB = childB.parentNode; +(function(tinymce) { + // Shorten names + var each = tinymce.each, + is = tinymce.is, + isWebKit = tinymce.isWebKit, + isIE = tinymce.isIE, + Entities = tinymce.html.Entities, + simpleSelectorRe = /^([a-z0-9],?)+$/i, + blockElementsMap = tinymce.html.Schema.blockElementsMap, + whiteSpaceRegExp = /^[ \t\r\n]*$/; - if (!childB) - childB = cmnRoot; + tinymce.create('tinymce.dom.DOMUtils', { + doc : null, + root : null, + files : null, + pixelStyles : /^(top|left|bottom|right|width|height|borderWidth)$/, + props : { + "for" : "htmlFor", + "class" : "className", + className : "className", + checked : "checked", + disabled : "disabled", + maxlength : "maxLength", + readonly : "readOnly", + selected : "selected", + value : "value", + id : "id", + name : "name", + type : "type" + }, - if (childA == childB) - return 0; // equal + DOMUtils : function(d, s) { + var t = this, globalStyle, name; - n = cmnRoot.firstChild; - while (n) { - if (n == childA) - return -1; // before + t.doc = d; + t.win = window; + t.files = {}; + t.cssFlicker = false; + t.counter = 0; + t.stdMode = !tinymce.isIE || d.documentMode >= 8; + t.boxModel = !tinymce.isIE || d.compatMode == "CSS1Compat" || t.stdMode; + t.hasOuterHTML = "outerHTML" in d.createElement("a"); - if (n == childB) - return 1; // after + t.settings = s = tinymce.extend({ + keep_values : false, + hex_colors : 1 + }, s); + + t.schema = s.schema; + t.styles = new tinymce.html.Styles({ + url_converter : s.url_converter, + url_converter_scope : s.url_converter_scope + }, s.schema); - n = n.nextSibling; + // Fix IE6SP2 flicker and check it failed for pre SP2 + if (tinymce.isIE6) { + try { + d.execCommand('BackgroundImageCache', false, true); + } catch (e) { + t.cssFlicker = true; + } } - }; - function _setEndPoint(st, n, o) { - var ec, sc; + if (isIE && s.schema) { + // Add missing HTML 4/5 elements to IE + ('abbr article aside audio canvas ' + + 'details figcaption figure footer ' + + 'header hgroup mark menu meter nav ' + + 'output progress section summary ' + + 'time video').replace(/\w+/g, function(name) { + d.createElement(name); + }); - if (st) { - t[START_CONTAINER] = n; - t[START_OFFSET] = o; - } else { - t[END_CONTAINER] = n; - t[END_OFFSET] = o; + // Create all custom elements + for (name in s.schema.getCustomElements()) { + d.createElement(name); + } } - // If one boundary-point of a Range is set to have a root container - // other than the current one for the Range, the Range is collapsed to - // the new position. This enforces the restriction that both boundary- - // points of a Range must have the same root container. - ec = t[END_CONTAINER]; - while (ec.parentNode) - ec = ec.parentNode; - - sc = t[START_CONTAINER]; - while (sc.parentNode) - sc = sc.parentNode; + tinymce.addUnload(t.destroy, t); + }, - if (sc == ec) { - // The start position of a Range is guaranteed to never be after the - // end position. To enforce this restriction, if the start is set to - // be at a position after the end, the Range is collapsed to that - // position. - if (_compareBoundaryPoints(t[START_CONTAINER], t[START_OFFSET], t[END_CONTAINER], t[END_OFFSET]) > 0) - t.collapse(st); - } else - t.collapse(st); + getRoot : function() { + var t = this, s = t.settings; - t.collapsed = _isCollapsed(); - t.commonAncestorContainer = dom.findCommonAncestor(t[START_CONTAINER], t[END_CONTAINER]); - }; + return (s && t.get(s.root_element)) || t.doc.body; + }, - function _traverse(how) { - var c, endContainerDepth = 0, startContainerDepth = 0, p, depthDiff, startNode, endNode, sp, ep; + getViewPort : function(w) { + var d, b; - if (t[START_CONTAINER] == t[END_CONTAINER]) - return _traverseSameContainer(how); + w = !w ? this.win : w; + d = w.document; + b = this.boxModel ? d.documentElement : d.body; - for (c = t[END_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { - if (p == t[START_CONTAINER]) - return _traverseCommonStartContainer(c, how); + // Returns viewport size excluding scrollbars + return { + x : w.pageXOffset || b.scrollLeft, + y : w.pageYOffset || b.scrollTop, + w : w.innerWidth || b.clientWidth, + h : w.innerHeight || b.clientHeight + }; + }, - ++endContainerDepth; - } + getRect : function(e) { + var p, t = this, sr; - for (c = t[START_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { - if (p == t[END_CONTAINER]) - return _traverseCommonEndContainer(c, how); + e = t.get(e); + p = t.getPos(e); + sr = t.getSize(e); - ++startContainerDepth; - } + return { + x : p.x, + y : p.y, + w : sr.w, + h : sr.h + }; + }, - depthDiff = startContainerDepth - endContainerDepth; + getSize : function(e) { + var t = this, w, h; - startNode = t[START_CONTAINER]; - while (depthDiff > 0) { - startNode = startNode.parentNode; - depthDiff--; - } + e = t.get(e); + w = t.getStyle(e, 'width'); + h = t.getStyle(e, 'height'); - endNode = t[END_CONTAINER]; - while (depthDiff < 0) { - endNode = endNode.parentNode; - depthDiff++; - } + // Non pixel value, then force offset/clientWidth + if (w.indexOf('px') === -1) + w = 0; - // ascend the ancestor hierarchy until we have a common parent. - for (sp = startNode.parentNode, ep = endNode.parentNode; sp != ep; sp = sp.parentNode, ep = ep.parentNode) { - startNode = sp; - endNode = ep; - } + // Non pixel value, then force offset/clientWidth + if (h.indexOf('px') === -1) + h = 0; - return _traverseCommonAncestors(startNode, endNode, how); - }; + return { + w : parseInt(w) || e.offsetWidth || e.clientWidth, + h : parseInt(h) || e.offsetHeight || e.clientHeight + }; + }, - function _traverseSameContainer(how) { - var frag, s, sub, n, cnt, sibling, xferNode; + getParent : function(n, f, r) { + return this.getParents(n, f, r, false); + }, - if (how != DELETE) - frag = doc.createDocumentFragment(); + getParents : function(n, f, r, c) { + var t = this, na, se = t.settings, o = []; - // If selection is empty, just return the fragment - if (t[START_OFFSET] == t[END_OFFSET]) - return frag; + n = t.get(n); + c = c === undefined; - // Text node needs special case handling - if (t[START_CONTAINER].nodeType == 3 /* TEXT_NODE */) { - // get the substring - s = t[START_CONTAINER].nodeValue; - sub = s.substring(t[START_OFFSET], t[END_OFFSET]); + if (se.strict_root) + r = r || t.getRoot(); - // set the original text node to its new value - if (how != CLONE) { - t[START_CONTAINER].deleteData(t[START_OFFSET], t[END_OFFSET] - t[START_OFFSET]); + // Wrap node name as func + if (is(f, 'string')) { + na = f; - // Nothing is partially selected, so collapse to start point - t.collapse(TRUE); + if (f === '*') { + f = function(n) {return n.nodeType == 1;}; + } else { + f = function(n) { + return t.is(n, na); + }; } - - if (how == DELETE) - return; - - frag.appendChild(doc.createTextNode(sub)); - return frag; } - // Copy nodes between the start/end offsets. - n = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]); - cnt = t[END_OFFSET] - t[START_OFFSET]; - - while (cnt > 0) { - sibling = n.nextSibling; - xferNode = _traverseFullySelected(n, how); + while (n) { + if (n == r || !n.nodeType || n.nodeType === 9) + break; - if (frag) - frag.appendChild( xferNode ); + if (!f || f(n)) { + if (c) + o.push(n); + else + return n; + } - --cnt; - n = sibling; + n = n.parentNode; } - // Nothing is partially selected, so collapse to start point - if (how != CLONE) - t.collapse(TRUE); + return c ? o : null; + }, - return frag; - }; + get : function(e) { + var n; - function _traverseCommonStartContainer(endAncestor, how) { - var frag, n, endIdx, cnt, sibling, xferNode; + if (e && this.doc && typeof(e) == 'string') { + n = e; + e = this.doc.getElementById(e); - if (how != DELETE) - frag = doc.createDocumentFragment(); + // IE and Opera returns meta elements when they match the specified input ID, but getElementsByName seems to do the trick + if (e && e.id !== n) + return this.doc.getElementsByName(n)[1]; + } - n = _traverseRightBoundary(endAncestor, how); + return e; + }, - if (frag) - frag.appendChild(n); + getNext : function(node, selector) { + return this._findSib(node, selector, 'nextSibling'); + }, - endIdx = nodeIndex(endAncestor); - cnt = endIdx - t[START_OFFSET]; + getPrev : function(node, selector) { + return this._findSib(node, selector, 'previousSibling'); + }, - if (cnt <= 0) { - // Collapse to just before the endAncestor, which - // is partially selected. - if (how != CLONE) { - t.setEndBefore(endAncestor); - t.collapse(FALSE); - } - return frag; - } + select : function(pa, s) { + var t = this; - n = endAncestor.previousSibling; - while (cnt > 0) { - sibling = n.previousSibling; - xferNode = _traverseFullySelected(n, how); + return tinymce.dom.Sizzle(pa, t.get(s) || t.get(t.settings.root_element) || t.doc, []); + }, - if (frag) - frag.insertBefore(xferNode, frag.firstChild); + is : function(n, selector) { + var i; - --cnt; - n = sibling; - } + // If it isn't an array then try to do some simple selectors instead of Sizzle for to boost performance + if (n.length === undefined) { + // Simple all selector + if (selector === '*') + return n.nodeType == 1; - // Collapse to just before the endAncestor, which - // is partially selected. - if (how != CLONE) { - t.setEndBefore(endAncestor); - t.collapse(FALSE); - } + // Simple selector just elements + if (simpleSelectorRe.test(selector)) { + selector = selector.toLowerCase().split(/,/); + n = n.nodeName.toLowerCase(); - return frag; - }; + for (i = selector.length - 1; i >= 0; i--) { + if (selector[i] == n) + return true; + } - function _traverseCommonEndContainer(startAncestor, how) { - var frag, startIdx, n, cnt, sibling, xferNode; + return false; + } + } - if (how != DELETE) - frag = doc.createDocumentFragment(); + return tinymce.dom.Sizzle.matches(selector, n.nodeType ? [n] : n).length > 0; + }, - n = _traverseLeftBoundary(startAncestor, how); - if (frag) - frag.appendChild(n); - startIdx = nodeIndex(startAncestor); - ++startIdx; // Because we already traversed it.... + add : function(p, n, a, h, c) { + var t = this; - cnt = t[END_OFFSET] - startIdx; - n = startAncestor.nextSibling; - while (cnt > 0) { - sibling = n.nextSibling; - xferNode = _traverseFullySelected(n, how); + return this.run(p, function(p) { + var e, k; - if (frag) - frag.appendChild(xferNode); + e = is(n, 'string') ? t.doc.createElement(n) : n; + t.setAttribs(e, a); - --cnt; - n = sibling; - } + if (h) { + if (h.nodeType) + e.appendChild(h); + else + t.setHTML(e, h); + } - if (how != CLONE) { - t.setStartAfter(startAncestor); - t.collapse(TRUE); - } + return !c ? p.appendChild(e) : e; + }); + }, - return frag; - }; + create : function(n, a, h) { + return this.add(this.doc.createElement(n), n, a, h, 1); + }, - function _traverseCommonAncestors(startAncestor, endAncestor, how) { - var n, frag, commonParent, startOffset, endOffset, cnt, sibling, nextSibling; + createHTML : function(n, a, h) { + var o = '', t = this, k; - if (how != DELETE) - frag = doc.createDocumentFragment(); + o += '<' + n; - n = _traverseLeftBoundary(startAncestor, how); - if (frag) - frag.appendChild(n); + for (k in a) { + if (a.hasOwnProperty(k)) + o += ' ' + k + '="' + t.encode(a[k]) + '"'; + } - commonParent = startAncestor.parentNode; - startOffset = nodeIndex(startAncestor); - endOffset = nodeIndex(endAncestor); - ++startOffset; + // A call to tinymce.is doesn't work for some odd reason on IE9 possible bug inside their JS runtime + if (typeof(h) != "undefined") + return o + '>' + h + ''; - cnt = endOffset - startOffset; - sibling = startAncestor.nextSibling; + return o + ' />'; + }, - while (cnt > 0) { - nextSibling = sibling.nextSibling; - n = _traverseFullySelected(sibling, how); + remove : function(node, keep_children) { + return this.run(node, function(node) { + var child, parent = node.parentNode; - if (frag) - frag.appendChild(n); + if (!parent) + return null; - sibling = nextSibling; - --cnt; - } + if (keep_children) { + while (child = node.firstChild) { + // IE 8 will crash if you don't remove completely empty text nodes + if (!tinymce.isIE || child.nodeType !== 3 || child.nodeValue) + parent.insertBefore(child, node); + else + node.removeChild(child); + } + } - n = _traverseRightBoundary(endAncestor, how); + return parent.removeChild(node); + }); + }, - if (frag) - frag.appendChild(n); + setStyle : function(n, na, v) { + var t = this; - if (how != CLONE) { - t.setStartAfter(startAncestor); - t.collapse(TRUE); - } + return t.run(n, function(e) { + var s, i; - return frag; - }; + s = e.style; - function _traverseRightBoundary(root, how) { - var next = _getSelectedNode(t[END_CONTAINER], t[END_OFFSET] - 1), parent, clonedParent, prevSibling, clonedChild, clonedGrandParent, isFullySelected = next != t[END_CONTAINER]; + // Camelcase it, if needed + na = na.replace(/-(\D)/g, function(a, b){ + return b.toUpperCase(); + }); - if (next == root) - return _traverseNode(next, isFullySelected, FALSE, how); + // Default px suffix on these + if (t.pixelStyles.test(na) && (tinymce.is(v, 'number') || /^[\-0-9\.]+$/.test(v))) + v += 'px'; - parent = next.parentNode; - clonedParent = _traverseNode(parent, FALSE, FALSE, how); + switch (na) { + case 'opacity': + // IE specific opacity + if (isIE) { + s.filter = v === '' ? '' : "alpha(opacity=" + (v * 100) + ")"; - while (parent) { - while (next) { - prevSibling = next.previousSibling; - clonedChild = _traverseNode(next, isFullySelected, FALSE, how); + if (!n.currentStyle || !n.currentStyle.hasLayout) + s.display = 'inline-block'; + } - if (how != DELETE) - clonedParent.insertBefore(clonedChild, clonedParent.firstChild); + // Fix for older browsers + s[na] = s['-moz-opacity'] = s['-khtml-opacity'] = v || ''; + break; - isFullySelected = TRUE; - next = prevSibling; + case 'float': + isIE ? s.styleFloat = v : s.cssFloat = v; + break; + + default: + s[na] = v || ''; } - if (parent == root) - return clonedParent; - - next = parent.previousSibling; - parent = parent.parentNode; + // Force update of the style data + if (t.settings.update_styles) + t.setAttrib(e, 'data-mce-style'); + }); + }, - clonedGrandParent = _traverseNode(parent, FALSE, FALSE, how); + getStyle : function(n, na, c) { + n = this.get(n); - if (how != DELETE) - clonedGrandParent.appendChild(clonedParent); - - clonedParent = clonedGrandParent; - } - }; - - function _traverseLeftBoundary(root, how) { - var next = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]), isFullySelected = next != t[START_CONTAINER], parent, clonedParent, nextSibling, clonedChild, clonedGrandParent; - - if (next == root) - return _traverseNode(next, isFullySelected, TRUE, how); - - parent = next.parentNode; - clonedParent = _traverseNode(parent, FALSE, TRUE, how); - - while (parent) { - while (next) { - nextSibling = next.nextSibling; - clonedChild = _traverseNode(next, isFullySelected, TRUE, how); + if (!n) + return; - if (how != DELETE) - clonedParent.appendChild(clonedChild); + // Gecko + if (this.doc.defaultView && c) { + // Remove camelcase + na = na.replace(/[A-Z]/g, function(a){ + return '-' + a; + }); - isFullySelected = TRUE; - next = nextSibling; + try { + return this.doc.defaultView.getComputedStyle(n, null).getPropertyValue(na); + } catch (ex) { + // Old safari might fail + return null; } + } - if (parent == root) - return clonedParent; + // Camelcase it, if needed + na = na.replace(/-(\D)/g, function(a, b){ + return b.toUpperCase(); + }); - next = parent.nextSibling; - parent = parent.parentNode; + if (na == 'float') + na = isIE ? 'styleFloat' : 'cssFloat'; - clonedGrandParent = _traverseNode(parent, FALSE, TRUE, how); + // IE & Opera + if (n.currentStyle && c) + return n.currentStyle[na]; - if (how != DELETE) - clonedGrandParent.appendChild(clonedParent); + return n.style ? n.style[na] : undefined; + }, - clonedParent = clonedGrandParent; - } - }; + setStyles : function(e, o) { + var t = this, s = t.settings, ol; - function _traverseNode(n, isFullySelected, isLeft, how) { - var txtValue, newNodeValue, oldNodeValue, offset, newNode; + ol = s.update_styles; + s.update_styles = 0; - if (isFullySelected) - return _traverseFullySelected(n, how); + each(o, function(v, n) { + t.setStyle(e, n, v); + }); - if (n.nodeType == 3 /* TEXT_NODE */) { - txtValue = n.nodeValue; + // Update style info + s.update_styles = ol; + if (s.update_styles) + t.setAttrib(e, s.cssText); + }, - if (isLeft) { - offset = t[START_OFFSET]; - newNodeValue = txtValue.substring(offset); - oldNodeValue = txtValue.substring(0, offset); - } else { - offset = t[END_OFFSET]; - newNodeValue = txtValue.substring(0, offset); - oldNodeValue = txtValue.substring(offset); + removeAllAttribs: function(e) { + return this.run(e, function(e) { + var i, attrs = e.attributes; + for (i = attrs.length - 1; i >= 0; i--) { + e.removeAttributeNode(attrs.item(i)); } + }); + }, - if (how != CLONE) - n.nodeValue = oldNodeValue; - - if (how == DELETE) - return; - - newNode = n.cloneNode(FALSE); - newNode.nodeValue = newNodeValue; - - return newNode; - } + setAttrib : function(e, n, v) { + var t = this; - if (how == DELETE) + // Whats the point + if (!e || !n) return; - return n.cloneNode(FALSE); - }; - - function _traverseFullySelected(n, how) { - if (how != DELETE) - return how == CLONE ? n.cloneNode(TRUE) : n; - - n.parentNode.removeChild(n); - }; - }; - - ns.Range = Range; -})(tinymce.dom); - -(function() { - function Selection(selection) { - var t = this, invisibleChar = '\uFEFF', range, lastIERng, dom = selection.dom, TRUE = true, FALSE = false; - - // Returns a W3C DOM compatible range object by using the IE Range API - function getRange() { - var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed; - - // If selection is outside the current document just return an empty range - element = ieRange.item ? ieRange.item(0) : ieRange.parentElement(); - if (element.ownerDocument != dom.doc) - return domRange; - - // Handle control selection or text selection of a image - if (ieRange.item || !element.hasChildNodes()) { - domRange.setStart(element.parentNode, dom.nodeIndex(element)); - domRange.setEnd(domRange.startContainer, domRange.startOffset + 1); - - return domRange; - } - - collapsed = selection.isCollapsed(); - - function findEndPoint(start) { - var marker, container, offset, nodes, startIndex = 0, endIndex, index, parent, checkRng, position; + // Strict XML mode + if (t.settings.strict) + n = n.toLowerCase(); - // Setup temp range and collapse it - checkRng = ieRange.duplicate(); - checkRng.collapse(start); + return this.run(e, function(e) { + var s = t.settings; + if (v !== null) { + switch (n) { + case "style": + if (!is(v, 'string')) { + each(v, function(v, n) { + t.setStyle(e, n, v); + }); - // Create marker and insert it at the end of the endpoints parent - marker = dom.create('a'); - parent = checkRng.parentElement(); + return; + } - // If parent doesn't have any children then set the container to that parent and the index to 0 - if (!parent.hasChildNodes()) { - domRange[start ? 'setStart' : 'setEnd'](parent, 0); - return; - } + // No mce_style for elements with these since they might get resized by the user + if (s.keep_values) { + if (v && !t._isRes(v)) + e.setAttribute('data-mce-style', v, 2); + else + e.removeAttribute('data-mce-style', 2); + } - parent.appendChild(marker); - checkRng.moveToElementText(marker); - position = ieRange.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', checkRng); - if (position > 0) { - // The position is after the end of the parent element. - // This is the case where IE puts the caret to the left edge of a table. - domRange[start ? 'setStartAfter' : 'setEndAfter'](parent); - dom.remove(marker); - return; - } + e.style.cssText = v; + break; - // Setup node list and endIndex - nodes = tinymce.grep(parent.childNodes); - endIndex = nodes.length - 1; - // Perform a binary search for the position - while (startIndex <= endIndex) { - index = Math.floor((startIndex + endIndex) / 2); - - // Insert marker and check it's position relative to the selection - parent.insertBefore(marker, nodes[index]); - checkRng.moveToElementText(marker); - position = ieRange.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', checkRng); - if (position > 0) { - // Marker is to the right - startIndex = index + 1; - } else if (position < 0) { - // Marker is to the left - endIndex = index - 1; - } else { - // Maker is where we are - found = true; - break; - } - } + case "class": + e.className = v || ''; // Fix IE null bug + break; - // Setup container - container = position > 0 || index == 0 ? marker.nextSibling : marker.previousSibling; + case "src": + case "href": + if (s.keep_values) { + if (s.url_converter) + v = s.url_converter.call(s.url_converter_scope || t, v, n, e); - // Handle element selection - if (container.nodeType == 1) { - dom.remove(marker); + t.setAttrib(e, 'data-mce-' + n, v, 2); + } - // Find offset and container - offset = dom.nodeIndex(container); - container = container.parentNode; + break; - // Move the offset if we are setting the end or the position is after an element - if (!start || index > 0) - offset++; - } else { - // Calculate offset within text node - if (position > 0 || index == 0) { - checkRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', ieRange); - offset = checkRng.text.length; - } else { - checkRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', ieRange); - offset = container.nodeValue.length - checkRng.text.length; + case "shape": + e.setAttribute('data-mce-style', v); + break; } - - dom.remove(marker); } + if (is(v) && v !== null && v.length !== 0) + e.setAttribute(n, '' + v, 2); + else + e.removeAttribute(n, 2); + }); + }, - domRange[start ? 'setStart' : 'setEnd'](container, offset); - }; + setAttribs : function(e, o) { + var t = this; - // Find start point - findEndPoint(true); + return this.run(e, function(e) { + each(o, function(v, n) { + t.setAttrib(e, n, v); + }); + }); + }, - // Find end point if needed - if (!collapsed) - findEndPoint(); + getAttrib : function(e, n, dv) { + var v, t = this, undef; - return domRange; - }; + e = t.get(e); - this.addRange = function(rng) { - var ieRng, ieRng2, doc = selection.dom.doc, body = doc.body, startPos, endPos, sc, so, ec, eo, marker, lastIndex, skipStart, skipEnd; + if (!e || e.nodeType !== 1) + return dv === undef ? false : dv; - this.destroy(); + if (!is(dv)) + dv = ''; - // Setup some shorter versions - sc = rng.startContainer; - so = rng.startOffset; - ec = rng.endContainer; - eo = rng.endOffset; - ieRng = body.createTextRange(); + // Try the mce variant for these + if (/^(src|href|style|coords|shape)$/.test(n)) { + v = e.getAttribute("data-mce-" + n); - // If document selection move caret to first node in document - if (sc == doc || ec == doc) { - ieRng = body.createTextRange(); - ieRng.collapse(); - ieRng.select(); - return; + if (v) + return v; } - // If child index resolve it - if (sc.nodeType == 1 && sc.hasChildNodes()) { - lastIndex = sc.childNodes.length - 1; - - // Index is higher that the child count then we need to jump over the start container - if (so > lastIndex) { - skipStart = 1; - sc = sc.childNodes[lastIndex]; - } else - sc = sc.childNodes[so]; - - // Child was text node then move offset to start of it - if (sc.nodeType == 3) - so = 0; + if (isIE && t.props[n]) { + v = e[t.props[n]]; + v = v && v.nodeValue ? v.nodeValue : v; } - // If child index resolve it - if (ec.nodeType == 1 && ec.hasChildNodes()) { - lastIndex = ec.childNodes.length - 1; + if (!v) + v = e.getAttribute(n, 2); - if (eo == 0) { - skipEnd = 1; - ec = ec.childNodes[0]; - } else { - ec = ec.childNodes[Math.min(lastIndex, eo - 1)]; + // Check boolean attribs + if (/^(checked|compact|declare|defer|disabled|ismap|multiple|nohref|noshade|nowrap|readonly|selected)$/.test(n)) { + if (e[t.props[n]] === true && v === '') + return n; - // Child was text node then move offset to end of text node - if (ec.nodeType == 3) - eo = ec.nodeValue.length; - } + return v ? n : ''; } - // Single element selection - if (sc == ec && sc.nodeType == 1) { - // Make control selection for some elements - if (/^(IMG|TABLE)$/.test(sc.nodeName) && so != eo) { - ieRng = body.createControlRange(); - ieRng.addElement(sc); - } else { - ieRng = body.createTextRange(); + // Inner input elements will override attributes on form elements + if (e.nodeName === "FORM" && e.getAttributeNode(n)) + return e.getAttributeNode(n).nodeValue; - // Padd empty elements with invisible character - if (!sc.hasChildNodes() && sc.canHaveHTML) - sc.innerHTML = invisibleChar; + if (n === 'style') { + v = v || e.style.cssText; - // Select element contents - ieRng.moveToElementText(sc); + if (v) { + v = t.serializeStyle(t.parseStyle(v), e.nodeName); - // If it's only containing a padding remove it so the caret remains - if (sc.innerHTML == invisibleChar) { - ieRng.collapse(TRUE); - sc.removeChild(sc.firstChild); - } + if (t.settings.keep_values && !t._isRes(v)) + e.setAttribute('data-mce-style', v); } - - if (so == eo) - ieRng.collapse(eo <= rng.endContainer.childNodes.length - 1); - - ieRng.select(); - ieRng.scrollIntoView(); - return; } - // Create range and marker - ieRng = body.createTextRange(); - marker = doc.createElement('span'); - marker.innerHTML = ' '; - - // Set start of range to startContainer/startOffset - if (sc.nodeType == 3) { - // Insert marker after/before startContainer - if (skipStart) - dom.insertAfter(marker, sc); - else - sc.parentNode.insertBefore(marker, sc); + // Remove Apple and WebKit stuff + if (isWebKit && n === "class" && v) + v = v.replace(/(apple|webkit)\-[a-z\-]+/gi, ''); - // Select marker the caret to offset position - ieRng.moveToElementText(marker); - marker.parentNode.removeChild(marker); + // Handle IE issues + if (isIE) { + switch (n) { + case 'rowspan': + case 'colspan': + // IE returns 1 as default value + if (v === 1) + v = ''; - // Move if we need to, moving it 0 characters actually moves it! - if (so > 0) - ieRng.move('character', so); - } else { - ieRng.moveToElementText(sc); + break; - if (skipStart) - ieRng.collapse(FALSE); - } + case 'size': + // IE returns +0 as default value for size + if (v === '+0' || v === 20 || v === 0) + v = ''; - // If same text container then we can do a more simple move - if (sc == ec && sc.nodeType == 3) { - try { - ieRng.moveEnd('character', eo - so); - ieRng.select(); - ieRng.scrollIntoView(); - } catch (ex) { - // Some times a Runtime error of the 800a025e type gets thrown - // especially when the caret is placed before a table. - // This is a somewhat strange location for the caret. - // TODO: Find a better solution for this would possible require a rewrite of the setRng method - } + break; - return; - } + case 'width': + case 'height': + case 'vspace': + case 'checked': + case 'disabled': + case 'readonly': + if (v === 0) + v = ''; - // Set end of range to endContainer/endOffset - ieRng2 = body.createTextRange(); - if (ec.nodeType == 3) { - // Insert marker after/before startContainer - ec.parentNode.insertBefore(marker, ec); + break; - // Move selection to end marker and move caret to end offset - ieRng2.moveToElementText(marker); - marker.parentNode.removeChild(marker); - ieRng2.move('character', eo); - ieRng.setEndPoint('EndToStart', ieRng2); - } else { - ieRng2.moveToElementText(ec); - ieRng2.collapse(!!skipEnd); - ieRng.setEndPoint('EndToEnd', ieRng2); - } + case 'hspace': + // IE returns -1 as default value + if (v === -1) + v = ''; - ieRng.select(); - ieRng.scrollIntoView(); - }; + break; - this.getRangeAt = function() { - // Setup new range if the cache is empty - if (!range || !tinymce.dom.RangeUtils.compareRanges(lastIERng, selection.getRng())) { - range = getRange(); + case 'maxlength': + case 'tabindex': + // IE returns default value + if (v === 32768 || v === 2147483647 || v === '32768') + v = ''; - // Store away text range for next call - lastIERng = selection.getRng(); - } + break; - // IE will say that the range is equal then produce an invalid argument exception - // if you perform specific operations in a keyup event. For example Ctrl+Del. - // This hack will invalidate the range cache if the exception occurs - try { - range.startContainer.nextSibling; - } catch (ex) { - range = getRange(); - lastIERng = null; - } + case 'multiple': + case 'compact': + case 'noshade': + case 'nowrap': + if (v === 65535) + return n; - // Return cached range - return range; - }; + return dv; - this.destroy = function() { - // Destroy cached range and last IE range to avoid memory leaks - lastIERng = range = null; - }; + case 'shape': + v = v.toLowerCase(); + break; - // IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode - if (selection.dom.boxModel) { - (function() { - var doc = dom.doc, body = doc.body, started, startRng; + default: + // IE has odd anonymous function for event attributes + if (n.indexOf('on') === 0 && v) + v = tinymce._replace(/^function\s+\w+\(\)\s+\{\s+(.*)\s+\}$/, '$1', '' + v); + } + } - // Make HTML element unselectable since we are going to handle selection by hand - doc.documentElement.unselectable = TRUE; + return (v !== undef && v !== null && v !== '') ? '' + v : dv; + }, - // Return range from point or null if it failed - function rngFromPoint(x, y) { - var rng = body.createTextRange(); + getPos : function(n, ro) { + var t = this, x = 0, y = 0, e, d = t.doc, r; - try { - rng.moveToPoint(x, y); - } catch (ex) { - // IE sometimes throws and exception, so lets just ignore it - rng = null; - } + n = t.get(n); + ro = ro || d.body; - return rng; - }; + if (n) { + // Use getBoundingClientRect if it exists since it's faster than looping offset nodes + if (n.getBoundingClientRect) { + n = n.getBoundingClientRect(); + e = t.boxModel ? d.documentElement : d.body; - // Fires while the selection is changing - function selectionChange(e) { - var pointRng; + // Add scroll offsets from documentElement or body since IE with the wrong box model will use d.body and so do WebKit + // Also remove the body/documentelement clientTop/clientLeft on IE 6, 7 since they offset the position + x = n.left + (d.documentElement.scrollLeft || d.body.scrollLeft) - e.clientTop; + y = n.top + (d.documentElement.scrollTop || d.body.scrollTop) - e.clientLeft; - // Check if the button is down or not - if (e.button) { - // Create range from mouse position - pointRng = rngFromPoint(e.x, e.y); + return {x : x, y : y}; + } - if (pointRng) { - // Check if pointRange is before/after selection then change the endPoint - if (pointRng.compareEndPoints('StartToStart', startRng) > 0) - pointRng.setEndPoint('StartToStart', startRng); - else - pointRng.setEndPoint('EndToEnd', startRng); + r = n; + while (r && r != ro && r.nodeType) { + x += r.offsetLeft || 0; + y += r.offsetTop || 0; + r = r.offsetParent; + } - pointRng.select(); - } - } else - endSelection(); + r = n.parentNode; + while (r && r != ro && r.nodeType) { + x -= r.scrollLeft || 0; + y -= r.scrollTop || 0; + r = r.parentNode; } + } - // Removes listeners - function endSelection() { - dom.unbind(doc, 'mouseup', endSelection); - dom.unbind(doc, 'mousemove', selectionChange); - started = 0; - }; + return {x : x, y : y}; + }, - // Detect when user selects outside BODY - dom.bind(doc, 'mousedown', function(e) { - if (e.target.nodeName === 'HTML') { - if (started) - endSelection(); + parseStyle : function(st) { + return this.styles.parse(st); + }, - started = 1; + serializeStyle : function(o, name) { + return this.styles.serialize(o, name); + }, - // Setup start position - startRng = rngFromPoint(e.x, e.y); - if (startRng) { - // Listen for selection change events - dom.bind(doc, 'mouseup', endSelection); - dom.bind(doc, 'mousemove', selectionChange); + loadCSS : function(u) { + var t = this, d = t.doc, head; - startRng.select(); - } - } - }); - })(); - } - }; + if (!u) + u = ''; - // Expose the selection object - tinymce.dom.TridentSelection = Selection; -})(); + head = t.select('head')[0]; + each(u.split(','), function(u) { + var link; -/* - * Sizzle CSS Selector Engine - v1.0 - * Copyright 2009, The Dojo Foundation - * Released under the MIT, BSD, and GPL Licenses. - * More information: http://sizzlejs.com/ - */ -(function(){ + if (t.files[u]) + return; -var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, - done = 0, - toString = Object.prototype.toString, - hasDuplicate = false, - baseHasDuplicate = true; + t.files[u] = true; + link = t.create('link', {rel : 'stylesheet', href : tinymce._addVer(u)}); -// Here we check if the JavaScript engine is using some sort of -// optimization where it does not always call our comparision -// function. If that is the case, discard the hasDuplicate value. -// Thus far that includes Google Chrome. -[0, 0].sort(function(){ - baseHasDuplicate = false; - return 0; -}); + // IE 8 has a bug where dynamically loading stylesheets would produce a 1 item remaining bug + // This fix seems to resolve that issue by realcing the document ones a stylesheet finishes loading + // It's ugly but it seems to work fine. + if (isIE && d.documentMode && d.recalc) { + link.onload = function() { + if (d.recalc) + d.recalc(); -var Sizzle = function(selector, context, results, seed) { - results = results || []; - context = context || document; + link.onload = null; + }; + } - var origContext = context; + head.appendChild(link); + }); + }, - if ( context.nodeType !== 1 && context.nodeType !== 9 ) { - return []; - } - - if ( !selector || typeof selector !== "string" ) { - return results; - } + addClass : function(e, c) { + return this.run(e, function(e) { + var o; - var parts = [], m, set, checkSet, extra, prune = true, contextXML = Sizzle.isXML(context), - soFar = selector, ret, cur, pop, i; - - // Reset the position of the chunker regexp (start from head) - do { - chunker.exec(""); - m = chunker.exec(soFar); + if (!c) + return 0; - if ( m ) { - soFar = m[3]; - - parts.push( m[1] ); - - if ( m[2] ) { - extra = m[3]; - break; - } - } - } while ( m ); + if (this.hasClass(e, c)) + return e.className; - if ( parts.length > 1 && origPOS.exec( selector ) ) { - if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { - set = posProcess( parts[0] + parts[1], context ); - } else { - set = Expr.relative[ parts[0] ] ? - [ context ] : - Sizzle( parts.shift(), context ); + o = this.removeClass(e, c); - while ( parts.length ) { - selector = parts.shift(); + return e.className = (o != '' ? (o + ' ') : '') + c; + }); + }, - if ( Expr.relative[ selector ] ) { - selector += parts.shift(); - } - - set = posProcess( selector, set ); - } - } - } else { - // Take a shortcut and set the context if the root selector is an ID - // (but not if it'll be faster if the inner selector is an ID) - if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && - Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { - ret = Sizzle.find( parts.shift(), context, contextXML ); - context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0]; - } + removeClass : function(e, c) { + var t = this, re; - if ( context ) { - ret = seed ? - { expr: parts.pop(), set: makeArray(seed) } : - Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); - set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set; + return t.run(e, function(e) { + var v; - if ( parts.length > 0 ) { - checkSet = makeArray(set); - } else { - prune = false; - } + if (t.hasClass(e, c)) { + if (!re) + re = new RegExp("(^|\\s+)" + c + "(\\s+|$)", "g"); - while ( parts.length ) { - cur = parts.pop(); - pop = cur; + v = e.className.replace(re, ' '); + v = tinymce.trim(v != ' ' ? v : ''); - if ( !Expr.relative[ cur ] ) { - cur = ""; - } else { - pop = parts.pop(); - } + e.className = v; - if ( pop == null ) { - pop = context; + // Empty class attr + if (!v) { + e.removeAttribute('class'); + e.removeAttribute('className'); + } + + return v; } - Expr.relative[ cur ]( checkSet, pop, contextXML ); - } - } else { - checkSet = parts = []; - } - } + return e.className; + }); + }, - if ( !checkSet ) { - checkSet = set; - } + hasClass : function(n, c) { + n = this.get(n); - if ( !checkSet ) { - Sizzle.error( cur || selector ); - } + if (!n || !c) + return false; - if ( toString.call(checkSet) === "[object Array]" ) { - if ( !prune ) { - results.push.apply( results, checkSet ); - } else if ( context && context.nodeType === 1 ) { - for ( i = 0; checkSet[i] != null; i++ ) { - if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { - results.push( set[i] ); - } - } - } else { - for ( i = 0; checkSet[i] != null; i++ ) { - if ( checkSet[i] && checkSet[i].nodeType === 1 ) { - results.push( set[i] ); - } - } - } - } else { - makeArray( checkSet, results ); - } + return (' ' + n.className + ' ').indexOf(' ' + c + ' ') !== -1; + }, - if ( extra ) { - Sizzle( extra, origContext, results, seed ); - Sizzle.uniqueSort( results ); - } + show : function(e) { + return this.setStyle(e, 'display', 'block'); + }, - return results; -}; + hide : function(e) { + return this.setStyle(e, 'display', 'none'); + }, -Sizzle.uniqueSort = function(results){ - if ( sortOrder ) { - hasDuplicate = baseHasDuplicate; - results.sort(sortOrder); + isHidden : function(e) { + e = this.get(e); - if ( hasDuplicate ) { - for ( var i = 1; i < results.length; i++ ) { - if ( results[i] === results[i-1] ) { - results.splice(i--, 1); - } - } - } - } + return !e || e.style.display == 'none' || this.getStyle(e, 'display') == 'none'; + }, - return results; -}; + uniqueId : function(p) { + return (!p ? 'mce_' : p) + (this.counter++); + }, -Sizzle.matches = function(expr, set){ - return Sizzle(expr, null, null, set); -}; + setHTML : function(element, html) { + var self = this; -Sizzle.find = function(expr, context, isXML){ - var set; + return self.run(element, function(element) { + if (isIE) { + // Remove all child nodes, IE keeps empty text nodes in DOM + while (element.firstChild) + element.removeChild(element.firstChild); - if ( !expr ) { - return []; - } + try { + // IE will remove comments from the beginning + // unless you padd the contents with something + element.innerHTML = '
    ' + html; + element.removeChild(element.firstChild); + } catch (ex) { + // IE sometimes produces an unknown runtime error on innerHTML if it's an block element within a block element for example a div inside a p + // This seems to fix this problem + + // Create new div with HTML contents and a BR infront to keep comments + element = self.create('div'); + element.innerHTML = '
    ' + html; + + // Add all children from div to target + each (element.childNodes, function(node, i) { + // Skip br element + if (i) + element.appendChild(node); + }); + } + } else + element.innerHTML = html; - for ( var i = 0, l = Expr.order.length; i < l; i++ ) { - var type = Expr.order[i], match; - - if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { - var left = match[1]; - match.splice(1,1); + return html; + }); + }, - if ( left.substr( left.length - 1 ) !== "\\" ) { - match[1] = (match[1] || "").replace(/\\/g, ""); - set = Expr.find[ type ]( match, context, isXML ); - if ( set != null ) { - expr = expr.replace( Expr.match[ type ], "" ); - break; - } - } - } - } + getOuterHTML : function(elm) { + var doc, self = this; - if ( !set ) { - set = context.getElementsByTagName("*"); - } + elm = self.get(elm); - return {set: set, expr: expr}; -}; + if (!elm) + return null; -Sizzle.filter = function(expr, set, inplace, not){ - var old = expr, result = [], curLoop = set, match, anyFound, - isXMLFilter = set && set[0] && Sizzle.isXML(set[0]); + if (elm.nodeType === 1 && self.hasOuterHTML) + return elm.outerHTML; - while ( expr && set.length ) { - for ( var type in Expr.filter ) { - if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { - var filter = Expr.filter[ type ], found, item, left = match[1]; - anyFound = false; + doc = (elm.ownerDocument || self.doc).createElement("body"); + doc.appendChild(elm.cloneNode(true)); - match.splice(1,1); + return doc.innerHTML; + }, - if ( left.substr( left.length - 1 ) === "\\" ) { - continue; - } + setOuterHTML : function(e, h, d) { + var t = this; - if ( curLoop === result ) { - result = []; - } + function setHTML(e, h, d) { + var n, tp; - if ( Expr.preFilter[ type ] ) { - match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); + tp = d.createElement("body"); + tp.innerHTML = h; - if ( !match ) { - anyFound = found = true; - } else if ( match === true ) { - continue; - } + n = tp.lastChild; + while (n) { + t.insertAfter(n.cloneNode(true), e); + n = n.previousSibling; } - if ( match ) { - for ( var i = 0; (item = curLoop[i]) != null; i++ ) { - if ( item ) { - found = filter( item, match, i, curLoop ); - var pass = not ^ !!found; + t.remove(e); + }; - if ( inplace && found != null ) { - if ( pass ) { - anyFound = true; - } else { - curLoop[i] = false; - } - } else if ( pass ) { - result.push( item ); - anyFound = true; - } + return this.run(e, function(e) { + e = t.get(e); + + // Only set HTML on elements + if (e.nodeType == 1) { + d = d || e.ownerDocument || t.doc; + + if (isIE) { + try { + // Try outerHTML for IE it sometimes produces an unknown runtime error + if (isIE && e.nodeType == 1) + e.outerHTML = h; + else + setHTML(e, h, d); + } catch (ex) { + // Fix for unknown runtime error + setHTML(e, h, d); } - } + } else + setHTML(e, h, d); } + }); + }, - if ( found !== undefined ) { - if ( !inplace ) { - curLoop = result; - } + decode : Entities.decode, - expr = expr.replace( Expr.match[ type ], "" ); + encode : Entities.encodeAllRaw, - if ( !anyFound ) { - return []; - } + insertAfter : function(node, reference_node) { + reference_node = this.get(reference_node); - break; - } - } - } + return this.run(node, function(node) { + var parent, nextSibling; - // Improper expression - if ( expr === old ) { - if ( anyFound == null ) { - Sizzle.error( expr ); - } else { - break; - } - } + parent = reference_node.parentNode; + nextSibling = reference_node.nextSibling; - old = expr; - } + if (nextSibling) + parent.insertBefore(node, nextSibling); + else + parent.appendChild(node); - return curLoop; -}; + return node; + }); + }, -Sizzle.error = function( msg ) { - throw "Syntax error, unrecognized expression: " + msg; -}; + isBlock : function(node) { + var type = node.nodeType; -var Expr = Sizzle.selectors = { - order: [ "ID", "NAME", "TAG" ], - match: { - ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, - CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, - NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/, - ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/, - TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/, - CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+\-]*)\))?/, - POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, - PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ - }, - leftMatch: {}, - attrMap: { - "class": "className", - "for": "htmlFor" - }, - attrHandle: { - href: function(elem){ - return elem.getAttribute("href"); - } - }, - relative: { - "+": function(checkSet, part){ - var isPartStr = typeof part === "string", - isTag = isPartStr && !/\W/.test(part), - isPartStrNotTag = isPartStr && !isTag; + // If it's a node then check the type and use the nodeName + if (type) + return !!(type === 1 && blockElementsMap[node.nodeName]); - if ( isTag ) { - part = part.toLowerCase(); - } + return !!blockElementsMap[node]; + }, - for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { - if ( (elem = checkSet[i]) ) { - while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} + replace : function(n, o, k) { + var t = this; - checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? - elem || false : - elem === part; + if (is(o, 'array')) + n = n.cloneNode(true); + + return t.run(o, function(o) { + if (k) { + each(tinymce.grep(o.childNodes), function(c) { + n.appendChild(c); + }); } - } - if ( isPartStrNotTag ) { - Sizzle.filter( part, checkSet, true ); - } + return o.parentNode.replaceChild(n, o); + }); }, - ">": function(checkSet, part){ - var isPartStr = typeof part === "string", - elem, i = 0, l = checkSet.length; - if ( isPartStr && !/\W/.test(part) ) { - part = part.toLowerCase(); + rename : function(elm, name) { + var t = this, newElm; - for ( ; i < l; i++ ) { - elem = checkSet[i]; - if ( elem ) { - var parent = elem.parentNode; - checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; - } - } - } else { - for ( ; i < l; i++ ) { - elem = checkSet[i]; - if ( elem ) { - checkSet[i] = isPartStr ? - elem.parentNode : - elem.parentNode === part; - } - } + if (elm.nodeName != name.toUpperCase()) { + // Rename block element + newElm = t.create(name); - if ( isPartStr ) { - Sizzle.filter( part, checkSet, true ); - } + // Copy attribs to new block + each(t.getAttribs(elm), function(attr_node) { + t.setAttrib(newElm, attr_node.nodeName, t.getAttrib(elm, attr_node.nodeName)); + }); + + // Replace block + t.replace(newElm, elm, 1); } + + return newElm || elm; }, - "": function(checkSet, part, isXML){ - var doneName = done++, checkFn = dirCheck, nodeCheck; - if ( typeof part === "string" && !/\W/.test(part) ) { - part = part.toLowerCase(); - nodeCheck = part; - checkFn = dirNodeCheck; + findCommonAncestor : function(a, b) { + var ps = a, pe; + + while (ps) { + pe = b; + + while (pe && ps != pe) + pe = pe.parentNode; + + if (ps == pe) + break; + + ps = ps.parentNode; } - checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML); + if (!ps && a.ownerDocument) + return a.ownerDocument.documentElement; + + return ps; }, - "~": function(checkSet, part, isXML){ - var doneName = done++, checkFn = dirCheck, nodeCheck; - if ( typeof part === "string" && !/\W/.test(part) ) { - part = part.toLowerCase(); - nodeCheck = part; - checkFn = dirNodeCheck; - } + toHex : function(s) { + var c = /^\s*rgb\s*?\(\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?\)\s*$/i.exec(s); - checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML); - } - }, - find: { - ID: function(match, context, isXML){ - if ( typeof context.getElementById !== "undefined" && !isXML ) { - var m = context.getElementById(match[1]); - return m ? [m] : []; + function hex(s) { + s = parseInt(s).toString(16); + + return s.length > 1 ? s : '0' + s; // 0 -> 00 + }; + + if (c) { + s = '#' + hex(c[1]) + hex(c[2]) + hex(c[3]); + + return s; } + + return s; }, - NAME: function(match, context){ - if ( typeof context.getElementsByName !== "undefined" ) { - var ret = [], results = context.getElementsByName(match[1]); - for ( var i = 0, l = results.length; i < l; i++ ) { - if ( results[i].getAttribute("name") === match[1] ) { - ret.push( results[i] ); + getClasses : function() { + var t = this, cl = [], i, lo = {}, f = t.settings.class_filter, ov; + + if (t.classes) + return t.classes; + + function addClasses(s) { + // IE style imports + each(s.imports, function(r) { + addClasses(r); + }); + + each(s.cssRules || s.rules, function(r) { + // Real type or fake it on IE + switch (r.type || 1) { + // Rule + case 1: + if (r.selectorText) { + each(r.selectorText.split(','), function(v) { + v = v.replace(/^\s*|\s*$|^\s\./g, ""); + + // Is internal or it doesn't contain a class + if (/\.mce/.test(v) || !/\.[\w\-]+$/.test(v)) + return; + + // Remove everything but class name + ov = v; + v = tinymce._replace(/.*\.([a-z0-9_\-]+).*/i, '$1', v); + + // Filter classes + if (f && !(v = f(v, ov))) + return; + + if (!lo[v]) { + cl.push({'class' : v}); + lo[v] = 1; + } + }); + } + break; + + // Import + case 3: + addClasses(r.styleSheet); + break; } - } + }); + }; - return ret.length === 0 ? null : ret; + try { + each(t.doc.styleSheets, addClasses); + } catch (ex) { + // Ignore } + + if (cl.length > 0) + t.classes = cl; + + return cl; }, - TAG: function(match, context){ - return context.getElementsByTagName(match[1]); - } - }, - preFilter: { - CLASS: function(match, curLoop, inplace, result, not, isXML){ - match = " " + match[1].replace(/\\/g, "") + " "; - if ( isXML ) { - return match; - } + run : function(e, f, s) { + var t = this, o; - for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { - if ( elem ) { - if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n]/g, " ").indexOf(match) >= 0) ) { - if ( !inplace ) { - result.push( elem ); - } - } else if ( inplace ) { - curLoop[i] = false; + if (t.doc && typeof(e) === 'string') + e = t.get(e); + + if (!e) + return false; + + s = s || this; + if (!e.nodeType && (e.length || e.length === 0)) { + o = []; + + each(e, function(e, i) { + if (e) { + if (typeof(e) == 'string') + e = t.doc.getElementById(e); + + o.push(f.call(s, e, i)); } - } + }); + + return o; } - return false; - }, - ID: function(match){ - return match[1].replace(/\\/g, ""); - }, - TAG: function(match, curLoop){ - return match[1].toLowerCase(); + return f.call(s, e); }, - CHILD: function(match){ - if ( match[1] === "nth" ) { - // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' - var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec( - match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || - !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); - // calculate the numbers (first)n+(last) including if they are negative - match[2] = (test[1] + (test[2] || 1)) - 0; - match[3] = test[3] - 0; - } + getAttribs : function(n) { + var o; - // TODO: Move to normal caching system - match[0] = done++; + n = this.get(n); - return match; - }, - ATTR: function(match, curLoop, inplace, result, not, isXML){ - var name = match[1].replace(/\\/g, ""); - - if ( !isXML && Expr.attrMap[name] ) { - match[1] = Expr.attrMap[name]; - } + if (!n) + return []; - if ( match[2] === "~=" ) { - match[4] = " " + match[4] + " "; + if (isIE) { + o = []; + + // Object will throw exception in IE + if (n.nodeName == 'OBJECT') + return n.attributes; + + // IE doesn't keep the selected attribute if you clone option elements + if (n.nodeName === 'OPTION' && this.getAttrib(n, 'selected')) + o.push({specified : 1, nodeName : 'selected'}); + + // It's crazy that this is faster in IE but it's because it returns all attributes all the time + n.cloneNode(false).outerHTML.replace(/<\/?[\w:\-]+ ?|=[\"][^\"]+\"|=\'[^\']+\'|=[\w\-]+|>/gi, '').replace(/[\w:\-]+/gi, function(a) { + o.push({specified : 1, nodeName : a}); + }); + + return o; } - return match; + return n.attributes; }, - PSEUDO: function(match, curLoop, inplace, result, not){ - if ( match[1] === "not" ) { - // If we're dealing with a complex expression, or a simple one - if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { - match[3] = Sizzle(match[3], null, null, curLoop); - } else { - var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); - if ( !inplace ) { - result.push.apply( result, ret ); - } - return false; - } - } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { - return true; + + isEmpty : function(node, elements) { + var self = this, i, attributes, type, walker, name, parentNode; + + node = node.firstChild; + if (node) { + walker = new tinymce.dom.TreeWalker(node); + elements = elements || self.schema ? self.schema.getNonEmptyElements() : null; + + do { + type = node.nodeType; + + if (type === 1) { + // Ignore bogus elements + if (node.getAttribute('data-mce-bogus')) + continue; + + // Keep empty elements like + name = node.nodeName.toLowerCase(); + if (elements && elements[name]) { + // Ignore single BR elements in blocks like


    + parentNode = node.parentNode; + if (name === 'br' && self.isBlock(parentNode) && parentNode.firstChild === node && parentNode.lastChild === node) { + continue; + } + + return false; + } + + // Keep elements with data-bookmark attributes or name attribute like
    + attributes = self.getAttribs(node); + i = node.attributes.length; + while (i--) { + name = node.attributes[i].nodeName; + if (name === "name" || name === 'data-mce-bookmark') + return false; + } + } + + // Keep non whitespace text nodes + if ((type === 3 && !whiteSpaceRegExp.test(node.nodeValue))) + return false; + } while (node = walker.next()); } - - return match; - }, - POS: function(match){ - match.unshift( true ); - return match; - } - }, - filters: { - enabled: function(elem){ - return elem.disabled === false && elem.type !== "hidden"; - }, - disabled: function(elem){ - return elem.disabled === true; - }, - checked: function(elem){ - return elem.checked === true; - }, - selected: function(elem){ - // Accessing this property makes selected-by-default - // options in Safari work properly - elem.parentNode.selectedIndex; - return elem.selected === true; - }, - parent: function(elem){ - return !!elem.firstChild; - }, - empty: function(elem){ - return !elem.firstChild; - }, - has: function(elem, i, match){ - return !!Sizzle( match[3], elem ).length; - }, - header: function(elem){ - return (/h\d/i).test( elem.nodeName ); - }, - text: function(elem){ - return "text" === elem.type; - }, - radio: function(elem){ - return "radio" === elem.type; - }, - checkbox: function(elem){ - return "checkbox" === elem.type; - }, - file: function(elem){ - return "file" === elem.type; - }, - password: function(elem){ - return "password" === elem.type; - }, - submit: function(elem){ - return "submit" === elem.type; - }, - image: function(elem){ - return "image" === elem.type; - }, - reset: function(elem){ - return "reset" === elem.type; - }, - button: function(elem){ - return "button" === elem.type || elem.nodeName.toLowerCase() === "button"; - }, - input: function(elem){ - return (/input|select|textarea|button/i).test(elem.nodeName); - } - }, - setFilters: { - first: function(elem, i){ - return i === 0; + + return true; }, - last: function(elem, i, match, array){ - return i === array.length - 1; + + destroy : function(s) { + var t = this; + + if (t.events) + t.events.destroy(); + + t.win = t.doc = t.root = t.events = null; + + // Manual destroy then remove unload handler + if (!s) + tinymce.removeUnload(t.destroy); }, - even: function(elem, i){ - return i % 2 === 0; + + createRng : function() { + var d = this.doc; + + return d.createRange ? d.createRange() : new tinymce.dom.Range(this); }, - odd: function(elem, i){ - return i % 2 === 1; + + nodeIndex : function(node, normalized) { + var idx = 0, lastNodeType, lastNode, nodeType; + + if (node) { + for (lastNodeType = node.nodeType, node = node.previousSibling, lastNode = node; node; node = node.previousSibling) { + nodeType = node.nodeType; + + // Normalize text nodes + if (normalized && nodeType == 3) { + if (nodeType == lastNodeType || !node.nodeValue.length) + continue; + } + idx++; + lastNodeType = nodeType; + } + } + + return idx; }, - lt: function(elem, i, match){ - return i < match[3] - 0; + + split : function(pe, e, re) { + var t = this, r = t.createRng(), bef, aft, pa; + + // W3C valid browsers tend to leave empty nodes to the left/right side of the contents, this makes sense + // but we don't want that in our code since it serves no purpose for the end user + // For example if this is chopped: + //

    text 1CHOPtext 2

    + // would produce: + //

    text 1

    CHOP

    text 2

    + // this function will then trim of empty edges and produce: + //

    text 1

    CHOP

    text 2

    + function trim(node) { + var i, children = node.childNodes, type = node.nodeType; + + if (type == 1 && node.getAttribute('data-mce-type') == 'bookmark') + return; + + for (i = children.length - 1; i >= 0; i--) + trim(children[i]); + + if (type != 9) { + // Keep non whitespace text nodes + if (type == 3 && node.nodeValue.length > 0) { + // If parent element isn't a block or there isn't any useful contents for example "

    " + if (!t.isBlock(node.parentNode) || tinymce.trim(node.nodeValue).length > 0) + return; + } else if (type == 1) { + // If the only child is a bookmark then move it up + children = node.childNodes; + if (children.length == 1 && children[0] && children[0].nodeType == 1 && children[0].getAttribute('data-mce-type') == 'bookmark') + node.parentNode.insertBefore(children[0], node); + + // Keep non empty elements or img, hr etc + if (children.length || /^(br|hr|input|img)$/i.test(node.nodeName)) + return; + } + + t.remove(node); + } + + return node; + }; + + if (pe && e) { + // Get before chunk + r.setStart(pe.parentNode, t.nodeIndex(pe)); + r.setEnd(e.parentNode, t.nodeIndex(e)); + bef = r.extractContents(); + + // Get after chunk + r = t.createRng(); + r.setStart(e.parentNode, t.nodeIndex(e) + 1); + r.setEnd(pe.parentNode, t.nodeIndex(pe) + 1); + aft = r.extractContents(); + + // Insert before chunk + pa = pe.parentNode; + pa.insertBefore(trim(bef), pe); + + // Insert middle chunk + if (re) + pa.replaceChild(re, e); + else + pa.insertBefore(e, pe); + + // Insert after chunk + pa.insertBefore(trim(aft), pe); + t.remove(pe); + + return re || e; + } }, - gt: function(elem, i, match){ - return i > match[3] - 0; + + bind : function(target, name, func, scope) { + var t = this; + + if (!t.events) + t.events = new tinymce.dom.EventUtils(); + + return t.events.add(target, name, func, scope || this); }, - nth: function(elem, i, match){ - return match[3] - 0 === i; + + unbind : function(target, name, func) { + var t = this; + + if (!t.events) + t.events = new tinymce.dom.EventUtils(); + + return t.events.remove(target, name, func); }, - eq: function(elem, i, match){ - return match[3] - 0 === i; - } - }, - filter: { - PSEUDO: function(elem, match, i, array){ - var name = match[1], filter = Expr.filters[ name ]; - if ( filter ) { - return filter( elem, i, match, array ); - } else if ( name === "contains" ) { - return (elem.textContent || elem.innerText || Sizzle.getText([ elem ]) || "").indexOf(match[3]) >= 0; - } else if ( name === "not" ) { - var not = match[3]; - for ( var j = 0, l = not.length; j < l; j++ ) { - if ( not[j] === elem ) { - return false; - } + _findSib : function(node, selector, name) { + var t = this, f = selector; + + if (node) { + // If expression make a function of it using is + if (is(f, 'string')) { + f = function(node) { + return t.is(node, selector); + }; } - return true; - } else { - Sizzle.error( "Syntax error, unrecognized expression: " + name ); + // Loop all siblings + for (node = node[name]; node; node = node[name]) { + if (f(node)) + return node; + } } + + return null; }, - CHILD: function(elem, match){ - var type = match[1], node = elem; - switch (type) { - case 'only': - case 'first': - while ( (node = node.previousSibling) ) { - if ( node.nodeType === 1 ) { - return false; - } + + _isRes : function(c) { + // Is live resizble element + return /^(top|left|bottom|right|width|height)/i.test(c) || /;\s*(top|left|bottom|right|width|height)/i.test(c); + } + + /* + walk : function(n, f, s) { + var d = this.doc, w; + + if (d.createTreeWalker) { + w = d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, false); + + while ((n = w.nextNode()) != null) + f.call(s || this, n); + } else + tinymce.walk(n, f, 'childNodes', s); + } + */ + + /* + toRGB : function(s) { + var c = /^\s*?#([0-9A-F]{2})([0-9A-F]{1,2})([0-9A-F]{2})?\s*?$/.exec(s); + + if (c) { + // #FFF -> #FFFFFF + if (!is(c[3])) + c[3] = c[2] = c[1]; + + return "rgb(" + parseInt(c[1], 16) + "," + parseInt(c[2], 16) + "," + parseInt(c[3], 16) + ")"; + } + + return s; + } + */ + }); + + tinymce.DOM = new tinymce.dom.DOMUtils(document, {process_html : 0}); +})(tinymce); + +(function(ns) { + // Range constructor + function Range(dom) { + var t = this, + doc = dom.doc, + EXTRACT = 0, + CLONE = 1, + DELETE = 2, + TRUE = true, + FALSE = false, + START_OFFSET = 'startOffset', + START_CONTAINER = 'startContainer', + END_CONTAINER = 'endContainer', + END_OFFSET = 'endOffset', + extend = tinymce.extend, + nodeIndex = dom.nodeIndex; + + extend(t, { + // Inital states + startContainer : doc, + startOffset : 0, + endContainer : doc, + endOffset : 0, + collapsed : TRUE, + commonAncestorContainer : doc, + + // Range constants + START_TO_START : 0, + START_TO_END : 1, + END_TO_END : 2, + END_TO_START : 3, + + // Public methods + setStart : setStart, + setEnd : setEnd, + setStartBefore : setStartBefore, + setStartAfter : setStartAfter, + setEndBefore : setEndBefore, + setEndAfter : setEndAfter, + collapse : collapse, + selectNode : selectNode, + selectNodeContents : selectNodeContents, + compareBoundaryPoints : compareBoundaryPoints, + deleteContents : deleteContents, + extractContents : extractContents, + cloneContents : cloneContents, + insertNode : insertNode, + surroundContents : surroundContents, + cloneRange : cloneRange + }); + + function setStart(n, o) { + _setEndPoint(TRUE, n, o); + }; + + function setEnd(n, o) { + _setEndPoint(FALSE, n, o); + }; + + function setStartBefore(n) { + setStart(n.parentNode, nodeIndex(n)); + }; + + function setStartAfter(n) { + setStart(n.parentNode, nodeIndex(n) + 1); + }; + + function setEndBefore(n) { + setEnd(n.parentNode, nodeIndex(n)); + }; + + function setEndAfter(n) { + setEnd(n.parentNode, nodeIndex(n) + 1); + }; + + function collapse(ts) { + if (ts) { + t[END_CONTAINER] = t[START_CONTAINER]; + t[END_OFFSET] = t[START_OFFSET]; + } else { + t[START_CONTAINER] = t[END_CONTAINER]; + t[START_OFFSET] = t[END_OFFSET]; + } + + t.collapsed = TRUE; + }; + + function selectNode(n) { + setStartBefore(n); + setEndAfter(n); + }; + + function selectNodeContents(n) { + setStart(n, 0); + setEnd(n, n.nodeType === 1 ? n.childNodes.length : n.nodeValue.length); + }; + + function compareBoundaryPoints(h, r) { + var sc = t[START_CONTAINER], so = t[START_OFFSET], ec = t[END_CONTAINER], eo = t[END_OFFSET], + rsc = r.startContainer, rso = r.startOffset, rec = r.endContainer, reo = r.endOffset; + + // Check START_TO_START + if (h === 0) + return _compareBoundaryPoints(sc, so, rsc, rso); + + // Check START_TO_END + if (h === 1) + return _compareBoundaryPoints(ec, eo, rsc, rso); + + // Check END_TO_END + if (h === 2) + return _compareBoundaryPoints(ec, eo, rec, reo); + + // Check END_TO_START + if (h === 3) + return _compareBoundaryPoints(sc, so, rec, reo); + }; + + function deleteContents() { + _traverse(DELETE); + }; + + function extractContents() { + return _traverse(EXTRACT); + }; + + function cloneContents() { + return _traverse(CLONE); + }; + + function insertNode(n) { + var startContainer = this[START_CONTAINER], + startOffset = this[START_OFFSET], nn, o; + + // Node is TEXT_NODE or CDATA + if ((startContainer.nodeType === 3 || startContainer.nodeType === 4) && startContainer.nodeValue) { + if (!startOffset) { + // At the start of text + startContainer.parentNode.insertBefore(n, startContainer); + } else if (startOffset >= startContainer.nodeValue.length) { + // At the end of text + dom.insertAfter(n, startContainer); + } else { + // Middle, need to split + nn = startContainer.splitText(startOffset); + startContainer.parentNode.insertBefore(n, nn); + } + } else { + // Insert element node + if (startContainer.childNodes.length > 0) + o = startContainer.childNodes[startOffset]; + + if (o) + startContainer.insertBefore(n, o); + else + startContainer.appendChild(n); + } + }; + + function surroundContents(n) { + var f = t.extractContents(); + + t.insertNode(n); + n.appendChild(f); + t.selectNode(n); + }; + + function cloneRange() { + return extend(new Range(dom), { + startContainer : t[START_CONTAINER], + startOffset : t[START_OFFSET], + endContainer : t[END_CONTAINER], + endOffset : t[END_OFFSET], + collapsed : t.collapsed, + commonAncestorContainer : t.commonAncestorContainer + }); + }; + + // Private methods + + function _getSelectedNode(container, offset) { + var child; + + if (container.nodeType == 3 /* TEXT_NODE */) + return container; + + if (offset < 0) + return container; + + child = container.firstChild; + while (child && offset > 0) { + --offset; + child = child.nextSibling; + } + + if (child) + return child; + + return container; + }; + + function _isCollapsed() { + return (t[START_CONTAINER] == t[END_CONTAINER] && t[START_OFFSET] == t[END_OFFSET]); + }; + + function _compareBoundaryPoints(containerA, offsetA, containerB, offsetB) { + var c, offsetC, n, cmnRoot, childA, childB; + + // In the first case the boundary-points have the same container. A is before B + // if its offset is less than the offset of B, A is equal to B if its offset is + // equal to the offset of B, and A is after B if its offset is greater than the + // offset of B. + if (containerA == containerB) { + if (offsetA == offsetB) + return 0; // equal + + if (offsetA < offsetB) + return -1; // before + + return 1; // after + } + + // In the second case a child node C of the container of A is an ancestor + // container of B. In this case, A is before B if the offset of A is less than or + // equal to the index of the child node C and A is after B otherwise. + c = containerB; + while (c && c.parentNode != containerA) + c = c.parentNode; + + if (c) { + offsetC = 0; + n = containerA.firstChild; + + while (n != c && offsetC < offsetA) { + offsetC++; + n = n.nextSibling; + } + + if (offsetA <= offsetC) + return -1; // before + + return 1; // after + } + + // In the third case a child node C of the container of B is an ancestor container + // of A. In this case, A is before B if the index of the child node C is less than + // the offset of B and A is after B otherwise. + c = containerA; + while (c && c.parentNode != containerB) { + c = c.parentNode; + } + + if (c) { + offsetC = 0; + n = containerB.firstChild; + + while (n != c && offsetC < offsetB) { + offsetC++; + n = n.nextSibling; + } + + if (offsetC < offsetB) + return -1; // before + + return 1; // after + } + + // In the fourth case, none of three other cases hold: the containers of A and B + // are siblings or descendants of sibling nodes. In this case, A is before B if + // the container of A is before the container of B in a pre-order traversal of the + // Ranges' context tree and A is after B otherwise. + cmnRoot = dom.findCommonAncestor(containerA, containerB); + childA = containerA; + + while (childA && childA.parentNode != cmnRoot) + childA = childA.parentNode; + + if (!childA) + childA = cmnRoot; + + childB = containerB; + while (childB && childB.parentNode != cmnRoot) + childB = childB.parentNode; + + if (!childB) + childB = cmnRoot; + + if (childA == childB) + return 0; // equal + + n = cmnRoot.firstChild; + while (n) { + if (n == childA) + return -1; // before + + if (n == childB) + return 1; // after + + n = n.nextSibling; + } + }; + + function _setEndPoint(st, n, o) { + var ec, sc; + + if (st) { + t[START_CONTAINER] = n; + t[START_OFFSET] = o; + } else { + t[END_CONTAINER] = n; + t[END_OFFSET] = o; + } + + // If one boundary-point of a Range is set to have a root container + // other than the current one for the Range, the Range is collapsed to + // the new position. This enforces the restriction that both boundary- + // points of a Range must have the same root container. + ec = t[END_CONTAINER]; + while (ec.parentNode) + ec = ec.parentNode; + + sc = t[START_CONTAINER]; + while (sc.parentNode) + sc = sc.parentNode; + + if (sc == ec) { + // The start position of a Range is guaranteed to never be after the + // end position. To enforce this restriction, if the start is set to + // be at a position after the end, the Range is collapsed to that + // position. + if (_compareBoundaryPoints(t[START_CONTAINER], t[START_OFFSET], t[END_CONTAINER], t[END_OFFSET]) > 0) + t.collapse(st); + } else + t.collapse(st); + + t.collapsed = _isCollapsed(); + t.commonAncestorContainer = dom.findCommonAncestor(t[START_CONTAINER], t[END_CONTAINER]); + }; + + function _traverse(how) { + var c, endContainerDepth = 0, startContainerDepth = 0, p, depthDiff, startNode, endNode, sp, ep; + + if (t[START_CONTAINER] == t[END_CONTAINER]) + return _traverseSameContainer(how); + + for (c = t[END_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { + if (p == t[START_CONTAINER]) + return _traverseCommonStartContainer(c, how); + + ++endContainerDepth; + } + + for (c = t[START_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { + if (p == t[END_CONTAINER]) + return _traverseCommonEndContainer(c, how); + + ++startContainerDepth; + } + + depthDiff = startContainerDepth - endContainerDepth; + + startNode = t[START_CONTAINER]; + while (depthDiff > 0) { + startNode = startNode.parentNode; + depthDiff--; + } + + endNode = t[END_CONTAINER]; + while (depthDiff < 0) { + endNode = endNode.parentNode; + depthDiff++; + } + + // ascend the ancestor hierarchy until we have a common parent. + for (sp = startNode.parentNode, ep = endNode.parentNode; sp != ep; sp = sp.parentNode, ep = ep.parentNode) { + startNode = sp; + endNode = ep; + } + + return _traverseCommonAncestors(startNode, endNode, how); + }; + + function _traverseSameContainer(how) { + var frag, s, sub, n, cnt, sibling, xferNode; + + if (how != DELETE) + frag = doc.createDocumentFragment(); + + // If selection is empty, just return the fragment + if (t[START_OFFSET] == t[END_OFFSET]) + return frag; + + // Text node needs special case handling + if (t[START_CONTAINER].nodeType == 3 /* TEXT_NODE */) { + // get the substring + s = t[START_CONTAINER].nodeValue; + sub = s.substring(t[START_OFFSET], t[END_OFFSET]); + + // set the original text node to its new value + if (how != CLONE) { + t[START_CONTAINER].deleteData(t[START_OFFSET], t[END_OFFSET] - t[START_OFFSET]); + + // Nothing is partially selected, so collapse to start point + t.collapse(TRUE); + } + + if (how == DELETE) + return; + + frag.appendChild(doc.createTextNode(sub)); + return frag; + } + + // Copy nodes between the start/end offsets. + n = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]); + cnt = t[END_OFFSET] - t[START_OFFSET]; + + while (cnt > 0) { + sibling = n.nextSibling; + xferNode = _traverseFullySelected(n, how); + + if (frag) + frag.appendChild( xferNode ); + + --cnt; + n = sibling; + } + + // Nothing is partially selected, so collapse to start point + if (how != CLONE) + t.collapse(TRUE); + + return frag; + }; + + function _traverseCommonStartContainer(endAncestor, how) { + var frag, n, endIdx, cnt, sibling, xferNode; + + if (how != DELETE) + frag = doc.createDocumentFragment(); + + n = _traverseRightBoundary(endAncestor, how); + + if (frag) + frag.appendChild(n); + + endIdx = nodeIndex(endAncestor); + cnt = endIdx - t[START_OFFSET]; + + if (cnt <= 0) { + // Collapse to just before the endAncestor, which + // is partially selected. + if (how != CLONE) { + t.setEndBefore(endAncestor); + t.collapse(FALSE); + } + + return frag; + } + + n = endAncestor.previousSibling; + while (cnt > 0) { + sibling = n.previousSibling; + xferNode = _traverseFullySelected(n, how); + + if (frag) + frag.insertBefore(xferNode, frag.firstChild); + + --cnt; + n = sibling; + } + + // Collapse to just before the endAncestor, which + // is partially selected. + if (how != CLONE) { + t.setEndBefore(endAncestor); + t.collapse(FALSE); + } + + return frag; + }; + + function _traverseCommonEndContainer(startAncestor, how) { + var frag, startIdx, n, cnt, sibling, xferNode; + + if (how != DELETE) + frag = doc.createDocumentFragment(); + + n = _traverseLeftBoundary(startAncestor, how); + if (frag) + frag.appendChild(n); + + startIdx = nodeIndex(startAncestor); + ++startIdx; // Because we already traversed it + + cnt = t[END_OFFSET] - startIdx; + n = startAncestor.nextSibling; + while (cnt > 0) { + sibling = n.nextSibling; + xferNode = _traverseFullySelected(n, how); + + if (frag) + frag.appendChild(xferNode); + + --cnt; + n = sibling; + } + + if (how != CLONE) { + t.setStartAfter(startAncestor); + t.collapse(TRUE); + } + + return frag; + }; + + function _traverseCommonAncestors(startAncestor, endAncestor, how) { + var n, frag, commonParent, startOffset, endOffset, cnt, sibling, nextSibling; + + if (how != DELETE) + frag = doc.createDocumentFragment(); + + n = _traverseLeftBoundary(startAncestor, how); + if (frag) + frag.appendChild(n); + + commonParent = startAncestor.parentNode; + startOffset = nodeIndex(startAncestor); + endOffset = nodeIndex(endAncestor); + ++startOffset; + + cnt = endOffset - startOffset; + sibling = startAncestor.nextSibling; + + while (cnt > 0) { + nextSibling = sibling.nextSibling; + n = _traverseFullySelected(sibling, how); + + if (frag) + frag.appendChild(n); + + sibling = nextSibling; + --cnt; + } + + n = _traverseRightBoundary(endAncestor, how); + + if (frag) + frag.appendChild(n); + + if (how != CLONE) { + t.setStartAfter(startAncestor); + t.collapse(TRUE); + } + + return frag; + }; + + function _traverseRightBoundary(root, how) { + var next = _getSelectedNode(t[END_CONTAINER], t[END_OFFSET] - 1), parent, clonedParent, prevSibling, clonedChild, clonedGrandParent, isFullySelected = next != t[END_CONTAINER]; + + if (next == root) + return _traverseNode(next, isFullySelected, FALSE, how); + + parent = next.parentNode; + clonedParent = _traverseNode(parent, FALSE, FALSE, how); + + while (parent) { + while (next) { + prevSibling = next.previousSibling; + clonedChild = _traverseNode(next, isFullySelected, FALSE, how); + + if (how != DELETE) + clonedParent.insertBefore(clonedChild, clonedParent.firstChild); + + isFullySelected = TRUE; + next = prevSibling; + } + + if (parent == root) + return clonedParent; + + next = parent.previousSibling; + parent = parent.parentNode; + + clonedGrandParent = _traverseNode(parent, FALSE, FALSE, how); + + if (how != DELETE) + clonedGrandParent.appendChild(clonedParent); + + clonedParent = clonedGrandParent; + } + }; + + function _traverseLeftBoundary(root, how) { + var next = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]), isFullySelected = next != t[START_CONTAINER], parent, clonedParent, nextSibling, clonedChild, clonedGrandParent; + + if (next == root) + return _traverseNode(next, isFullySelected, TRUE, how); + + parent = next.parentNode; + clonedParent = _traverseNode(parent, FALSE, TRUE, how); + + while (parent) { + while (next) { + nextSibling = next.nextSibling; + clonedChild = _traverseNode(next, isFullySelected, TRUE, how); + + if (how != DELETE) + clonedParent.appendChild(clonedChild); + + isFullySelected = TRUE; + next = nextSibling; + } + + if (parent == root) + return clonedParent; + + next = parent.nextSibling; + parent = parent.parentNode; + + clonedGrandParent = _traverseNode(parent, FALSE, TRUE, how); + + if (how != DELETE) + clonedGrandParent.appendChild(clonedParent); + + clonedParent = clonedGrandParent; + } + }; + + function _traverseNode(n, isFullySelected, isLeft, how) { + var txtValue, newNodeValue, oldNodeValue, offset, newNode; + + if (isFullySelected) + return _traverseFullySelected(n, how); + + if (n.nodeType == 3 /* TEXT_NODE */) { + txtValue = n.nodeValue; + + if (isLeft) { + offset = t[START_OFFSET]; + newNodeValue = txtValue.substring(offset); + oldNodeValue = txtValue.substring(0, offset); + } else { + offset = t[END_OFFSET]; + newNodeValue = txtValue.substring(0, offset); + oldNodeValue = txtValue.substring(offset); + } + + if (how != CLONE) + n.nodeValue = oldNodeValue; + + if (how == DELETE) + return; + + newNode = n.cloneNode(FALSE); + newNode.nodeValue = newNodeValue; + + return newNode; + } + + if (how == DELETE) + return; + + return n.cloneNode(FALSE); + }; + + function _traverseFullySelected(n, how) { + if (how != DELETE) + return how == CLONE ? n.cloneNode(TRUE) : n; + + n.parentNode.removeChild(n); + }; + }; + + ns.Range = Range; +})(tinymce.dom); + +(function() { + function Selection(selection) { + var self = this, dom = selection.dom, TRUE = true, FALSE = false; + + function getPosition(rng, start) { + var checkRng, startIndex = 0, endIndex, inside, + children, child, offset, index, position = -1, parent; + + // Setup test range, collapse it and get the parent + checkRng = rng.duplicate(); + checkRng.collapse(start); + parent = checkRng.parentElement(); + + // Check if the selection is within the right document + if (parent.ownerDocument !== selection.dom.doc) + return; + + // IE will report non editable elements as it's parent so look for an editable one + while (parent.contentEditable === "false") { + parent = parent.parentNode; + } + + // If parent doesn't have any children then return that we are inside the element + if (!parent.hasChildNodes()) { + return {node : parent, inside : 1}; + } + + // Setup node list and endIndex + children = parent.children; + endIndex = children.length - 1; + + // Perform a binary search for the position + while (startIndex <= endIndex) { + index = Math.floor((startIndex + endIndex) / 2); + + // Move selection to node and compare the ranges + child = children[index]; + checkRng.moveToElementText(child); + position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng); + + // Before/after or an exact match + if (position > 0) { + endIndex = index - 1; + } else if (position < 0) { + startIndex = index + 1; + } else { + return {node : child}; + } + } + + // Check if child position is before or we didn't find a position + if (position < 0) { + // No element child was found use the parent element and the offset inside that + if (!child) { + checkRng.moveToElementText(parent); + checkRng.collapse(true); + child = parent; + inside = true; + } else + checkRng.collapse(false); + + checkRng.setEndPoint(start ? 'EndToStart' : 'EndToEnd', rng); + + // Fix for edge case:
  • - +
    @@ -150,10 +151,10 @@ - - + +
     
    - +
    @@ -162,10 +163,10 @@ - - + + <\/tr>/g, ''); return html; } diff --git a/js/tiny_mce/themes/advanced/js/color_picker.js b/js/tiny_mce/themes/advanced/js/color_picker.js index fd9700f222..f51e703b0a 100644 --- a/js/tiny_mce/themes/advanced/js/color_picker.js +++ b/js/tiny_mce/themes/advanced/js/color_picker.js @@ -33,37 +33,41 @@ var colors = [ ]; var named = { - '#F0F8FF':'AliceBlue','#FAEBD7':'AntiqueWhite','#00FFFF':'Aqua','#7FFFD4':'Aquamarine','#F0FFFF':'Azure','#F5F5DC':'Beige', - '#FFE4C4':'Bisque','#000000':'Black','#FFEBCD':'BlanchedAlmond','#0000FF':'Blue','#8A2BE2':'BlueViolet','#A52A2A':'Brown', - '#DEB887':'BurlyWood','#5F9EA0':'CadetBlue','#7FFF00':'Chartreuse','#D2691E':'Chocolate','#FF7F50':'Coral','#6495ED':'CornflowerBlue', - '#FFF8DC':'Cornsilk','#DC143C':'Crimson','#00FFFF':'Cyan','#00008B':'DarkBlue','#008B8B':'DarkCyan','#B8860B':'DarkGoldenRod', - '#A9A9A9':'DarkGray','#A9A9A9':'DarkGrey','#006400':'DarkGreen','#BDB76B':'DarkKhaki','#8B008B':'DarkMagenta','#556B2F':'DarkOliveGreen', - '#FF8C00':'Darkorange','#9932CC':'DarkOrchid','#8B0000':'DarkRed','#E9967A':'DarkSalmon','#8FBC8F':'DarkSeaGreen','#483D8B':'DarkSlateBlue', - '#2F4F4F':'DarkSlateGray','#2F4F4F':'DarkSlateGrey','#00CED1':'DarkTurquoise','#9400D3':'DarkViolet','#FF1493':'DeepPink','#00BFFF':'DeepSkyBlue', - '#696969':'DimGray','#696969':'DimGrey','#1E90FF':'DodgerBlue','#B22222':'FireBrick','#FFFAF0':'FloralWhite','#228B22':'ForestGreen', - '#FF00FF':'Fuchsia','#DCDCDC':'Gainsboro','#F8F8FF':'GhostWhite','#FFD700':'Gold','#DAA520':'GoldenRod','#808080':'Gray','#808080':'Grey', - '#008000':'Green','#ADFF2F':'GreenYellow','#F0FFF0':'HoneyDew','#FF69B4':'HotPink','#CD5C5C':'IndianRed','#4B0082':'Indigo','#FFFFF0':'Ivory', - '#F0E68C':'Khaki','#E6E6FA':'Lavender','#FFF0F5':'LavenderBlush','#7CFC00':'LawnGreen','#FFFACD':'LemonChiffon','#ADD8E6':'LightBlue', - '#F08080':'LightCoral','#E0FFFF':'LightCyan','#FAFAD2':'LightGoldenRodYellow','#D3D3D3':'LightGray','#D3D3D3':'LightGrey','#90EE90':'LightGreen', - '#FFB6C1':'LightPink','#FFA07A':'LightSalmon','#20B2AA':'LightSeaGreen','#87CEFA':'LightSkyBlue','#778899':'LightSlateGray','#778899':'LightSlateGrey', - '#B0C4DE':'LightSteelBlue','#FFFFE0':'LightYellow','#00FF00':'Lime','#32CD32':'LimeGreen','#FAF0E6':'Linen','#FF00FF':'Magenta','#800000':'Maroon', - '#66CDAA':'MediumAquaMarine','#0000CD':'MediumBlue','#BA55D3':'MediumOrchid','#9370D8':'MediumPurple','#3CB371':'MediumSeaGreen','#7B68EE':'MediumSlateBlue', - '#00FA9A':'MediumSpringGreen','#48D1CC':'MediumTurquoise','#C71585':'MediumVioletRed','#191970':'MidnightBlue','#F5FFFA':'MintCream','#FFE4E1':'MistyRose','#FFE4B5':'Moccasin', - '#FFDEAD':'NavajoWhite','#000080':'Navy','#FDF5E6':'OldLace','#808000':'Olive','#6B8E23':'OliveDrab','#FFA500':'Orange','#FF4500':'OrangeRed','#DA70D6':'Orchid', - '#EEE8AA':'PaleGoldenRod','#98FB98':'PaleGreen','#AFEEEE':'PaleTurquoise','#D87093':'PaleVioletRed','#FFEFD5':'PapayaWhip','#FFDAB9':'PeachPuff', - '#CD853F':'Peru','#FFC0CB':'Pink','#DDA0DD':'Plum','#B0E0E6':'PowderBlue','#800080':'Purple','#FF0000':'Red','#BC8F8F':'RosyBrown','#4169E1':'RoyalBlue', - '#8B4513':'SaddleBrown','#FA8072':'Salmon','#F4A460':'SandyBrown','#2E8B57':'SeaGreen','#FFF5EE':'SeaShell','#A0522D':'Sienna','#C0C0C0':'Silver', - '#87CEEB':'SkyBlue','#6A5ACD':'SlateBlue','#708090':'SlateGray','#708090':'SlateGrey','#FFFAFA':'Snow','#00FF7F':'SpringGreen', - '#4682B4':'SteelBlue','#D2B48C':'Tan','#008080':'Teal','#D8BFD8':'Thistle','#FF6347':'Tomato','#40E0D0':'Turquoise','#EE82EE':'Violet', - '#F5DEB3':'Wheat','#FFFFFF':'White','#F5F5F5':'WhiteSmoke','#FFFF00':'Yellow','#9ACD32':'YellowGreen' + '#F0F8FF':'Alice Blue','#FAEBD7':'Antique White','#00FFFF':'Aqua','#7FFFD4':'Aquamarine','#F0FFFF':'Azure','#F5F5DC':'Beige', + '#FFE4C4':'Bisque','#000000':'Black','#FFEBCD':'Blanched Almond','#0000FF':'Blue','#8A2BE2':'Blue Violet','#A52A2A':'Brown', + '#DEB887':'Burly Wood','#5F9EA0':'Cadet Blue','#7FFF00':'Chartreuse','#D2691E':'Chocolate','#FF7F50':'Coral','#6495ED':'Cornflower Blue', + '#FFF8DC':'Cornsilk','#DC143C':'Crimson','#00FFFF':'Cyan','#00008B':'Dark Blue','#008B8B':'Dark Cyan','#B8860B':'Dark Golden Rod', + '#A9A9A9':'Dark Gray','#A9A9A9':'Dark Grey','#006400':'Dark Green','#BDB76B':'Dark Khaki','#8B008B':'Dark Magenta','#556B2F':'Dark Olive Green', + '#FF8C00':'Darkorange','#9932CC':'Dark Orchid','#8B0000':'Dark Red','#E9967A':'Dark Salmon','#8FBC8F':'Dark Sea Green','#483D8B':'Dark Slate Blue', + '#2F4F4F':'Dark Slate Gray','#2F4F4F':'Dark Slate Grey','#00CED1':'Dark Turquoise','#9400D3':'Dark Violet','#FF1493':'Deep Pink','#00BFFF':'Deep Sky Blue', + '#696969':'Dim Gray','#696969':'Dim Grey','#1E90FF':'Dodger Blue','#B22222':'Fire Brick','#FFFAF0':'Floral White','#228B22':'Forest Green', + '#FF00FF':'Fuchsia','#DCDCDC':'Gainsboro','#F8F8FF':'Ghost White','#FFD700':'Gold','#DAA520':'Golden Rod','#808080':'Gray','#808080':'Grey', + '#008000':'Green','#ADFF2F':'Green Yellow','#F0FFF0':'Honey Dew','#FF69B4':'Hot Pink','#CD5C5C':'Indian Red','#4B0082':'Indigo','#FFFFF0':'Ivory', + '#F0E68C':'Khaki','#E6E6FA':'Lavender','#FFF0F5':'Lavender Blush','#7CFC00':'Lawn Green','#FFFACD':'Lemon Chiffon','#ADD8E6':'Light Blue', + '#F08080':'Light Coral','#E0FFFF':'Light Cyan','#FAFAD2':'Light Golden Rod Yellow','#D3D3D3':'Light Gray','#D3D3D3':'Light Grey','#90EE90':'Light Green', + '#FFB6C1':'Light Pink','#FFA07A':'Light Salmon','#20B2AA':'Light Sea Green','#87CEFA':'Light Sky Blue','#778899':'Light Slate Gray','#778899':'Light Slate Grey', + '#B0C4DE':'Light Steel Blue','#FFFFE0':'Light Yellow','#00FF00':'Lime','#32CD32':'Lime Green','#FAF0E6':'Linen','#FF00FF':'Magenta','#800000':'Maroon', + '#66CDAA':'Medium Aqua Marine','#0000CD':'Medium Blue','#BA55D3':'Medium Orchid','#9370D8':'Medium Purple','#3CB371':'Medium Sea Green','#7B68EE':'Medium Slate Blue', + '#00FA9A':'Medium Spring Green','#48D1CC':'Medium Turquoise','#C71585':'Medium Violet Red','#191970':'Midnight Blue','#F5FFFA':'Mint Cream','#FFE4E1':'Misty Rose','#FFE4B5':'Moccasin', + '#FFDEAD':'Navajo White','#000080':'Navy','#FDF5E6':'Old Lace','#808000':'Olive','#6B8E23':'Olive Drab','#FFA500':'Orange','#FF4500':'Orange Red','#DA70D6':'Orchid', + '#EEE8AA':'Pale Golden Rod','#98FB98':'Pale Green','#AFEEEE':'Pale Turquoise','#D87093':'Pale Violet Red','#FFEFD5':'Papaya Whip','#FFDAB9':'Peach Puff', + '#CD853F':'Peru','#FFC0CB':'Pink','#DDA0DD':'Plum','#B0E0E6':'Powder Blue','#800080':'Purple','#FF0000':'Red','#BC8F8F':'Rosy Brown','#4169E1':'Royal Blue', + '#8B4513':'Saddle Brown','#FA8072':'Salmon','#F4A460':'Sandy Brown','#2E8B57':'Sea Green','#FFF5EE':'Sea Shell','#A0522D':'Sienna','#C0C0C0':'Silver', + '#87CEEB':'Sky Blue','#6A5ACD':'Slate Blue','#708090':'Slate Gray','#708090':'Slate Grey','#FFFAFA':'Snow','#00FF7F':'Spring Green', + '#4682B4':'Steel Blue','#D2B48C':'Tan','#008080':'Teal','#D8BFD8':'Thistle','#FF6347':'Tomato','#40E0D0':'Turquoise','#EE82EE':'Violet', + '#F5DEB3':'Wheat','#FFFFFF':'White','#F5F5F5':'White Smoke','#FFFF00':'Yellow','#9ACD32':'Yellow Green' }; +var namedLookup = {}; + function init() { - var inputColor = convertRGBToHex(tinyMCEPopup.getWindowArg('input_color')); + var inputColor = convertRGBToHex(tinyMCEPopup.getWindowArg('input_color')), key, value; tinyMCEPopup.resizeToInnerSize(); generatePicker(); + generateWebColors(); + generateNamedColors(); if (inputColor) { changeFinalColor(inputColor); @@ -73,6 +77,45 @@ function init() { if (col) updateLight(col.r, col.g, col.b); } + + for (key in named) { + value = named[key]; + namedLookup[value.replace(/\s+/, '').toLowerCase()] = key.replace(/#/, '').toLowerCase(); + } +} + +function toHexColor(color) { + var matches, red, green, blue, toInt = parseInt; + + function hex(value) { + value = parseInt(value).toString(16); + + return value.length > 1 ? value : '0' + value; // Padd with leading zero + }; + + color = color.replace(/[\s#]+/g, '').toLowerCase(); + color = namedLookup[color] || color; + matches = /^rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)|([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})|([a-f0-9])([a-f0-9])([a-f0-9])$/.exec(color); + + if (matches) { + if (matches[1]) { + red = toInt(matches[1]); + green = toInt(matches[2]); + blue = toInt(matches[3]); + } else if (matches[4]) { + red = toInt(matches[4], 16); + green = toInt(matches[5], 16); + blue = toInt(matches[6], 16); + } else if (matches[7]) { + red = toInt(matches[7] + matches[7], 16); + green = toInt(matches[8] + matches[8], 16); + blue = toInt(matches[9] + matches[9], 16); + } + + return '#' + hex(red) + hex(green) + hex(blue); + } + + return ''; } function insertAction() { @@ -81,7 +124,7 @@ function insertAction() { tinyMCEPopup.restoreSelection(); if (f) - f(color); + f(toHexColor(color)); tinyMCEPopup.close(); } @@ -91,7 +134,7 @@ function showColor(color, name) { document.getElementById("colorname").innerHTML = name; document.getElementById("preview").style.backgroundColor = color; - document.getElementById("color").value = color.toLowerCase(); + document.getElementById("color").value = color.toUpperCase(); } function convertRGBToHex(col) { @@ -153,23 +196,40 @@ function generateWebColors() { if (el.className == 'generated') return; - h += '
     
    - +
    diff --git a/js/tiny_mce/plugins/template/js/template.js b/js/tiny_mce/plugins/template/js/template.js index 24045d7311..bc3045d244 100644 --- a/js/tiny_mce/plugins/template/js/template.js +++ b/js/tiny_mce/plugins/template/js/template.js @@ -42,7 +42,7 @@ var TemplateDialog = { if (e) { e.style.height = Math.abs(h) + 'px'; - e.style.width = Math.abs(w - 5) + 'px'; + e.style.width = Math.abs(w - 5) + 'px'; } }, diff --git a/js/tiny_mce/plugins/template/langs/en_dlg.js b/js/tiny_mce/plugins/template/langs/en_dlg.js index 2471c3fa04..83e599d68f 100644 --- a/js/tiny_mce/plugins/template/langs/en_dlg.js +++ b/js/tiny_mce/plugins/template/langs/en_dlg.js @@ -1,15 +1 @@ -tinyMCE.addI18n('en.template_dlg',{ -title:"Templates", -label:"Template", -desc_label:"Description", -desc:"Insert predefined template content", -select:"Select a template", -preview:"Preview", -warning:"Warning: Updating a template with a different one may cause data loss.", -mdate_format:"%Y-%m-%d %H:%M:%S", -cdate_format:"%Y-%m-%d %H:%M:%S", -months_long:"January,February,March,April,May,June,July,August,September,October,November,December", -months_short:"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec", -day_long:"Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday", -day_short:"Sun,Mon,Tue,Wed,Thu,Fri,Sat,Sun" -}); \ No newline at end of file +tinyMCE.addI18n('en.template_dlg',{title:"Templates",label:"Template","desc_label":"Description",desc:"Insert Predefined Template Content",select:"Select a Template",preview:"Preview",warning:"Warning: Updating a template with a different one may cause data loss.","mdate_format":"%Y-%m-%d %H:%M:%S","cdate_format":"%Y-%m-%d %H:%M:%S","months_long":"January,February,March,April,May,June,July,August,September,October,November,December","months_short":"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec","day_long":"Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday","day_short":"Sun,Mon,Tue,Wed,Thu,Fri,Sat,Sun"}); \ No newline at end of file diff --git a/js/tiny_mce/plugins/visualchars/editor_plugin.js b/js/tiny_mce/plugins/visualchars/editor_plugin.js index 53d31c44fa..1a148e8b4f 100644 --- a/js/tiny_mce/plugins/visualchars/editor_plugin.js +++ b/js/tiny_mce/plugins/visualchars/editor_plugin.js @@ -1 +1 @@ -(function(){tinymce.create("tinymce.plugins.VisualChars",{init:function(a,b){var c=this;c.editor=a;a.addCommand("mceVisualChars",c._toggleVisualChars,c);a.addButton("visualchars",{title:"visualchars.desc",cmd:"mceVisualChars"});a.onBeforeGetContent.add(function(d,e){if(c.state){c.state=true;c._toggleVisualChars()}})},getInfo:function(){return{longname:"Visual characters",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",infourl:"http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/visualchars",version:tinymce.majorVersion+"."+tinymce.minorVersion}},_toggleVisualChars:function(){var m=this,g=m.editor,a,e,f,k=g.getDoc(),l=g.getBody(),j,n=g.selection,c;m.state=!m.state;g.controlManager.setActive("visualchars",m.state);if(m.state){a=[];tinymce.walk(l,function(b){if(b.nodeType==3&&b.nodeValue&&b.nodeValue.indexOf("\u00a0")!=-1){a.push(b)}},"childNodes");for(e=0;e$1');j=j.replace(/\u00a0/g,"\u00b7");g.dom.setOuterHTML(a[e],j,k)}}else{a=tinymce.grep(g.dom.select("span",l),function(b){return g.dom.hasClass(b,"mceVisualNbsp")});for(e=0;e$1');c=k.dom.create("div",null,l);while(node=c.lastChild){k.dom.insertAfter(node,a[g])}k.dom.remove(a[g])}}else{a=k.dom.select("span.mceItemNbsp",o);for(g=a.length-1;g>=0;g--){k.dom.remove(a[g],1)}}q.moveToBookmark(f)}});tinymce.PluginManager.add("visualchars",tinymce.plugins.VisualChars)})(); \ No newline at end of file diff --git a/js/tiny_mce/plugins/visualchars/editor_plugin_src.js b/js/tiny_mce/plugins/visualchars/editor_plugin_src.js index 0a5275fe28..df985905b6 100644 --- a/js/tiny_mce/plugins/visualchars/editor_plugin_src.js +++ b/js/tiny_mce/plugins/visualchars/editor_plugin_src.js @@ -22,9 +22,9 @@ ed.addButton('visualchars', {title : 'visualchars.desc', cmd : 'mceVisualChars'}); ed.onBeforeGetContent.add(function(ed, o) { - if (t.state) { + if (t.state && o.format != 'raw' && !o.draft) { t.state = true; - t._toggleVisualChars(); + t._toggleVisualChars(false); } }); }, @@ -41,12 +41,15 @@ // Private methods - _toggleVisualChars : function() { - var t = this, ed = t.editor, nl, i, h, d = ed.getDoc(), b = ed.getBody(), nv, s = ed.selection, bo; + _toggleVisualChars : function(bookmark) { + var t = this, ed = t.editor, nl, i, h, d = ed.getDoc(), b = ed.getBody(), nv, s = ed.selection, bo, div, bm; t.state = !t.state; ed.controlManager.setActive('visualchars', t.state); + if (bookmark) + bm = s.getBookmark(); + if (t.state) { nl = []; tinymce.walk(b, function(n) { @@ -54,20 +57,24 @@ nl.push(n); }, 'childNodes'); - for (i=0; i$1'); - nv = nv.replace(/\u00a0/g, '\u00b7'); - ed.dom.setOuterHTML(nl[i], nv, d); + nv = nv.replace(/(\u00a0)/g, '$1'); + + div = ed.dom.create('div', null, nv); + while (node = div.lastChild) + ed.dom.insertAfter(node, nl[i]); + + ed.dom.remove(nl[i]); } } else { - nl = tinymce.grep(ed.dom.select('span', b), function(n) { - return ed.dom.hasClass(n, 'mceVisualNbsp'); - }); + nl = ed.dom.select('span.mceItemNbsp', b); - for (i=0; i= 0; i--) + ed.dom.remove(nl[i], 1); } + + s.moveToBookmark(bm); } }); diff --git a/js/tiny_mce/plugins/wordcount/editor_plugin.js b/js/tiny_mce/plugins/wordcount/editor_plugin.js index a099e6a8c5..a752ad32ae 100644 --- a/js/tiny_mce/plugins/wordcount/editor_plugin.js +++ b/js/tiny_mce/plugins/wordcount/editor_plugin.js @@ -1 +1 @@ -(function(){tinymce.create("tinymce.plugins.WordCount",{block:0,id:null,countre:null,cleanre:null,init:function(a,b){var c=this,d=0;c.countre=a.getParam("wordcount_countregex",/\S\s+/g);c.cleanre=a.getParam("wordcount_cleanregex",/[0-9.(),;:!?%#$¿'"_+=\\\/-]*/g);c.id=a.id+"-word-count";a.onPostRender.add(function(f,e){var g,h;h=f.getParam("wordcount_target_id");if(!h){g=tinymce.DOM.get(f.id+"_path_row");if(g){tinymce.DOM.add(g.parentNode,"div",{style:"float: right"},f.getLang("wordcount.words","Words: ")+'0')}}else{tinymce.DOM.add(h,"span",{},'0')}});a.onInit.add(function(e){e.selection.onSetContent.add(function(){c._count(e)});c._count(e)});a.onSetContent.add(function(e){c._count(e)});a.onKeyUp.add(function(f,g){if(g.keyCode==d){return}if(13==g.keyCode||8==d||46==d){c._count(f)}d=g.keyCode})},_count:function(b){var c=this,a=0;if(c.block){return}c.block=1;setTimeout(function(){var d=b.getContent({format:"raw"});if(d){d=d.replace(/<.[^<>]*?>/g," ").replace(/ | /gi," ");d=d.replace(c.cleanre,"");d.replace(c.countre,function(){a++})}tinymce.DOM.setHTML(c.id,a.toString());setTimeout(function(){c.block=0},2000)},1)},getInfo:function(){return{longname:"Word Count plugin",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",infourl:"http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/wordcount",version:tinymce.majorVersion+"."+tinymce.minorVersion}}});tinymce.PluginManager.add("wordcount",tinymce.plugins.WordCount)})(); \ No newline at end of file +(function(){tinymce.create("tinymce.plugins.WordCount",{block:0,id:null,countre:null,cleanre:null,init:function(a,b){var c=this,d=0;c.countre=a.getParam("wordcount_countregex",/[\w\u2019\'-]+/g);c.cleanre=a.getParam("wordcount_cleanregex",/[0-9.(),;:!?%#$?\'\"_+=\\\/-]*/g);c.id=a.id+"-word-count";a.onPostRender.add(function(f,e){var g,h;h=f.getParam("wordcount_target_id");if(!h){g=tinymce.DOM.get(f.id+"_path_row");if(g){tinymce.DOM.add(g.parentNode,"div",{style:"float: right"},f.getLang("wordcount.words","Words: ")+'0')}}else{tinymce.DOM.add(h,"span",{},'0')}});a.onInit.add(function(e){e.selection.onSetContent.add(function(){c._count(e)});c._count(e)});a.onSetContent.add(function(e){c._count(e)});a.onKeyUp.add(function(f,g){if(g.keyCode==d){return}if(13==g.keyCode||8==d||46==d){c._count(f)}d=g.keyCode})},_getCount:function(c){var a=0;var b=c.getContent({format:"raw"});if(b){b=b.replace(/\.\.\./g," ");b=b.replace(/<.[^<>]*?>/g," ").replace(/ | /gi," ");b=b.replace(/(\w+)(&.+?;)+(\w+)/,"$1$3").replace(/&.+?;/g," ");b=b.replace(this.cleanre,"");var d=b.match(this.countre);if(d){a=d.length}}return a},_count:function(a){var b=this;if(b.block){return}b.block=1;setTimeout(function(){if(!a.destroyed){var c=b._getCount(a);tinymce.DOM.setHTML(b.id,c.toString());setTimeout(function(){b.block=0},2000)}},1)},getInfo:function(){return{longname:"Word Count plugin",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",infourl:"http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/wordcount",version:tinymce.majorVersion+"."+tinymce.minorVersion}}});tinymce.PluginManager.add("wordcount",tinymce.plugins.WordCount)})(); \ No newline at end of file diff --git a/js/tiny_mce/plugins/wordcount/editor_plugin_src.js b/js/tiny_mce/plugins/wordcount/editor_plugin_src.js index 5cb92fa0f0..e94743bae1 100644 --- a/js/tiny_mce/plugins/wordcount/editor_plugin_src.js +++ b/js/tiny_mce/plugins/wordcount/editor_plugin_src.js @@ -9,7 +9,7 @@ */ (function() { - tinymce.create('tinymce.plugins.WordCount', { + tinymce.create('tinymce.plugins.WordCount', { block : 0, id : null, countre : null, @@ -18,8 +18,8 @@ init : function(ed, url) { var t = this, last = 0; - t.countre = ed.getParam('wordcount_countregex', /\S\s+/g); - t.cleanre = ed.getParam('wordcount_cleanregex', /[0-9.(),;:!?%#$¿'"_+=\\\/-]*/g); + t.countre = ed.getParam('wordcount_countregex', /[\w\u2019\'-]+/g); // u2019 == ’ + t.cleanre = ed.getParam('wordcount_cleanregex', /[0-9.(),;:!?%#$?\'\"_+=\\\/-]*/g); t.id = ed.id + '-word-count'; ed.onPostRender.add(function(ed, cm) { @@ -32,11 +32,12 @@ if (row) tinymce.DOM.add(row.parentNode, 'div', {'style': 'float: right'}, ed.getLang('wordcount.words', 'Words: ') + '0'); - } else + } else { tinymce.DOM.add(id, 'span', {}, '0'); + } }); - ed.onInit.add(function(ed) { + ed.onInit.add(function(ed) { ed.selection.onSetContent.add(function() { t._count(ed); }); @@ -59,8 +60,29 @@ }); }, + _getCount : function(ed) { + var tc = 0; + var tx = ed.getContent({ format: 'raw' }); + + if (tx) { + tx = tx.replace(/\.\.\./g, ' '); // convert ellipses to spaces + tx = tx.replace(/<.[^<>]*?>/g, ' ').replace(/ | /gi, ' '); // remove html tags and space chars + + // deal with html entities + tx = tx.replace(/(\w+)(&.+?;)+(\w+)/, "$1$3").replace(/&.+?;/g, ' '); + tx = tx.replace(this.cleanre, ''); // remove numbers and punctuation + + var wordArray = tx.match(this.countre); + if (wordArray) { + tc = wordArray.length; + } + } + + return tc; + }, + _count : function(ed) { - var t = this, tc = 0; + var t = this; // Keep multiple calls from happening at the same time if (t.block) @@ -69,21 +91,15 @@ t.block = 1; setTimeout(function() { - var tx = ed.getContent({format : 'raw'}); - - if (tx) { - tx = tx.replace(/<.[^<>]*?>/g, ' ').replace(/ | /gi, ' '); // remove html tags and space chars - tx = tx.replace(t.cleanre, ''); // remove numbers and punctuation - tx.replace(t.countre, function() {tc++;}); // count the words + if (!ed.destroyed) { + var tc = t._getCount(ed); + tinymce.DOM.setHTML(t.id, tc.toString()); + setTimeout(function() {t.block = 0;}, 2000); } - - tinymce.DOM.setHTML(t.id, tc.toString()); - - setTimeout(function() {t.block = 0;}, 2000); }, 1); }, - getInfo: function() { + getInfo: function() { return { longname : 'Word Count plugin', author : 'Moxiecode Systems AB', @@ -91,8 +107,8 @@ infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/wordcount', version : tinymce.majorVersion + "." + tinymce.minorVersion }; - } - }); + } + }); - tinymce.PluginManager.add('wordcount', tinymce.plugins.WordCount); + tinymce.PluginManager.add('wordcount', tinymce.plugins.WordCount); })(); diff --git a/js/tiny_mce/plugins/xhtmlxtras/abbr.htm b/js/tiny_mce/plugins/xhtmlxtras/abbr.htm index 3aeac0deba..30a894f7c3 100644 --- a/js/tiny_mce/plugins/xhtmlxtras/abbr.htm +++ b/js/tiny_mce/plugins/xhtmlxtras/abbr.htm @@ -10,11 +10,12 @@ - + + @@ -23,7 +24,7 @@
    {#xhtmlxtras_dlg.fieldset_attrib_tab} -
     
    +
    @@ -41,7 +42,7 @@ - + @@ -67,7 +68,7 @@
    {#xhtmlxtras_dlg.fieldset_events_tab} -
    :
    ::
    +
    diff --git a/js/tiny_mce/plugins/xhtmlxtras/acronym.htm b/js/tiny_mce/plugins/xhtmlxtras/acronym.htm index 31ee7b70f3..c109345928 100644 --- a/js/tiny_mce/plugins/xhtmlxtras/acronym.htm +++ b/js/tiny_mce/plugins/xhtmlxtras/acronym.htm @@ -10,11 +10,12 @@ - + + @@ -23,7 +24,7 @@
    {#xhtmlxtras_dlg.fieldset_attrib_tab} -
    :
    +
    @@ -41,7 +42,7 @@ - + @@ -67,7 +68,7 @@
    {#xhtmlxtras_dlg.fieldset_events_tab} -
    :
    ::
    +
    diff --git a/js/tiny_mce/plugins/xhtmlxtras/attributes.htm b/js/tiny_mce/plugins/xhtmlxtras/attributes.htm index 17054da3ed..e8d606a340 100644 --- a/js/tiny_mce/plugins/xhtmlxtras/attributes.htm +++ b/js/tiny_mce/plugins/xhtmlxtras/attributes.htm @@ -9,12 +9,13 @@ - + + @@ -22,7 +23,7 @@
    {#xhtmlxtras_dlg.attribute_attrib_tab} -
    :
    +
    @@ -75,7 +76,7 @@
    {#xhtmlxtras_dlg.attribute_events_tab} -
    :
    +
    diff --git a/js/tiny_mce/plugins/xhtmlxtras/cite.htm b/js/tiny_mce/plugins/xhtmlxtras/cite.htm index d0a3e3a8e5..0ac6bdb667 100644 --- a/js/tiny_mce/plugins/xhtmlxtras/cite.htm +++ b/js/tiny_mce/plugins/xhtmlxtras/cite.htm @@ -10,11 +10,12 @@ - + + @@ -23,7 +24,7 @@
    {#xhtmlxtras_dlg.fieldset_attrib_tab} -
    :
    +
    @@ -67,7 +68,7 @@
    {#xhtmlxtras_dlg.fieldset_events_tab} -
    :
    +
    diff --git a/js/tiny_mce/plugins/xhtmlxtras/del.htm b/js/tiny_mce/plugins/xhtmlxtras/del.htm index 8b07fa8429..5f667510f5 100644 --- a/js/tiny_mce/plugins/xhtmlxtras/del.htm +++ b/js/tiny_mce/plugins/xhtmlxtras/del.htm @@ -10,11 +10,12 @@ - + + @@ -23,14 +24,14 @@
    {#xhtmlxtras_dlg.fieldset_general_tab} -
    :
    +
    @@ -43,7 +44,7 @@
    {#xhtmlxtras_dlg.fieldset_attrib_tab} -
    : - +
    - +
    +
    @@ -61,7 +62,7 @@ - + @@ -87,7 +88,7 @@
    {#xhtmlxtras_dlg.fieldset_events_tab} -
    :
    ::
    +
    diff --git a/js/tiny_mce/plugins/xhtmlxtras/editor_plugin.js b/js/tiny_mce/plugins/xhtmlxtras/editor_plugin.js index e5195265e0..9b98a5154b 100644 --- a/js/tiny_mce/plugins/xhtmlxtras/editor_plugin.js +++ b/js/tiny_mce/plugins/xhtmlxtras/editor_plugin.js @@ -1 +1 @@ -(function(){tinymce.create("tinymce.plugins.XHTMLXtrasPlugin",{init:function(b,c){b.addCommand("mceCite",function(){b.windowManager.open({file:c+"/cite.htm",width:350+parseInt(b.getLang("xhtmlxtras.cite_delta_width",0)),height:250+parseInt(b.getLang("xhtmlxtras.cite_delta_height",0)),inline:1},{plugin_url:c})});b.addCommand("mceAcronym",function(){b.windowManager.open({file:c+"/acronym.htm",width:350+parseInt(b.getLang("xhtmlxtras.acronym_delta_width",0)),height:250+parseInt(b.getLang("xhtmlxtras.acronym_delta_width",0)),inline:1},{plugin_url:c})});b.addCommand("mceAbbr",function(){b.windowManager.open({file:c+"/abbr.htm",width:350+parseInt(b.getLang("xhtmlxtras.abbr_delta_width",0)),height:250+parseInt(b.getLang("xhtmlxtras.abbr_delta_width",0)),inline:1},{plugin_url:c})});b.addCommand("mceDel",function(){b.windowManager.open({file:c+"/del.htm",width:340+parseInt(b.getLang("xhtmlxtras.del_delta_width",0)),height:310+parseInt(b.getLang("xhtmlxtras.del_delta_width",0)),inline:1},{plugin_url:c})});b.addCommand("mceIns",function(){b.windowManager.open({file:c+"/ins.htm",width:340+parseInt(b.getLang("xhtmlxtras.ins_delta_width",0)),height:310+parseInt(b.getLang("xhtmlxtras.ins_delta_width",0)),inline:1},{plugin_url:c})});b.addCommand("mceAttributes",function(){b.windowManager.open({file:c+"/attributes.htm",width:380,height:370,inline:1},{plugin_url:c})});b.addButton("cite",{title:"xhtmlxtras.cite_desc",cmd:"mceCite"});b.addButton("acronym",{title:"xhtmlxtras.acronym_desc",cmd:"mceAcronym"});b.addButton("abbr",{title:"xhtmlxtras.abbr_desc",cmd:"mceAbbr"});b.addButton("del",{title:"xhtmlxtras.del_desc",cmd:"mceDel"});b.addButton("ins",{title:"xhtmlxtras.ins_desc",cmd:"mceIns"});b.addButton("attribs",{title:"xhtmlxtras.attribs_desc",cmd:"mceAttributes"});if(tinymce.isIE){function a(d,e){if(e.set){e.content=e.content.replace(/]+)>/gi,"");e.content=e.content.replace(/<\/abbr>/gi,"")}}b.onBeforeSetContent.add(a);b.onPostProcess.add(a)}b.onNodeChange.add(function(e,d,g,f){g=e.dom.getParent(g,"CITE,ACRONYM,ABBR,DEL,INS");d.setDisabled("cite",f);d.setDisabled("acronym",f);d.setDisabled("abbr",f);d.setDisabled("del",f);d.setDisabled("ins",f);d.setDisabled("attribs",g&&g.nodeName=="BODY");d.setActive("cite",0);d.setActive("acronym",0);d.setActive("abbr",0);d.setActive("del",0);d.setActive("ins",0);if(g){do{d.setDisabled(g.nodeName.toLowerCase(),0);d.setActive(g.nodeName.toLowerCase(),1)}while(g=g.parentNode)}});b.onPreInit.add(function(){b.dom.create("abbr")})},getInfo:function(){return{longname:"XHTML Xtras Plugin",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",infourl:"http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/xhtmlxtras",version:tinymce.majorVersion+"."+tinymce.minorVersion}}});tinymce.PluginManager.add("xhtmlxtras",tinymce.plugins.XHTMLXtrasPlugin)})(); \ No newline at end of file +(function(){tinymce.create("tinymce.plugins.XHTMLXtrasPlugin",{init:function(a,b){a.addCommand("mceCite",function(){a.windowManager.open({file:b+"/cite.htm",width:350+parseInt(a.getLang("xhtmlxtras.cite_delta_width",0)),height:250+parseInt(a.getLang("xhtmlxtras.cite_delta_height",0)),inline:1},{plugin_url:b})});a.addCommand("mceAcronym",function(){a.windowManager.open({file:b+"/acronym.htm",width:350+parseInt(a.getLang("xhtmlxtras.acronym_delta_width",0)),height:250+parseInt(a.getLang("xhtmlxtras.acronym_delta_height",0)),inline:1},{plugin_url:b})});a.addCommand("mceAbbr",function(){a.windowManager.open({file:b+"/abbr.htm",width:350+parseInt(a.getLang("xhtmlxtras.abbr_delta_width",0)),height:250+parseInt(a.getLang("xhtmlxtras.abbr_delta_height",0)),inline:1},{plugin_url:b})});a.addCommand("mceDel",function(){a.windowManager.open({file:b+"/del.htm",width:340+parseInt(a.getLang("xhtmlxtras.del_delta_width",0)),height:310+parseInt(a.getLang("xhtmlxtras.del_delta_height",0)),inline:1},{plugin_url:b})});a.addCommand("mceIns",function(){a.windowManager.open({file:b+"/ins.htm",width:340+parseInt(a.getLang("xhtmlxtras.ins_delta_width",0)),height:310+parseInt(a.getLang("xhtmlxtras.ins_delta_height",0)),inline:1},{plugin_url:b})});a.addCommand("mceAttributes",function(){a.windowManager.open({file:b+"/attributes.htm",width:380+parseInt(a.getLang("xhtmlxtras.attr_delta_width",0)),height:370+parseInt(a.getLang("xhtmlxtras.attr_delta_height",0)),inline:1},{plugin_url:b})});a.addButton("cite",{title:"xhtmlxtras.cite_desc",cmd:"mceCite"});a.addButton("acronym",{title:"xhtmlxtras.acronym_desc",cmd:"mceAcronym"});a.addButton("abbr",{title:"xhtmlxtras.abbr_desc",cmd:"mceAbbr"});a.addButton("del",{title:"xhtmlxtras.del_desc",cmd:"mceDel"});a.addButton("ins",{title:"xhtmlxtras.ins_desc",cmd:"mceIns"});a.addButton("attribs",{title:"xhtmlxtras.attribs_desc",cmd:"mceAttributes"});a.onNodeChange.add(function(d,c,f,e){f=d.dom.getParent(f,"CITE,ACRONYM,ABBR,DEL,INS");c.setDisabled("cite",e);c.setDisabled("acronym",e);c.setDisabled("abbr",e);c.setDisabled("del",e);c.setDisabled("ins",e);c.setDisabled("attribs",f&&f.nodeName=="BODY");c.setActive("cite",0);c.setActive("acronym",0);c.setActive("abbr",0);c.setActive("del",0);c.setActive("ins",0);if(f){do{c.setDisabled(f.nodeName.toLowerCase(),0);c.setActive(f.nodeName.toLowerCase(),1)}while(f=f.parentNode)}});a.onPreInit.add(function(){a.dom.create("abbr")})},getInfo:function(){return{longname:"XHTML Xtras Plugin",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",infourl:"http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/xhtmlxtras",version:tinymce.majorVersion+"."+tinymce.minorVersion}}});tinymce.PluginManager.add("xhtmlxtras",tinymce.plugins.XHTMLXtrasPlugin)})(); \ No newline at end of file diff --git a/js/tiny_mce/plugins/xhtmlxtras/editor_plugin_src.js b/js/tiny_mce/plugins/xhtmlxtras/editor_plugin_src.js index 9b51b8368d..f24057211c 100644 --- a/js/tiny_mce/plugins/xhtmlxtras/editor_plugin_src.js +++ b/js/tiny_mce/plugins/xhtmlxtras/editor_plugin_src.js @@ -27,7 +27,7 @@ ed.windowManager.open({ file : url + '/acronym.htm', width : 350 + parseInt(ed.getLang('xhtmlxtras.acronym_delta_width', 0)), - height : 250 + parseInt(ed.getLang('xhtmlxtras.acronym_delta_width', 0)), + height : 250 + parseInt(ed.getLang('xhtmlxtras.acronym_delta_height', 0)), inline : 1 }, { plugin_url : url @@ -38,7 +38,7 @@ ed.windowManager.open({ file : url + '/abbr.htm', width : 350 + parseInt(ed.getLang('xhtmlxtras.abbr_delta_width', 0)), - height : 250 + parseInt(ed.getLang('xhtmlxtras.abbr_delta_width', 0)), + height : 250 + parseInt(ed.getLang('xhtmlxtras.abbr_delta_height', 0)), inline : 1 }, { plugin_url : url @@ -49,7 +49,7 @@ ed.windowManager.open({ file : url + '/del.htm', width : 340 + parseInt(ed.getLang('xhtmlxtras.del_delta_width', 0)), - height : 310 + parseInt(ed.getLang('xhtmlxtras.del_delta_width', 0)), + height : 310 + parseInt(ed.getLang('xhtmlxtras.del_delta_height', 0)), inline : 1 }, { plugin_url : url @@ -60,7 +60,7 @@ ed.windowManager.open({ file : url + '/ins.htm', width : 340 + parseInt(ed.getLang('xhtmlxtras.ins_delta_width', 0)), - height : 310 + parseInt(ed.getLang('xhtmlxtras.ins_delta_width', 0)), + height : 310 + parseInt(ed.getLang('xhtmlxtras.ins_delta_height', 0)), inline : 1 }, { plugin_url : url @@ -70,8 +70,8 @@ ed.addCommand('mceAttributes', function() { ed.windowManager.open({ file : url + '/attributes.htm', - width : 380, - height : 370, + width : 380 + parseInt(ed.getLang('xhtmlxtras.attr_delta_width', 0)), + height : 370 + parseInt(ed.getLang('xhtmlxtras.attr_delta_height', 0)), inline : 1 }, { plugin_url : url @@ -86,18 +86,6 @@ ed.addButton('ins', {title : 'xhtmlxtras.ins_desc', cmd : 'mceIns'}); ed.addButton('attribs', {title : 'xhtmlxtras.attribs_desc', cmd : 'mceAttributes'}); - if (tinymce.isIE) { - function fix(ed, o) { - if (o.set) { - o.content = o.content.replace(/]+)>/gi, ''); - o.content = o.content.replace(/<\/abbr>/gi, ''); - } - }; - - ed.onBeforeSetContent.add(fix); - ed.onPostProcess.add(fix); - } - ed.onNodeChange.add(function(ed, cm, n, co) { n = ed.dom.getParent(n, 'CITE,ACRONYM,ABBR,DEL,INS'); diff --git a/js/tiny_mce/plugins/xhtmlxtras/ins.htm b/js/tiny_mce/plugins/xhtmlxtras/ins.htm index 6c5470cfcc..d001ac7c4d 100644 --- a/js/tiny_mce/plugins/xhtmlxtras/ins.htm +++ b/js/tiny_mce/plugins/xhtmlxtras/ins.htm @@ -10,11 +10,12 @@ - + + @@ -23,19 +24,19 @@
    {#xhtmlxtras_dlg.fieldset_general_tab} -
    :
    +
    - + @@ -43,9 +44,9 @@
    {#xhtmlxtras_dlg.fieldset_attrib_tab} -
    : - +
    - +
    :
    +
    - + @@ -61,7 +62,7 @@ - + @@ -87,7 +88,7 @@
    {#xhtmlxtras_dlg.fieldset_events_tab} -
    ::
    ::
    +
    diff --git a/js/tiny_mce/plugins/xhtmlxtras/js/attributes.js b/js/tiny_mce/plugins/xhtmlxtras/js/attributes.js index d62a219e6b..9c99995adb 100644 --- a/js/tiny_mce/plugins/xhtmlxtras/js/attributes.js +++ b/js/tiny_mce/plugins/xhtmlxtras/js/attributes.js @@ -53,7 +53,6 @@ function insertAction() { var inst = tinyMCEPopup.editor; var elm = inst.selection.getNode(); - tinyMCEPopup.execCommand("mceBeginUndoLevel"); setAllAttribs(elm); tinyMCEPopup.execCommand("mceEndUndoLevel"); tinyMCEPopup.close(); @@ -72,21 +71,7 @@ function setAttrib(elm, attrib, value) { value = valueElm.value; } - if (value != "") { - dom.setAttrib(elm, attrib.toLowerCase(), value); - - if (attrib == "style") - attrib = "style.cssText"; - - if (attrib.substring(0, 2) == 'on') - value = 'return true;' + value; - - if (attrib == "class") - attrib = "className"; - - elm[attrib]=value; - } else - elm.removeAttribute(attrib); + dom.setAttrib(elm, attrib.toLowerCase(), value); } function setAllAttribs(elm) { diff --git a/js/tiny_mce/plugins/xhtmlxtras/js/del.js b/js/tiny_mce/plugins/xhtmlxtras/js/del.js index 9e5d8c5717..1f957dc786 100644 --- a/js/tiny_mce/plugins/xhtmlxtras/js/del.js +++ b/js/tiny_mce/plugins/xhtmlxtras/js/del.js @@ -21,17 +21,17 @@ function setElementAttribs(elm) { setAllCommonAttribs(elm); setAttrib(elm, 'datetime'); setAttrib(elm, 'cite'); + elm.removeAttribute('data-mce-new'); } function insertDel() { var elm = tinyMCEPopup.editor.dom.getParent(SXE.focusElement, 'DEL'); - tinyMCEPopup.execCommand('mceBeginUndoLevel'); if (elm == null) { var s = SXE.inst.selection.getContent(); if(s.length > 0) { insertInlineElement('del'); - var elementArray = tinymce.grep(SXE.inst.dom.select('del'), function(n) {return n.id == '#sxe_temp_del#';}); + var elementArray = SXE.inst.dom.select('del[data-mce-new]'); for (var i=0; i 0) { @@ -165,11 +164,11 @@ SXE.insertElement = function(element_name) { for (var i=0; i 0) { - insertInlineElement('INS'); - var elementArray = tinymce.grep(SXE.inst.dom.select('ins'), function(n) {return n.id == '#sxe_temp_ins#';}); + insertInlineElement('ins'); + var elementArray = SXE.inst.dom.select('ins[data-mce-new]'); for (var i=0; i
    :
    +
    - + - - + +
    {#advanced_dlg.anchor_title}{#advanced_dlg.anchor_title}
    {#advanced_dlg.anchor_name}:
    diff --git a/js/tiny_mce/themes/advanced/charmap.htm b/js/tiny_mce/themes/advanced/charmap.htm index 3991b8141b..d4b6bdfb7b 100644 --- a/js/tiny_mce/themes/advanced/charmap.htm +++ b/js/tiny_mce/themes/advanced/charmap.htm @@ -5,48 +5,51 @@ - - - - - - -
    {#advanced_dlg.charmap_title}
    + + + + + + + - - - - - + + + + + + + + + +
    - - - - - - - - -
     
     
    -
    - - - - - - - - - - - - - - - - -
    HTML-Code
     
     
    NUM-Code
     
    -
    + + + + + + + +
     
     
    +
    + + + + + + + + + + + + + + + + +
     
     
     
    +
    {#advanced_dlg.charmap_usage}
    - diff --git a/js/tiny_mce/themes/advanced/color_picker.htm b/js/tiny_mce/themes/advanced/color_picker.htm index 096e7550c3..ad1bb0f6cc 100644 --- a/js/tiny_mce/themes/advanced/color_picker.htm +++ b/js/tiny_mce/themes/advanced/color_picker.htm @@ -6,13 +6,14 @@ - + + @@ -34,7 +35,7 @@
    - {#advanced_dlg.colorpicker_palette_title} + {#advanced_dlg.colorpicker_palette_title}
    @@ -44,9 +45,9 @@
    -
    - {#advanced_dlg.colorpicker_named_title} -
    +
    + {#advanced_dlg.colorpicker_named_title} +
    @@ -65,7 +66,7 @@
    - +
    diff --git a/js/tiny_mce/themes/advanced/editor_template.js b/js/tiny_mce/themes/advanced/editor_template.js index dc61977461..812578d0bc 100644 --- a/js/tiny_mce/themes/advanced/editor_template.js +++ b/js/tiny_mce/themes/advanced/editor_template.js @@ -1 +1 @@ -(function(e){var d=e.DOM,b=e.dom.Event,h=e.extend,f=e.each,a=e.util.Cookie,g,c=e.explode;e.ThemeManager.requireLangPack("advanced");e.create("tinymce.themes.AdvancedTheme",{sizes:[8,10,12,14,18,24,36],controls:{bold:["bold_desc","Bold"],italic:["italic_desc","Italic"],underline:["underline_desc","Underline"],strikethrough:["striketrough_desc","Strikethrough"],justifyleft:["justifyleft_desc","JustifyLeft"],justifycenter:["justifycenter_desc","JustifyCenter"],justifyright:["justifyright_desc","JustifyRight"],justifyfull:["justifyfull_desc","JustifyFull"],bullist:["bullist_desc","InsertUnorderedList"],numlist:["numlist_desc","InsertOrderedList"],outdent:["outdent_desc","Outdent"],indent:["indent_desc","Indent"],cut:["cut_desc","Cut"],copy:["copy_desc","Copy"],paste:["paste_desc","Paste"],undo:["undo_desc","Undo"],redo:["redo_desc","Redo"],link:["link_desc","mceLink"],unlink:["unlink_desc","unlink"],image:["image_desc","mceImage"],cleanup:["cleanup_desc","mceCleanup"],help:["help_desc","mceHelp"],code:["code_desc","mceCodeEditor"],hr:["hr_desc","InsertHorizontalRule"],removeformat:["removeformat_desc","RemoveFormat"],sub:["sub_desc","subscript"],sup:["sup_desc","superscript"],forecolor:["forecolor_desc","ForeColor"],forecolorpicker:["forecolor_desc","mceForeColor"],backcolor:["backcolor_desc","HiliteColor"],backcolorpicker:["backcolor_desc","mceBackColor"],charmap:["charmap_desc","mceCharMap"],visualaid:["visualaid_desc","mceToggleVisualAid"],anchor:["anchor_desc","mceInsertAnchor"],newdocument:["newdocument_desc","mceNewDocument"],blockquote:["blockquote_desc","mceBlockQuote"]},stateControls:["bold","italic","underline","strikethrough","bullist","numlist","justifyleft","justifycenter","justifyright","justifyfull","sub","sup","blockquote"],init:function(j,k){var l=this,m,i,n;l.editor=j;l.url=k;l.onResolveName=new e.util.Dispatcher(this);l.settings=m=h({theme_advanced_path:true,theme_advanced_toolbar_location:"bottom",theme_advanced_buttons1:"bold,italic,underline,strikethrough,|,justifyleft,justifycenter,justifyright,justifyfull,|,styleselect,formatselect",theme_advanced_buttons2:"bullist,numlist,|,outdent,indent,|,undo,redo,|,link,unlink,anchor,image,cleanup,help,code",theme_advanced_buttons3:"hr,removeformat,visualaid,|,sub,sup,|,charmap",theme_advanced_blockformats:"p,address,pre,h1,h2,h3,h4,h5,h6",theme_advanced_toolbar_align:"center",theme_advanced_fonts:"Andale Mono=andale mono,times;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier;Georgia=georgia,palatino;Helvetica=helvetica;Impact=impact,chicago;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco;Times New Roman=times new roman,times;Trebuchet MS=trebuchet ms,geneva;Verdana=verdana,geneva;Webdings=webdings;Wingdings=wingdings,zapf dingbats",theme_advanced_more_colors:1,theme_advanced_row_height:23,theme_advanced_resize_horizontal:1,theme_advanced_resizing_use_cookie:1,theme_advanced_font_sizes:"1,2,3,4,5,6,7",readonly:j.settings.readonly},j.settings);if(!m.font_size_style_values){m.font_size_style_values="8pt,10pt,12pt,14pt,18pt,24pt,36pt"}if(e.is(m.theme_advanced_font_sizes,"string")){m.font_size_style_values=e.explode(m.font_size_style_values);m.font_size_classes=e.explode(m.font_size_classes||"");n={};j.settings.theme_advanced_font_sizes=m.theme_advanced_font_sizes;f(j.getParam("theme_advanced_font_sizes","","hash"),function(q,p){var o;if(p==q&&q>=1&&q<=7){p=q+" ("+l.sizes[q-1]+"pt)";o=m.font_size_classes[q-1];q=m.font_size_style_values[q-1]||(l.sizes[q-1]+"pt")}if(/^\s*\./.test(q)){o=q.replace(/\./g,"")}n[p]=o?{"class":o}:{fontSize:q}});m.theme_advanced_font_sizes=n}if((i=m.theme_advanced_path_location)&&i!="none"){m.theme_advanced_statusbar_location=m.theme_advanced_path_location}if(m.theme_advanced_statusbar_location=="none"){m.theme_advanced_statusbar_location=0}j.onInit.add(function(){if(!j.settings.readonly){j.onNodeChange.add(l._nodeChanged,l)}if(j.settings.content_css!==false){j.dom.loadCSS(j.baseURI.toAbsolute("themes/advanced/skins/"+j.settings.skin+"/content.css"))}});j.onSetProgressState.add(function(q,o,r){var s,t=q.id,p;if(o){l.progressTimer=setTimeout(function(){s=q.getContainer();s=s.insertBefore(d.create("DIV",{style:"position:relative"}),s.firstChild);p=d.get(q.id+"_tbl");d.add(s,"div",{id:t+"_blocker","class":"mceBlocker",style:{width:p.clientWidth+2,height:p.clientHeight+2}});d.add(s,"div",{id:t+"_progress","class":"mceProgress",style:{left:p.clientWidth/2,top:p.clientHeight/2}})},r||0)}else{d.remove(t+"_blocker");d.remove(t+"_progress");clearTimeout(l.progressTimer)}});d.loadCSS(m.editor_css?j.documentBaseURI.toAbsolute(m.editor_css):k+"/skins/"+j.settings.skin+"/ui.css");if(m.skin_variant){d.loadCSS(k+"/skins/"+j.settings.skin+"/ui_"+m.skin_variant+".css")}},createControl:function(l,i){var j,k;if(k=i.createControl(l)){return k}switch(l){case"styleselect":return this._createStyleSelect();case"formatselect":return this._createBlockFormats();case"fontselect":return this._createFontSelect();case"fontsizeselect":return this._createFontSizeSelect();case"forecolor":return this._createForeColorMenu();case"backcolor":return this._createBackColorMenu()}if((j=this.controls[l])){return i.createButton(l,{title:"advanced."+j[0],cmd:j[1],ui:j[2],value:j[3]})}},execCommand:function(k,j,l){var i=this["_"+k];if(i){i.call(this,j,l);return true}return false},_importClasses:function(k){var i=this.editor,j=i.controlManager.get("styleselect");if(j.getLength()==0){f(i.dom.getClasses(),function(n,l){var m="style_"+l;i.formatter.register(m,{inline:"span",attributes:{"class":n["class"]},selector:"*"});j.add(n["class"],m)})}},_createStyleSelect:function(m){var k=this,i=k.editor,j=i.controlManager,l;l=j.createListBox("styleselect",{title:"advanced.style_select",onselect:function(o){var p,n=[];f(l.items,function(q){n.push(q.value)});i.focus();i.undoManager.add();p=i.formatter.matchAll(n);if(p[0]==o){i.formatter.remove(o)}else{i.formatter.apply(o)}i.undoManager.add();i.nodeChanged();return false}});i.onInit.add(function(){var o=0,n=i.getParam("style_formats");if(n){f(n,function(p){var q,r=0;f(p,function(){r++});if(r>1){q=p.name=p.name||"style_"+(o++);i.formatter.register(q,p);l.add(p.title,q)}else{l.add(p.title)}})}else{f(i.getParam("theme_advanced_styles","","hash"),function(r,q){var p;if(r){p="style_"+(o++);i.formatter.register(p,{inline:"span",classes:r,selector:"*"});l.add(k.editor.translate(q),p)}})}});if(l.getLength()==0){l.onPostRender.add(function(o,p){if(!l.NativeListBox){b.add(p.id+"_text","focus",k._importClasses,k);b.add(p.id+"_text","mousedown",k._importClasses,k);b.add(p.id+"_open","focus",k._importClasses,k);b.add(p.id+"_open","mousedown",k._importClasses,k)}else{b.add(p.id,"focus",k._importClasses,k)}})}return l},_createFontSelect:function(){var k,j=this,i=j.editor;k=i.controlManager.createListBox("fontselect",{title:"advanced.fontdefault",onselect:function(l){i.execCommand("FontName",false,l);k.select(function(m){return l==m});return false}});if(k){f(i.getParam("theme_advanced_fonts",j.settings.theme_advanced_fonts,"hash"),function(m,l){k.add(i.translate(l),m,{style:m.indexOf("dings")==-1?"font-family:"+m:""})})}return k},_createFontSizeSelect:function(){var m=this,k=m.editor,n,l=0,j=[];n=k.controlManager.createListBox("fontsizeselect",{title:"advanced.font_size",onselect:function(i){if(i["class"]){k.focus();k.undoManager.add();k.formatter.toggle("fontsize_class",{value:i["class"]});k.undoManager.add();k.nodeChanged()}else{k.execCommand("FontSize",false,i.fontSize)}n.select(function(o){return i==o});return false}});if(n){f(m.settings.theme_advanced_font_sizes,function(o,i){var p=o.fontSize;if(p>=1&&p<=7){p=m.sizes[parseInt(p)-1]+"pt"}n.add(i,o,{style:"font-size:"+p,"class":"mceFontSize"+(l++)+(" "+(o["class"]||""))})})}return n},_createBlockFormats:function(){var k,i={p:"advanced.paragraph",address:"advanced.address",pre:"advanced.pre",h1:"advanced.h1",h2:"advanced.h2",h3:"advanced.h3",h4:"advanced.h4",h5:"advanced.h5",h6:"advanced.h6",div:"advanced.div",blockquote:"advanced.blockquote",code:"advanced.code",dt:"advanced.dt",dd:"advanced.dd",samp:"advanced.samp"},j=this;k=j.editor.controlManager.createListBox("formatselect",{title:"advanced.block",cmd:"FormatBlock"});if(k){f(j.editor.getParam("theme_advanced_blockformats",j.settings.theme_advanced_blockformats,"hash"),function(m,l){k.add(j.editor.translate(l!=m?l:i[m]),m,{"class":"mce_formatPreview mce_"+m})})}return k},_createForeColorMenu:function(){var m,j=this,k=j.settings,l={},i;if(k.theme_advanced_more_colors){l.more_colors_func=function(){j._mceColorPicker(0,{color:m.value,func:function(n){m.setColor(n)}})}}if(i=k.theme_advanced_text_colors){l.colors=i}if(k.theme_advanced_default_foreground_color){l.default_color=k.theme_advanced_default_foreground_color}l.title="advanced.forecolor_desc";l.cmd="ForeColor";l.scope=this;m=j.editor.controlManager.createColorSplitButton("forecolor",l);return m},_createBackColorMenu:function(){var m,j=this,k=j.settings,l={},i;if(k.theme_advanced_more_colors){l.more_colors_func=function(){j._mceColorPicker(0,{color:m.value,func:function(n){m.setColor(n)}})}}if(i=k.theme_advanced_background_colors){l.colors=i}if(k.theme_advanced_default_background_color){l.default_color=k.theme_advanced_default_background_color}l.title="advanced.backcolor_desc";l.cmd="HiliteColor";l.scope=this;m=j.editor.controlManager.createColorSplitButton("backcolor",l);return m},renderUI:function(k){var m,l,q,v=this,r=v.editor,w=v.settings,u,j,i;m=j=d.create("span",{id:r.id+"_parent","class":"mceEditor "+r.settings.skin+"Skin"+(w.skin_variant?" "+r.settings.skin+"Skin"+v._ufirst(w.skin_variant):"")});if(!d.boxModel){m=d.add(m,"div",{"class":"mceOldBoxModel"})}m=u=d.add(m,"table",{id:r.id+"_tbl","class":"mceLayout",cellSpacing:0,cellPadding:0});m=q=d.add(m,"tbody");switch((w.theme_advanced_layout_manager||"").toLowerCase()){case"rowlayout":l=v._rowLayout(w,q,k);break;case"customlayout":l=r.execCallback("theme_advanced_custom_layout",w,q,k,j);break;default:l=v._simpleLayout(w,q,k,j)}m=k.targetNode;i=d.stdMode?u.getElementsByTagName("tr"):u.rows;d.addClass(i[0],"mceFirst");d.addClass(i[i.length-1],"mceLast");f(d.select("tr",q),function(o){d.addClass(o.firstChild,"mceFirst");d.addClass(o.childNodes[o.childNodes.length-1],"mceLast")});if(d.get(w.theme_advanced_toolbar_container)){d.get(w.theme_advanced_toolbar_container).appendChild(j)}else{d.insertAfter(j,m)}b.add(r.id+"_path_row","click",function(n){n=n.target;if(n.nodeName=="A"){v._sel(n.className.replace(/^.*mcePath_([0-9]+).*$/,"$1"));return b.cancel(n)}});if(!r.getParam("accessibility_focus")){b.add(d.add(j,"a",{href:"#"},""),"focus",function(){tinyMCE.get(r.id).focus()})}if(w.theme_advanced_toolbar_location=="external"){k.deltaHeight=0}v.deltaHeight=k.deltaHeight;k.targetNode=null;return{iframeContainer:l,editorContainer:r.id+"_parent",sizeContainer:u,deltaHeight:k.deltaHeight}},getInfo:function(){return{longname:"Advanced theme",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",version:e.majorVersion+"."+e.minorVersion}},resizeBy:function(i,j){var k=d.get(this.editor.id+"_tbl");this.resizeTo(k.clientWidth+i,k.clientHeight+j)},resizeTo:function(i,l){var j=this.editor,k=this.settings,m=d.get(j.id+"_tbl"),n=d.get(j.id+"_ifr");i=Math.max(k.theme_advanced_resizing_min_width||100,i);l=Math.max(k.theme_advanced_resizing_min_height||100,l);i=Math.min(k.theme_advanced_resizing_max_width||65535,i);l=Math.min(k.theme_advanced_resizing_max_height||65535,l);d.setStyle(m,"height","");d.setStyle(n,"height",l);if(k.theme_advanced_resize_horizontal){d.setStyle(m,"width","");d.setStyle(n,"width",i);if(i"))}q.push(d.createHTML("a",{href:"#",accesskey:"q",title:r.getLang("advanced.toolbar_focus")},""));for(p=1;(y=A["theme_advanced_buttons"+p]);p++){m=j.createToolbar("toolbar"+p,{"class":"mceToolbarRow"+p});if(A["theme_advanced_buttons"+p+"_add"]){y+=","+A["theme_advanced_buttons"+p+"_add"]}if(A["theme_advanced_buttons"+p+"_add_before"]){y=A["theme_advanced_buttons"+p+"_add_before"]+","+y}z._addControls(y,m);q.push(m.renderHTML());k.deltaHeight-=A.theme_advanced_row_height}q.push(d.createHTML("a",{href:"#",accesskey:"z",title:r.getLang("advanced.toolbar_focus"),onfocus:"tinyMCE.getInstanceById('"+r.id+"').focus();"},""));d.setHTML(l,q.join(""))},_addStatusBar:function(m,j){var k,v=this,p=v.editor,w=v.settings,i,q,u,l;k=d.add(m,"tr");k=l=d.add(k,"td",{"class":"mceStatusbar"});k=d.add(k,"div",{id:p.id+"_path_row"},w.theme_advanced_path?p.translate("advanced.path")+": ":" ");d.add(k,"a",{href:"#",accesskey:"x"});if(w.theme_advanced_resizing){d.add(l,"a",{id:p.id+"_resize",href:"javascript:;",onclick:"return false;","class":"mceResize"});if(w.theme_advanced_resizing_use_cookie){p.onPostRender.add(function(){var n=a.getHash("TinyMCE_"+p.id+"_size"),r=d.get(p.id+"_tbl");if(!n){return}v.resizeTo(n.cw,n.ch)})}p.onPostRender.add(function(){b.add(p.id+"_resize","mousedown",function(D){var t,r,s,o,C,z,A,F,n,E,x;function y(G){n=A+(G.screenX-C);E=F+(G.screenY-z);v.resizeTo(n,E)}function B(G){b.remove(d.doc,"mousemove",t);b.remove(p.getDoc(),"mousemove",r);b.remove(d.doc,"mouseup",s);b.remove(p.getDoc(),"mouseup",o);if(w.theme_advanced_resizing_use_cookie){a.setHash("TinyMCE_"+p.id+"_size",{cw:n,ch:E})}}D.preventDefault();C=D.screenX;z=D.screenY;x=d.get(v.editor.id+"_ifr");A=n=x.clientWidth;F=E=x.clientHeight;t=b.add(d.doc,"mousemove",y);r=b.add(p.getDoc(),"mousemove",y);s=b.add(d.doc,"mouseup",B);o=b.add(p.getDoc(),"mouseup",B)})})}j.deltaHeight-=21;k=m=null},_nodeChanged:function(r,z,l,x,j){var C=this,i,y=0,B,u,D=C.settings,A,k,w,m,q;e.each(C.stateControls,function(n){z.setActive(n,r.queryCommandState(C.controls[n][1]))});function o(p){var s,n=j.parents,t=p;if(typeof(p)=="string"){t=function(v){return v.nodeName==p}}for(s=0;s=1&&q<=7){p=q+" ("+l.sizes[q-1]+"pt)";o=m.font_size_classes[q-1];q=m.font_size_style_values[q-1]||(l.sizes[q-1]+"pt")}if(/^\s*\./.test(q)){o=q.replace(/\./g,"")}n[p]=o?{"class":o}:{fontSize:q}});m.theme_advanced_font_sizes=n}if((i=m.theme_advanced_path_location)&&i!="none"){m.theme_advanced_statusbar_location=m.theme_advanced_path_location}if(m.theme_advanced_statusbar_location=="none"){m.theme_advanced_statusbar_location=0}if(j.settings.content_css!==false){j.contentCSS.push(j.baseURI.toAbsolute(k+"/skins/"+j.settings.skin+"/content.css"))}j.onInit.add(function(){if(!j.settings.readonly){j.onNodeChange.add(l._nodeChanged,l);j.onKeyUp.add(l._updateUndoStatus,l);j.onMouseUp.add(l._updateUndoStatus,l);j.dom.bind(j.dom.getRoot(),"dragend",function(){l._updateUndoStatus(j)})}});j.onSetProgressState.add(function(q,o,r){var s,t=q.id,p;if(o){l.progressTimer=setTimeout(function(){s=q.getContainer();s=s.insertBefore(d.create("DIV",{style:"position:relative"}),s.firstChild);p=d.get(q.id+"_tbl");d.add(s,"div",{id:t+"_blocker","class":"mceBlocker",style:{width:p.clientWidth+2,height:p.clientHeight+2}});d.add(s,"div",{id:t+"_progress","class":"mceProgress",style:{left:p.clientWidth/2,top:p.clientHeight/2}})},r||0)}else{d.remove(t+"_blocker");d.remove(t+"_progress");clearTimeout(l.progressTimer)}});d.loadCSS(m.editor_css?j.documentBaseURI.toAbsolute(m.editor_css):k+"/skins/"+j.settings.skin+"/ui.css");if(m.skin_variant){d.loadCSS(k+"/skins/"+j.settings.skin+"/ui_"+m.skin_variant+".css")}},_isHighContrast:function(){var i,j=d.add(d.getRoot(),"div",{style:"background-color: rgb(171,239,86);"});i=(d.getStyle(j,"background-color",true)+"").toLowerCase().replace(/ /g,"");d.remove(j);return i!="rgb(171,239,86)"&&i!="#abef56"},createControl:function(l,i){var j,k;if(k=i.createControl(l)){return k}switch(l){case"styleselect":return this._createStyleSelect();case"formatselect":return this._createBlockFormats();case"fontselect":return this._createFontSelect();case"fontsizeselect":return this._createFontSizeSelect();case"forecolor":return this._createForeColorMenu();case"backcolor":return this._createBackColorMenu()}if((j=this.controls[l])){return i.createButton(l,{title:"advanced."+j[0],cmd:j[1],ui:j[2],value:j[3]})}},execCommand:function(k,j,l){var i=this["_"+k];if(i){i.call(this,j,l);return true}return false},_importClasses:function(k){var i=this.editor,j=i.controlManager.get("styleselect");if(j.getLength()==0){f(i.dom.getClasses(),function(n,l){var m="style_"+l;i.formatter.register(m,{inline:"span",attributes:{"class":n["class"]},selector:"*"});j.add(n["class"],m)})}},_createStyleSelect:function(m){var k=this,i=k.editor,j=i.controlManager,l;l=j.createListBox("styleselect",{title:"advanced.style_select",onselect:function(o){var p,n=[];f(l.items,function(q){n.push(q.value)});i.focus();i.undoManager.add();p=i.formatter.matchAll(n);if(!o||p[0]==o){if(p[0]){i.formatter.remove(p[0])}}else{i.formatter.apply(o)}i.undoManager.add();i.nodeChanged();return false}});i.onInit.add(function(){var o=0,n=i.getParam("style_formats");if(n){f(n,function(p){var q,r=0;f(p,function(){r++});if(r>1){q=p.name=p.name||"style_"+(o++);i.formatter.register(q,p);l.add(p.title,q)}else{l.add(p.title)}})}else{f(i.getParam("theme_advanced_styles","","hash"),function(r,q){var p;if(r){p="style_"+(o++);i.formatter.register(p,{inline:"span",classes:r,selector:"*"});l.add(k.editor.translate(q),p)}})}});if(l.getLength()==0){l.onPostRender.add(function(o,p){if(!l.NativeListBox){b.add(p.id+"_text","focus",k._importClasses,k);b.add(p.id+"_text","mousedown",k._importClasses,k);b.add(p.id+"_open","focus",k._importClasses,k);b.add(p.id+"_open","mousedown",k._importClasses,k)}else{b.add(p.id,"focus",k._importClasses,k)}})}return l},_createFontSelect:function(){var k,j=this,i=j.editor;k=i.controlManager.createListBox("fontselect",{title:"advanced.fontdefault",onselect:function(l){var m=k.items[k.selectedIndex];if(!l&&m){i.execCommand("FontName",false,m.value);return}i.execCommand("FontName",false,l);k.select(function(n){return l==n});if(m&&m.value==l){k.select(null)}return false}});if(k){f(i.getParam("theme_advanced_fonts",j.settings.theme_advanced_fonts,"hash"),function(m,l){k.add(i.translate(l),m,{style:m.indexOf("dings")==-1?"font-family:"+m:""})})}return k},_createFontSizeSelect:function(){var m=this,k=m.editor,n,l=0,j=[];n=k.controlManager.createListBox("fontsizeselect",{title:"advanced.font_size",onselect:function(i){var o=n.items[n.selectedIndex];if(!i&&o){o=o.value;if(o["class"]){k.formatter.toggle("fontsize_class",{value:o["class"]});k.undoManager.add();k.nodeChanged()}else{k.execCommand("FontSize",false,o.fontSize)}return}if(i["class"]){k.focus();k.undoManager.add();k.formatter.toggle("fontsize_class",{value:i["class"]});k.undoManager.add();k.nodeChanged()}else{k.execCommand("FontSize",false,i.fontSize)}n.select(function(p){return i==p});if(o&&(o.value.fontSize==i.fontSize||o.value["class"]==i["class"])){n.select(null)}return false}});if(n){f(m.settings.theme_advanced_font_sizes,function(o,i){var p=o.fontSize;if(p>=1&&p<=7){p=m.sizes[parseInt(p)-1]+"pt"}n.add(i,o,{style:"font-size:"+p,"class":"mceFontSize"+(l++)+(" "+(o["class"]||""))})})}return n},_createBlockFormats:function(){var k,i={p:"advanced.paragraph",address:"advanced.address",pre:"advanced.pre",h1:"advanced.h1",h2:"advanced.h2",h3:"advanced.h3",h4:"advanced.h4",h5:"advanced.h5",h6:"advanced.h6",div:"advanced.div",blockquote:"advanced.blockquote",code:"advanced.code",dt:"advanced.dt",dd:"advanced.dd",samp:"advanced.samp"},j=this;k=j.editor.controlManager.createListBox("formatselect",{title:"advanced.block",onselect:function(l){j.editor.execCommand("FormatBlock",false,l);return false}});if(k){f(j.editor.getParam("theme_advanced_blockformats",j.settings.theme_advanced_blockformats,"hash"),function(m,l){k.add(j.editor.translate(l!=m?l:i[m]),m,{"class":"mce_formatPreview mce_"+m})})}return k},_createForeColorMenu:function(){var m,j=this,k=j.settings,l={},i;if(k.theme_advanced_more_colors){l.more_colors_func=function(){j._mceColorPicker(0,{color:m.value,func:function(n){m.setColor(n)}})}}if(i=k.theme_advanced_text_colors){l.colors=i}if(k.theme_advanced_default_foreground_color){l.default_color=k.theme_advanced_default_foreground_color}l.title="advanced.forecolor_desc";l.cmd="ForeColor";l.scope=this;m=j.editor.controlManager.createColorSplitButton("forecolor",l);return m},_createBackColorMenu:function(){var m,j=this,k=j.settings,l={},i;if(k.theme_advanced_more_colors){l.more_colors_func=function(){j._mceColorPicker(0,{color:m.value,func:function(n){m.setColor(n)}})}}if(i=k.theme_advanced_background_colors){l.colors=i}if(k.theme_advanced_default_background_color){l.default_color=k.theme_advanced_default_background_color}l.title="advanced.backcolor_desc";l.cmd="HiliteColor";l.scope=this;m=j.editor.controlManager.createColorSplitButton("backcolor",l);return m},renderUI:function(k){var m,l,q,v=this,r=v.editor,w=v.settings,u,j,i;if(r.settings){r.settings.aria_label=w.aria_label+r.getLang("advanced.help_shortcut")}m=j=d.create("span",{role:"application","aria-labelledby":r.id+"_voice",id:r.id+"_parent","class":"mceEditor "+r.settings.skin+"Skin"+(w.skin_variant?" "+r.settings.skin+"Skin"+v._ufirst(w.skin_variant):"")});d.add(m,"span",{"class":"mceVoiceLabel",style:"display:none;",id:r.id+"_voice"},w.aria_label);if(!d.boxModel){m=d.add(m,"div",{"class":"mceOldBoxModel"})}m=u=d.add(m,"table",{role:"presentation",id:r.id+"_tbl","class":"mceLayout",cellSpacing:0,cellPadding:0});m=q=d.add(m,"tbody");switch((w.theme_advanced_layout_manager||"").toLowerCase()){case"rowlayout":l=v._rowLayout(w,q,k);break;case"customlayout":l=r.execCallback("theme_advanced_custom_layout",w,q,k,j);break;default:l=v._simpleLayout(w,q,k,j)}m=k.targetNode;i=u.rows;d.addClass(i[0],"mceFirst");d.addClass(i[i.length-1],"mceLast");f(d.select("tr",q),function(o){d.addClass(o.firstChild,"mceFirst");d.addClass(o.childNodes[o.childNodes.length-1],"mceLast")});if(d.get(w.theme_advanced_toolbar_container)){d.get(w.theme_advanced_toolbar_container).appendChild(j)}else{d.insertAfter(j,m)}b.add(r.id+"_path_row","click",function(n){n=n.target;if(n.nodeName=="A"){v._sel(n.className.replace(/^.*mcePath_([0-9]+).*$/,"$1"));return b.cancel(n)}});if(!r.getParam("accessibility_focus")){b.add(d.add(j,"a",{href:"#"},""),"focus",function(){tinyMCE.get(r.id).focus()})}if(w.theme_advanced_toolbar_location=="external"){k.deltaHeight=0}v.deltaHeight=k.deltaHeight;k.targetNode=null;r.onKeyDown.add(function(p,n){var s=121,o=122;if(n.altKey){if(n.keyCode===s){if(e.isWebKit){window.focus()}v.toolbarGroup.focus();return b.cancel(n)}else{if(n.keyCode===o){d.get(p.id+"_path_row").focus();return b.cancel(n)}}}});r.addShortcut("alt+0","","mceShortcuts",v);return{iframeContainer:l,editorContainer:r.id+"_parent",sizeContainer:u,deltaHeight:k.deltaHeight}},getInfo:function(){return{longname:"Advanced theme",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",version:e.majorVersion+"."+e.minorVersion}},resizeBy:function(i,j){var k=d.get(this.editor.id+"_ifr");this.resizeTo(k.clientWidth+i,k.clientHeight+j)},resizeTo:function(i,m,k){var j=this.editor,l=this.settings,n=d.get(j.id+"_tbl"),o=d.get(j.id+"_ifr");i=Math.max(l.theme_advanced_resizing_min_width||100,i);m=Math.max(l.theme_advanced_resizing_min_height||100,m);i=Math.min(l.theme_advanced_resizing_max_width||65535,i);m=Math.min(l.theme_advanced_resizing_max_height||65535,m);d.setStyle(n,"height","");d.setStyle(o,"height",m);if(l.theme_advanced_resize_horizontal){d.setStyle(n,"width","");d.setStyle(o,"width",i);if(i"));d.setHTML(l,q.join(""))},_addStatusBar:function(m,j){var k,v=this,p=v.editor,w=v.settings,i,q,u,l;k=d.add(m,"tr");k=l=d.add(k,"td",{"class":"mceStatusbar"});k=d.add(k,"div",{id:p.id+"_path_row",role:"group","aria-labelledby":p.id+"_path_voice"});if(w.theme_advanced_path){d.add(k,"span",{id:p.id+"_path_voice"},p.translate("advanced.path"));d.add(k,"span",{},": ")}else{d.add(k,"span",{}," ")}if(w.theme_advanced_resizing){d.add(l,"a",{id:p.id+"_resize",href:"javascript:;",onclick:"return false;","class":"mceResize",tabIndex:"-1"});if(w.theme_advanced_resizing_use_cookie){p.onPostRender.add(function(){var n=a.getHash("TinyMCE_"+p.id+"_size"),r=d.get(p.id+"_tbl");if(!n){return}v.resizeTo(n.cw,n.ch)})}p.onPostRender.add(function(){b.add(p.id+"_resize","click",function(n){n.preventDefault()});b.add(p.id+"_resize","mousedown",function(D){var t,r,s,o,C,z,A,F,n,E,x;function y(G){G.preventDefault();n=A+(G.screenX-C);E=F+(G.screenY-z);v.resizeTo(n,E)}function B(G){b.remove(d.doc,"mousemove",t);b.remove(p.getDoc(),"mousemove",r);b.remove(d.doc,"mouseup",s);b.remove(p.getDoc(),"mouseup",o);n=A+(G.screenX-C);E=F+(G.screenY-z);v.resizeTo(n,E,true)}D.preventDefault();C=D.screenX;z=D.screenY;x=d.get(v.editor.id+"_ifr");A=n=x.clientWidth;F=E=x.clientHeight;t=b.add(d.doc,"mousemove",y);r=b.add(p.getDoc(),"mousemove",y);s=b.add(d.doc,"mouseup",B);o=b.add(p.getDoc(),"mouseup",B)})})}j.deltaHeight-=21;k=m=null},_updateUndoStatus:function(j){var i=j.controlManager,k=j.undoManager;i.setDisabled("undo",!k.hasUndo()&&!k.typing);i.setDisabled("redo",!k.hasRedo())},_nodeChanged:function(m,r,D,q,E){var y=this,C,F=0,x,G,z=y.settings,w,k,u,B,l,j,i;e.each(y.stateControls,function(n){r.setActive(n,m.queryCommandState(y.controls[n][1]))});function o(p){var s,n=E.parents,t=p;if(typeof(p)=="string"){t=function(v){return v.nodeName==p}}for(s=0;s0){y.statusKeyboardNavigation=new e.ui.KeyboardNavigation({root:m.id+"_path_row",items:d.select("a",C),excludeFromTabOrder:true,onCancel:function(){m.focus()}},d)}}},_sel:function(i){this.editor.execCommand("mceSelectNodeDepth",false,i)},_mceInsertAnchor:function(k,j){var i=this.editor;i.windowManager.open({url:this.url+"/anchor.htm",width:320+parseInt(i.getLang("advanced.anchor_delta_width",0)),height:90+parseInt(i.getLang("advanced.anchor_delta_height",0)),inline:true},{theme_url:this.url})},_mceCharMap:function(){var i=this.editor;i.windowManager.open({url:this.url+"/charmap.htm",width:550+parseInt(i.getLang("advanced.charmap_delta_width",0)),height:260+parseInt(i.getLang("advanced.charmap_delta_height",0)),inline:true},{theme_url:this.url})},_mceHelp:function(){var i=this.editor;i.windowManager.open({url:this.url+"/about.htm",width:480,height:380,inline:true},{theme_url:this.url})},_mceShortcuts:function(){var i=this.editor;i.windowManager.open({url:this.url+"/shortcuts.htm",width:480,height:380,inline:true},{theme_url:this.url})},_mceColorPicker:function(k,j){var i=this.editor;j=j||{};i.windowManager.open({url:this.url+"/color_picker.htm",width:375+parseInt(i.getLang("advanced.colorpicker_delta_width",0)),height:250+parseInt(i.getLang("advanced.colorpicker_delta_height",0)),close_previous:false,inline:true},{input_color:j.color,func:j.func,theme_url:this.url})},_mceCodeEditor:function(j,k){var i=this.editor;i.windowManager.open({url:this.url+"/source_editor.htm",width:parseInt(i.getParam("theme_advanced_source_editor_width",720)),height:parseInt(i.getParam("theme_advanced_source_editor_height",580)),inline:true,resizable:true,maximizable:true},{theme_url:this.url})},_mceImage:function(j,k){var i=this.editor;if(i.dom.getAttrib(i.selection.getNode(),"class").indexOf("mceItem")!=-1){return}i.windowManager.open({url:this.url+"/image.htm",width:355+parseInt(i.getLang("advanced.image_delta_width",0)),height:275+parseInt(i.getLang("advanced.image_delta_height",0)),inline:true},{theme_url:this.url})},_mceLink:function(j,k){var i=this.editor;i.windowManager.open({url:this.url+"/link.htm",width:310+parseInt(i.getLang("advanced.link_delta_width",0)),height:200+parseInt(i.getLang("advanced.link_delta_height",0)),inline:true},{theme_url:this.url})},_mceNewDocument:function(){var i=this.editor;i.windowManager.confirm("advanced.newdocument",function(j){if(j){i.execCommand("mceSetContent",false,"")}})},_mceForeColor:function(){var i=this;this._mceColorPicker(0,{color:i.fgColor,func:function(j){i.fgColor=j;i.editor.execCommand("ForeColor",false,j)}})},_mceBackColor:function(){var i=this;this._mceColorPicker(0,{color:i.bgColor,func:function(j){i.bgColor=j;i.editor.execCommand("HiliteColor",false,j)}})},_ufirst:function(i){return i.substring(0,1).toUpperCase()+i.substring(1)}});e.ThemeManager.add("advanced",e.themes.AdvancedTheme)}(tinymce)); \ No newline at end of file diff --git a/js/tiny_mce/themes/advanced/editor_template_src.js b/js/tiny_mce/themes/advanced/editor_template_src.js index 279ca359ce..a3713b29e3 100644 --- a/js/tiny_mce/themes/advanced/editor_template_src.js +++ b/js/tiny_mce/themes/advanced/editor_template_src.js @@ -66,6 +66,9 @@ t.url = url; t.onResolveName = new tinymce.util.Dispatcher(this); + ed.forcedHighContrastMode = ed.settings.detect_highcontrast && t._isHighContrast(); + ed.settings.skin = ed.forcedHighContrastMode ? 'highcontrast' : ed.settings.skin; + // Default settings t.settings = s = extend({ theme_advanced_path : true, @@ -81,6 +84,8 @@ theme_advanced_resize_horizontal : 1, theme_advanced_resizing_use_cookie : 1, theme_advanced_font_sizes : "1,2,3,4,5,6,7", + theme_advanced_font_selector : "span", + theme_advanced_show_current_color: 0, readonly : ed.settings.readonly }, ed.settings); @@ -119,13 +124,19 @@ if (s.theme_advanced_statusbar_location == 'none') s.theme_advanced_statusbar_location = 0; + if (ed.settings.content_css !== false) + ed.contentCSS.push(ed.baseURI.toAbsolute(url + "/skins/" + ed.settings.skin + "/content.css")); + // Init editor ed.onInit.add(function() { - if (!ed.settings.readonly) + if (!ed.settings.readonly) { ed.onNodeChange.add(t._nodeChanged, t); - - if (ed.settings.content_css !== false) - ed.dom.loadCSS(ed.baseURI.toAbsolute("themes/advanced/skins/" + ed.settings.skin + "/content.css")); + ed.onKeyUp.add(t._updateUndoStatus, t); + ed.onMouseUp.add(t._updateUndoStatus, t); + ed.dom.bind(ed.dom.getRoot(), 'dragend', function() { + t._updateUndoStatus(ed); + }); + } }); ed.onSetProgressState.add(function(ed, b, ti) { @@ -153,6 +164,15 @@ DOM.loadCSS(url + "/skins/" + ed.settings.skin + "/ui_" + s.skin_variant + ".css"); }, + _isHighContrast : function() { + var actualColor, div = DOM.add(DOM.getRoot(), 'div', {'style': 'background-color: rgb(171,239,86);'}); + + actualColor = (DOM.getStyle(div, 'background-color', true) + '').toLowerCase().replace(/ /g, ''); + DOM.remove(div); + + return actualColor != 'rgb(171,239,86)' && actualColor != '#abef56'; + }, + createControl : function(n, cf) { var cd, c; @@ -230,9 +250,10 @@ // Toggle off the current format matches = ed.formatter.matchAll(formatNames); - if (matches[0] == name) - ed.formatter.remove(name); - else + if (!name || matches[0] == name) { + if (matches[0]) + ed.formatter.remove(matches[0]); + } else ed.formatter.apply(name); ed.undoManager.add(); @@ -300,6 +321,13 @@ c = ed.controlManager.createListBox('fontselect', { title : 'advanced.fontdefault', onselect : function(v) { + var cur = c.items[c.selectedIndex]; + + if (!v && cur) { + ed.execCommand('FontName', false, cur.value); + return; + } + ed.execCommand('FontName', false, v); // Fake selection, execCommand will fire a nodeChange and update the selection @@ -307,6 +335,10 @@ return v == sv; }); + if (cur && cur.value == v) { + c.select(null); + } + return false; // No auto select } }); @@ -324,6 +356,22 @@ var t = this, ed = t.editor, c, i = 0, cl = []; c = ed.controlManager.createListBox('fontsizeselect', {title : 'advanced.font_size', onselect : function(v) { + var cur = c.items[c.selectedIndex]; + + if (!v && cur) { + cur = cur.value; + + if (cur['class']) { + ed.formatter.toggle('fontsize_class', {value : cur['class']}); + ed.undoManager.add(); + ed.nodeChanged(); + } else { + ed.execCommand('FontSize', false, cur.fontSize); + } + + return; + } + if (v['class']) { ed.focus(); ed.undoManager.add(); @@ -338,6 +386,10 @@ return v == sv; }); + if (cur && (cur.value.fontSize == v.fontSize || cur.value['class'] == v['class'])) { + c.select(null); + } + return false; // No auto select }}); @@ -374,7 +426,11 @@ samp : 'advanced.samp' }, t = this; - c = t.editor.controlManager.createListBox('formatselect', {title : 'advanced.block', cmd : 'FormatBlock'}); + c = t.editor.controlManager.createListBox('formatselect', {title : 'advanced.block', onselect : function(v) { + t.editor.execCommand('FormatBlock', false, v); + return false; + }}); + if (c) { each(t.editor.getParam('theme_advanced_blockformats', t.settings.theme_advanced_blockformats, 'hash'), function(v, k) { c.add(t.editor.translate(k != v ? k : fmts[v]), v, {'class' : 'mce_formatPreview mce_' + v}); @@ -445,12 +501,19 @@ renderUI : function(o) { var n, ic, tb, t = this, ed = t.editor, s = t.settings, sc, p, nl; - n = p = DOM.create('span', {id : ed.id + '_parent', 'class' : 'mceEditor ' + ed.settings.skin + 'Skin' + (s.skin_variant ? ' ' + ed.settings.skin + 'Skin' + t._ufirst(s.skin_variant) : '')}); + if (ed.settings) { + ed.settings.aria_label = s.aria_label + ed.getLang('advanced.help_shortcut'); + } + + // TODO: ACC Should have an aria-describedby attribute which is user-configurable to describe what this field is actually for. + // Maybe actually inherit it from the original textara? + n = p = DOM.create('span', {role : 'application', 'aria-labelledby' : ed.id + '_voice', id : ed.id + '_parent', 'class' : 'mceEditor ' + ed.settings.skin + 'Skin' + (s.skin_variant ? ' ' + ed.settings.skin + 'Skin' + t._ufirst(s.skin_variant) : '')}); + DOM.add(n, 'span', {'class': 'mceVoiceLabel', 'style': 'display:none;', id: ed.id + '_voice'}, s.aria_label); if (!DOM.boxModel) n = DOM.add(n, 'div', {'class' : 'mceOldBoxModel'}); - n = sc = DOM.add(n, 'table', {id : ed.id + '_tbl', 'class' : 'mceLayout', cellSpacing : 0, cellPadding : 0}); + n = sc = DOM.add(n, 'table', {role : "presentation", id : ed.id + '_tbl', 'class' : 'mceLayout', cellSpacing : 0, cellPadding : 0}); n = tb = DOM.add(n, 'tbody'); switch ((s.theme_advanced_layout_manager || '').toLowerCase()) { @@ -469,7 +532,7 @@ n = o.targetNode; // Add classes to first and last TRs - nl = DOM.stdMode ? sc.getElementsByTagName('tr') : sc.rows; // Quick fix for IE 8 + nl = sc.rows; DOM.addClass(nl[0], 'mceFirst'); DOM.addClass(nl[nl.length - 1], 'mceLast'); @@ -525,6 +588,28 @@ t.deltaHeight = o.deltaHeight; o.targetNode = null; + ed.onKeyDown.add(function(ed, evt) { + var DOM_VK_F10 = 121, DOM_VK_F11 = 122; + + if (evt.altKey) { + if (evt.keyCode === DOM_VK_F10) { + // Make sure focus is given to toolbar in Safari. + // We can't do this in IE as it prevents giving focus to toolbar when editor is in a frame + if (tinymce.isWebKit) { + window.focus(); + } + t.toolbarGroup.focus(); + return Event.cancel(evt); + } else if (evt.keyCode === DOM_VK_F11) { + DOM.get(ed.id + '_path_row').focus(); + return Event.cancel(evt); + } + } + }); + + // alt+0 is the UK recommended shortcut for accessing the list of access controls. + ed.addShortcut('alt+0', '', 'mceShortcuts', t); + return { iframeContainer : ic, editorContainer : ed.id + '_parent', @@ -543,12 +628,12 @@ }, resizeBy : function(dw, dh) { - var e = DOM.get(this.editor.id + '_tbl'); + var e = DOM.get(this.editor.id + '_ifr'); this.resizeTo(e.clientWidth + dw, e.clientHeight + dh); }, - resizeTo : function(w, h) { + resizeTo : function(w, h, store) { var ed = this.editor, s = this.settings, e = DOM.get(ed.id + '_tbl'), ifr = DOM.get(ed.id + '_ifr'); // Boundery fix box @@ -566,8 +651,18 @@ DOM.setStyle(ifr, 'width', w); // Make sure that the size is never smaller than the over all ui - if (w < e.clientWidth) + if (w < e.clientWidth) { + w = e.clientWidth; DOM.setStyle(ifr, 'width', e.clientWidth); + } + } + + // Store away the size + if (store && s.theme_advanced_resizing_use_cookie) { + Cookie.setHash("TinyMCE_" + ed.id + "_size", { + cw : w, + ch : h + }); } }, @@ -662,7 +757,7 @@ each(explode(s.theme_advanced_containers || ''), function(c, i) { var v = s['theme_advanced_container_' + c] || ''; - switch (v.toLowerCase()) { + switch (c.toLowerCase()) { case 'mceeditor': n = DOM.add(tb, 'tr'); n = ic = DOM.add(n, 'td', {'class' : 'mceIframeContainer'}); @@ -730,17 +825,19 @@ }, _addToolbars : function(c, o) { - var t = this, i, tb, ed = t.editor, s = t.settings, v, cf = ed.controlManager, di, n, h = [], a; + var t = this, i, tb, ed = t.editor, s = t.settings, v, cf = ed.controlManager, di, n, h = [], a, toolbarGroup; - a = s.theme_advanced_toolbar_align.toLowerCase(); - a = 'mce' + t._ufirst(a); + toolbarGroup = cf.createToolbarGroup('toolbargroup', { + 'name': ed.getLang('advanced.toolbar'), + 'tab_focus_toolbar':ed.getParam('theme_advanced_tab_focus_toolbar') + }); - n = DOM.add(DOM.add(c, 'tr'), 'td', {'class' : 'mceToolbar ' + a}); + t.toolbarGroup = toolbarGroup; - if (!ed.getParam('accessibility_focus')) - h.push(DOM.createHTML('a', {href : '#', onfocus : 'tinyMCE.get(\'' + ed.id + '\').focus();'}, '')); + a = s.theme_advanced_toolbar_align.toLowerCase(); + a = 'mce' + t._ufirst(a); - h.push(DOM.createHTML('a', {href : '#', accesskey : 'q', title : ed.getLang("advanced.toolbar_focus")}, '')); + n = DOM.add(DOM.add(c, 'tr', {role: 'presentation'}), 'td', {'class' : 'mceToolbar ' + a, "role":"presentation"}); // Create toolbar and add the controls for (i=1; (v = s['theme_advanced_buttons' + i]); i++) { @@ -753,13 +850,11 @@ v = s['theme_advanced_buttons' + i + '_add_before'] + ',' + v; t._addControls(v, tb); - - //n.appendChild(n = tb.render()); - h.push(tb.renderHTML()); + toolbarGroup.add(tb); o.deltaHeight -= s.theme_advanced_row_height; } - + h.push(toolbarGroup.renderHTML()); h.push(DOM.createHTML('a', {href : '#', accesskey : 'z', title : ed.getLang("advanced.toolbar_focus"), onfocus : 'tinyMCE.getInstanceById(\'' + ed.id + '\').focus();'}, '')); DOM.setHTML(n, h.join('')); }, @@ -768,12 +863,18 @@ var n, t = this, ed = t.editor, s = t.settings, r, mf, me, td; n = DOM.add(tb, 'tr'); - n = td = DOM.add(n, 'td', {'class' : 'mceStatusbar'}); - n = DOM.add(n, 'div', {id : ed.id + '_path_row'}, s.theme_advanced_path ? ed.translate('advanced.path') + ': ' : ' '); - DOM.add(n, 'a', {href : '#', accesskey : 'x'}); + n = td = DOM.add(n, 'td', {'class' : 'mceStatusbar'}); + n = DOM.add(n, 'div', {id : ed.id + '_path_row', 'role': 'group', 'aria-labelledby': ed.id + '_path_voice'}); + if (s.theme_advanced_path) { + DOM.add(n, 'span', {id: ed.id + '_path_voice'}, ed.translate('advanced.path')); + DOM.add(n, 'span', {}, ': '); + } else { + DOM.add(n, 'span', {}, ' '); + } + if (s.theme_advanced_resizing) { - DOM.add(td, 'a', {id : ed.id + '_resize', href : 'javascript:;', onclick : "return false;", 'class' : 'mceResize'}); + DOM.add(td, 'a', {id : ed.id + '_resize', href : 'javascript:;', onclick : "return false;", 'class' : 'mceResize', tabIndex:"-1"}); if (s.theme_advanced_resizing_use_cookie) { ed.onPostRender.add(function() { @@ -787,12 +888,18 @@ } ed.onPostRender.add(function() { + Event.add(ed.id + '_resize', 'click', function(e) { + e.preventDefault(); + }); + Event.add(ed.id + '_resize', 'mousedown', function(e) { var mouseMoveHandler1, mouseMoveHandler2, mouseUpHandler1, mouseUpHandler2, startX, startY, startWidth, startHeight, width, height, ifrElm; function resizeOnMove(e) { + e.preventDefault(); + width = startWidth + (e.screenX - startX); height = startHeight + (e.screenY - startY); @@ -806,13 +913,9 @@ Event.remove(DOM.doc, 'mouseup', mouseUpHandler1); Event.remove(ed.getDoc(), 'mouseup', mouseUpHandler2); - // Store away the size - if (s.theme_advanced_resizing_use_cookie) { - Cookie.setHash("TinyMCE_" + ed.id + "_size", { - cw : width, - ch : height - }); - } + width = startWidth + (e.screenX - startX); + height = startHeight + (e.screenY - startY); + t.resizeTo(width, height, true); }; e.preventDefault(); @@ -837,8 +940,15 @@ n = tb = null; }, + _updateUndoStatus : function(ed) { + var cm = ed.controlManager, um = ed.undoManager; + + cm.setDisabled('undo', !um.hasUndo() && !um.typing); + cm.setDisabled('redo', !um.hasRedo()); + }, + _nodeChanged : function(ed, cm, n, co, ob) { - var t = this, p, de = 0, v, c, s = t.settings, cl, fz, fn, formatNames, matches; + var t = this, p, de = 0, v, c, s = t.settings, cl, fz, fn, fc, bc, formatNames, matches; tinymce.each(t.stateControls, function(c) { cm.setActive(c, ed.queryCommandState(t.controls[c][1])); @@ -860,8 +970,7 @@ }; cm.setActive('visualaid', ed.hasVisual); - cm.setDisabled('undo', !ed.undoManager.hasUndo() && !ed.typing); - cm.setDisabled('redo', !ed.undoManager.hasRedo()); + t._updateUndoStatus(ed); cm.setDisabled('outdent', !ed.queryCommandState('Outdent')); p = getParent('A'); @@ -878,12 +987,12 @@ } if (c = cm.get('anchor')) { - c.setActive(!!p && p.name); + c.setActive(!co && !!p && p.name); } p = getParent('IMG'); if (c = cm.get('image')) - c.setActive(!!p && n.className.indexOf('mceItem') == -1); + c.setActive(!co && !!p && n.className.indexOf('mceItem') == -1); if (c = cm.get('styleselect')) { t._importClasses(); @@ -909,12 +1018,20 @@ if (n.nodeName === 'SPAN') { if (!cl && n.className) cl = n.className; + } + if (ed.dom.is(n, s.theme_advanced_font_selector)) { if (!fz && n.style.fontSize) fz = n.style.fontSize; if (!fn && n.style.fontFamily) fn = n.style.fontFamily.replace(/[\"\']+/g, '').replace(/^([^,]+).*/, '$1').toLowerCase(); + + if (!fc && n.style.color) + fc = n.style.color; + + if (!bc && n.style.backgroundColor) + bc = n.style.backgroundColor; } return false; @@ -940,25 +1057,53 @@ return true; }); } + + if (s.theme_advanced_show_current_color) { + function updateColor(controlId, color) { + if (c = cm.get(controlId)) { + if (!color) + color = c.settings.default_color; + if (color !== c.value) { + c.displayColor(color); + } + } + } + updateColor('forecolor', fc); + updateColor('backcolor', bc); + } + + if (s.theme_advanced_show_current_color) { + function updateColor(controlId, color) { + if (c = cm.get(controlId)) { + if (!color) + color = c.settings.default_color; + if (color !== c.value) { + c.displayColor(color); + } + } + }; + + updateColor('forecolor', fc); + updateColor('backcolor', bc); + } if (s.theme_advanced_path && s.theme_advanced_statusbar_location) { p = DOM.get(ed.id + '_path') || DOM.add(ed.id + '_path_row', 'span', {id : ed.id + '_path'}); + + if (t.statusKeyboardNavigation) { + t.statusKeyboardNavigation.destroy(); + t.statusKeyboardNavigation = null; + } + DOM.setHTML(p, ''); getParent(function(n) { var na = n.nodeName.toLowerCase(), u, pi, ti = ''; - /*if (n.getAttribute('_mce_bogus')) - return; -*/ - // Ignore non element and hidden elements - if (n.nodeType != 1 || n.nodeName === 'BR' || (DOM.hasClass(n, 'mceItemHidden') || DOM.hasClass(n, 'mceItemRemoved'))) + // Ignore non element and bogus/hidden elements + if (n.nodeType != 1 || na === 'br' || n.getAttribute('data-mce-bogus') || DOM.hasClass(n, 'mceItemHidden') || DOM.hasClass(n, 'mceItemRemoved')) return; - // Fake name - if (v = DOM.getAttrib(n, 'mce_name')) - na = v; - // Handle prefix if (tinymce.isIE && n.scopeName !== 'HTML') na = n.scopeName + ':' + na; @@ -1033,14 +1178,25 @@ na = na.name; //u = "javascript:tinymce.EditorManager.get('" + ed.id + "').theme._sel('" + (de++) + "');"; - pi = DOM.create('a', {'href' : "javascript:;", onmousedown : "return false;", title : ti, 'class' : 'mcePath_' + (de++)}, na); + pi = DOM.create('a', {'href' : "javascript:;", role: 'button', onmousedown : "return false;", title : ti, 'class' : 'mcePath_' + (de++)}, na); if (p.hasChildNodes()) { - p.insertBefore(DOM.doc.createTextNode(' \u00bb '), p.firstChild); + p.insertBefore(DOM.create('span', {'aria-hidden': 'true'}, '\u00a0\u00bb '), p.firstChild); p.insertBefore(pi, p.firstChild); } else p.appendChild(pi); }, ed.getBody()); + + if (DOM.select('a', p).length > 0) { + t.statusKeyboardNavigation = new tinymce.ui.KeyboardNavigation({ + root: ed.id + "_path_row", + items: DOM.select('a', p), + excludeFromTabOrder: true, + onCancel: function() { + ed.focus(); + } + }, DOM); + } } }, @@ -1054,7 +1210,7 @@ var ed = this.editor; ed.windowManager.open({ - url : tinymce.baseURL + '/themes/advanced/anchor.htm', + url : this.url + '/anchor.htm', width : 320 + parseInt(ed.getLang('advanced.anchor_delta_width', 0)), height : 90 + parseInt(ed.getLang('advanced.anchor_delta_height', 0)), inline : true @@ -1067,9 +1223,9 @@ var ed = this.editor; ed.windowManager.open({ - url : tinymce.baseURL + '/themes/advanced/charmap.htm', + url : this.url + '/charmap.htm', width : 550 + parseInt(ed.getLang('advanced.charmap_delta_width', 0)), - height : 250 + parseInt(ed.getLang('advanced.charmap_delta_height', 0)), + height : 260 + parseInt(ed.getLang('advanced.charmap_delta_height', 0)), inline : true }, { theme_url : this.url @@ -1080,7 +1236,7 @@ var ed = this.editor; ed.windowManager.open({ - url : tinymce.baseURL + '/themes/advanced/about.htm', + url : this.url + '/about.htm', width : 480, height : 380, inline : true @@ -1089,13 +1245,25 @@ }); }, + _mceShortcuts : function() { + var ed = this.editor; + ed.windowManager.open({ + url: this.url + '/shortcuts.htm', + width: 480, + height: 380, + inline: true + }, { + theme_url: this.url + }); + }, + _mceColorPicker : function(u, v) { var ed = this.editor; v = v || {}; ed.windowManager.open({ - url : tinymce.baseURL + '/themes/advanced/color_picker.htm', + url : this.url + '/color_picker.htm', width : 375 + parseInt(ed.getLang('advanced.colorpicker_delta_width', 0)), height : 250 + parseInt(ed.getLang('advanced.colorpicker_delta_height', 0)), close_previous : false, @@ -1111,7 +1279,7 @@ var ed = this.editor; ed.windowManager.open({ - url : tinymce.baseURL + '/themes/advanced/source_editor.htm', + url : this.url + '/source_editor.htm', width : parseInt(ed.getParam("theme_advanced_source_editor_width", 720)), height : parseInt(ed.getParam("theme_advanced_source_editor_height", 580)), inline : true, @@ -1130,7 +1298,7 @@ return; ed.windowManager.open({ - url : tinymce.baseURL + '/themes/advanced/image.htm', + url : this.url + '/image.htm', width : 355 + parseInt(ed.getLang('advanced.image_delta_width', 0)), height : 275 + parseInt(ed.getLang('advanced.image_delta_height', 0)), inline : true @@ -1143,7 +1311,7 @@ var ed = this.editor; ed.windowManager.open({ - url : tinymce.baseURL + '/themes/advanced/link.htm', + url : this.url + '/link.htm', width : 310 + parseInt(ed.getLang('advanced.link_delta_width', 0)), height : 200 + parseInt(ed.getLang('advanced.link_delta_height', 0)), inline : true @@ -1191,4 +1359,4 @@ }); tinymce.ThemeManager.add('advanced', tinymce.themes.AdvancedTheme); -}(tinymce)); \ No newline at end of file +}(tinymce)); diff --git a/js/tiny_mce/themes/advanced/image.htm b/js/tiny_mce/themes/advanced/image.htm index f30d670641..b8ba729f6f 100644 --- a/js/tiny_mce/themes/advanced/image.htm +++ b/js/tiny_mce/themes/advanced/image.htm @@ -17,57 +17,57 @@
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - - - -
     
    - x -
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + +
     
    + x +
    diff --git a/js/tiny_mce/themes/advanced/img/colorpicker.jpg b/js/tiny_mce/themes/advanced/img/colorpicker.jpg index b4c542d107b25f68a9d4f9d7a109d0565d1f1437..b1a377aba7784d3a0a0fabb4d22b8114cde25ace 100644 GIT binary patch delta 2365 zcmYjO3pmqzAD>I9aMlowjp#C>)aEu~#cYyDxlQJt%Q?biYTo#tN~-`!P7ZFtr#nbAadE%ze9HDSzJMmFK4-1(Z9?%`+We=EjEv|W8 z!>fdSp#4VT&;i*tOEuLrFX!SvV5H)yQ$M{NTKU}Rsefh3dVL?o0U74_i)ZpjwWnX6 z6^GWUCyvDw8vI51WW6>*;PC!a;G{L3-)U)nS4+Mz!|^Nm96;x!$DzG3Jg0jb`V-hT z!RX58Qg_~Hsn)#>vYGK5Cq(JfK-%aRXp7MeN&Rl53iw{+L-+)W-WD?_Gv)rlC}&nC zch!w<-^aJ@-bXR16@ZXoZ~3np zHkJWU^yj*MC@S4CIAgtc)vERJ4#=w=c+x!dem~k9`*nt*JjoffY`OboyxPRP%Mp{o z3Is?qbu6n_(|p={5t(PxygI_$Ki%gJ3y86r22ZDXMbgbic+$jl@tct8aDGX^P8!5py0G(Y*e#hDL&adl}yF;&k=#MEQU?%wx7lj zg^D@iej=|$udQ;j$Y1u^zY&>rQP;i^ny}sb(MkyS^{Avxui{9St*d5QRuoe&vw$RE zP`P<}Ncy#QG=nPKM&5dVQ)wId<$bVOT??<&g0+x8xGtRC`12NY#>OFV_7*T09&zsL z$6JHpw~f9AFw<)B2$mIpy3fZdjzLS$Uyis3_x*H`v6FzN8P#9b2*N+pXh^Jp%< zz3=id_o{p`@dx!kNv6xdkh)!+r};VKp+81TQAtfJA$uPhjO16p$i=Pcb7qyFpUFM6 zXh>#7zZRNa?>}H--9-52_OSz4;D6Tgn<8GBk53x#-fy?Vuk56y^f59&d*(4bIYY00ZBdgfF#EuHHg( z-H;=%Gix(awxAypdOXi&u4L12N-yjqU%m}^LZL(2_cEC<8=?3Qff&iq${y@ zFYHZ8Zl6w}eQ6eK8+tW>tIlYTPzL6W=f4C+-eHM4GYxNYlbW?m2 zv%;g{S-F$|HG>Ed?QTs_%o#rHlh>vpG*rZ=ylRM7Kmr#65dLy#i?anDgQ}X^;>1~a zZck+}Q?VO0g;EfAEp%qzo>G;58DemHBwt@`H_tIb8QH%QjPuY;jrPuQ!_bXa>PS7+ zCaQ@Ogwiw)olB};8EQHb>)b?dAkC*9Dowu-4yq4o<8-aFiScSxs4FtRc(Q-wxKz27 zg8-xRADQKrR~tN^f`sQKX+|{ke1E0!^Nb3+(;j2w+`+fii%N0+&WuEHDS8DAKHlaS z?@2h$5%-+CDxab-pzt_@k!kV?6?MVcFOQ$hIR57;Sl-?!Z69h&Q7|G%A8m?u9*e&r zv8C|x7SsYfkkF6fbj*S|J;oOV@lt?q7_wswj0>wIQfvBT{NfXtsWp~R|5lk0ZtYmw zVZ^~LREbm!M)7dv7)Hg??v$Y7P_cU8uNZZ|D~e&mWU*ohW?@;);f?foEc?f5;ZjJU zlE9PwF=ZG1AgWk8uIO{K=)!MuCud}f7F~2$QoVYYmf~m$^t~}|b8Ut|v1?*G-R$Dx$TjdIOKcDK;mX@fH`!@w!LQ-p7}+gw+iH%#vd;c^>d>F zTrft9^Z7>o^Na*_ASiuZtWgMXI(|P{*mbX7wR~uTn{WVIZ^Hhifeczd?%Cc|P@jj4 z)cR==&S_jt*^-;slnb4tpqEkHb@wsy^*$YHECM(8+}PzN5^<#fmu9`LUSX0I^wtI$ z!!65Z1qrEnu+%&ix`kWiZaTea-x6IC~5 z*wI#uCo1&6l8=0_Yo?zsNR-n(!giGee;g)2ri9}6Xu!9b(LNjQ8&3CV7Oo+Hu!Kys@+D&1vNPYp$8X7kaM9`Kwp~Cx%94s{@0{2Sd9bYI*FY>E8Af!vsJOb`u+5UM zQ3W)7!-xd3PPi+Q-20;M#SNhvQcrH%T^RT1gzUYCM1*5pj*Cm`e4HUWKD9zl@;9b1 zEqikpFxXJ5j(H-uWIi9e2`<5UXf)auA+U?>!}WM`GpDrz$paBOarF7Yf-;CXS^$BV zBkb_-GlLW_p39MxymIwAl(O(=8lXK?9#&m!xHS`JOpBa W2*l#|Pa-Y~b(9+&nY$I(8UGuz^kRPi literal 3189 zcmbW0dsLEX8o)utyLkf>vO+8cOg9wF%x1j+p@RkpsHC1z^8%LRB~9&2XDqEGG)XNW za}>Dv$PIyhNYo}DFE8{K%%;saJRZN^Z|nBZpzy$8e9+2Iz;a<8Kk+#d^3T1~%eX+Yocd57U@)iBS;Lz~Rksn75)5aOo z?47y!`{oCW4<9{#^7PrO*Kd~J{`T(uhu>GYz#z*%v4Hp|*ne=j0$dhWR+d&aD_mfU z{lIJKY6bDeS-VBjZPE+fQ9+fq&?sTsg&TH0!Hk!%jG`%fj}7?y8(*!UeJ1mKAkW+N+qrtJ``cfL69@8V&h4pSlYZct zdbj(JoO9O?Qsypg_fMOg z#rMbU1sg3&fUGhub|uS1yIT&?FK_29gtOKhHhq6|)$&^OfnnC|ikp{TaNez5@_lf< zVtK=Xq%zSvAMNgxI$d``m?>^#DeXGE<=1t-8%N)&Uj?N0rRmZL=i-Ck?cDEJW9D3T zQNPlr2-xo8nJClmdhOM!G zSxEgwFp>mhr9k%KF1;r^Lf?*3q*Hw)AAX54&QN>v!`Sj4coX05(}r$KJj?NGNXrKD z8NeX+XC1e{BJniG?|2&dIw0`UbHjy&?fwkwr)jCV>jFx1PkkVvaTKR0CyLX7_nCecUzMp7ZL}O4zG~}I+CyvTeU-TI-o>tMCfOfLfd}6{ zn-VTf)-(a;Sp7?!H+8zxp-X96c*~5f=$(V9wU)QI1jM{4!5`D}1JYcRmW=fTf+e4QuYi-${T5Wl!DOA;{Oo23HgADWZ0p6&DQlQq?3y&OLbGnI?ce`qz*7HE3Q&J0yE1{KY(ay2sM|HXSio`Q) zzXlFjW+UfD{LLS0Y3NDMZ+bLSxya70{JN19=17g3?)?e9FZ5ZnrErV zvc9TlZ?yq&c7k1;y1CMvfr`2*p>dU3G~uVHuoh;U3XOlsL-Hc><_FsSENHw4o(p$j zw)bdIf$wKuY_M5uY7jo7*N8)xlDq44D&RA{O83Md zUZRt!OQyD3-d!M)y58T8o^7r1;Q)?=Jbggc))teO1jnW^(b!S@M~%0?c1D#A#m!42 z6EgV^RRPY~f@L299EO4F{YM6aRn%jA0bj&VhnX{+pd%E8D?>;{UE_=;kb=g2yfqfAsCc65n7)rm9R;0fugG!a?6I`}*+F&TF6jg!YbNSM&6n z!>=Ksh-cuFCLM#PT%OLR31*# zS!FN80v&b?Q9xLl3|=v$!KrSTHPk$lOz&cBC(uMCnl~&v&7{(2O78wex~cmSOpaE& z@n0x|jdJ&(EI@;CjEQDIz&KHWb$avInqg_#umE)7H0pr@iwQbrk>en z79En`gx%hnTVYhT!J&F=6h@YKI{B>qZeoJ13eb^8$|MD$Fd|@Xz9!KyjAO3$S7A&6 zYeXZFhR=5gk`glrvDnM5U17rT-%tL9$Xkv}o|0U3PlQp{eM3$Ocx?e|u{ujx6p2chSy@+SHkN##WBa9ifCVH+`fLyi`WHu2S0Ro<$2jyxdslxi%sXK_EHhD>M5VFx3b4`Flh zIc+g;!#Pf^N9TwRp)FB8seslma>NhVnFKcGYRfSYt`m)MKVN zJFFM37S4z!if;L>jai*Z;Dx9uyz#v$-IYW1Q)7knZia`sJ-gGm3ULV6Au?R(5Si3A z5F(+LINUNU&E#}=!BCsu%B>|82L8R~_$}at>B^3wP{a$xih^b*veU}^%SvA!+$lzK zBsz66-IK>Ysg7aaQ~#J+Ae@Vb#6Xz!tXUW*GLZDfkf66tq!{&32#Rm+|vJii{`y-7cV5enl_GL(c= z{?V^}q$&*ST0{H+~kYM|3uYAs#ozCy(?T>GWX{31NhEwAXaj z$-4<~)zvKkig3>%>7H#88haoT&KLQ(p^}5wZDdLx6KuYt)#=5@obg1ET z!{g_qB0WaNtYWyPG+?L#;E<_|jLW|K#~bMh0c5F+bE?jc+QiEu*c*>0hl)mt&v;q9 zPKAu!+3dJ`Y)zlylp^0O;m9NO(KQNpN*rDyx3ok0O5&`hV>Gm_4_)o#6CnbVu%_YL zkA_EL0QME}wev(ESKLmxMjDBc)Yb-aJM+rU(|mZh4tM?0}d<^7HhJa22mwL*EptRLFpXUAn5J_@V diff --git a/js/tiny_mce/themes/advanced/img/flash.gif b/js/tiny_mce/themes/advanced/img/flash.gif new file mode 100644 index 0000000000000000000000000000000000000000..dec3f7c7028df98657860529461af29b8793601c GIT binary patch literal 239 zcmVM~p;I&fgwbZVtlRJPxC7uw?yFxEX;uVr4IeWCJ^(5m4hjYVM>G^+2V)FnXE$mS p86yHh03AmHCKD}bWutOkFce4&0zF5CG_Myp4hRT+ig>^g06S0cRV@Gj literal 0 HcmV?d00001 diff --git a/js/tiny_mce/themes/advanced/img/icons.gif b/js/tiny_mce/themes/advanced/img/icons.gif index e46de5333082869b9bdab2576a554a2f9d01a966..641a9e3d314f4ea051692a2274aeb281456f5df9 100644 GIT binary patch delta 5712 zcmWlb_g_+r!+;NPfFmw6S5DldS!g*@OWe4%%#jUTiI!!SSIg=gjvz>=I7%9~NE=jI zq?J}$xC(7)Wm;BO8#kKQ%t2Q2e&3&-f8hB%pQn0##d>mRScun(R3*+Am{kL^W6aCs zDXcakldnkc0sz4JCx~y+!}VjZY`>aU;OIisFVXGFJxuXjgVjmw_rAfF9S%VpI&bc6 z%^-dP`(I2;H=z-Ofm`yRYz+3jZp)}fD;6`~Qlo|ZKMZtNYQR+_i3AVqfVytOCq#MH zViD7(w8-34IgnL)a+T~jHBu!Vz7yT?dRich%HU*}EOf{h>pzkhyKG|GDgEn~U@ey$ z_=GSWqQtlM&XE?x8%Nd$tE0qYXZ($YjDv3Of*qal=i{0jz62DU?{c*bu>+3M?-MvK!(t$v-|or{|~Z33pgBtLH* zC=3h--3*kG4OD~Q@93hS=8Tx*_bVv5rr1yQrp4O3hp0I9zw|ifzP7}Y5*8Z$Y8U4C z+RyP)Qt0U%y)i2^V)k1BvbJ_bqjFH)u`swtV-PlUjE+29ZA&crKGtzbE_OV&e80{O zSuxpfRjVVcQDJ?GsclWB=$O;wfDOu&-{Qopi2jCTJC?sae-8?R(8!xICR#Su<+6Oa zNMf;dqD+^tmB}pEGnIt{ZVBt-*d!-vIH<9idXl5<0esK|>#M6m%yv9=J@5Fg<`GP= z+4(`L{&M+_+0K`PQYX0Qu@mSExoEh)I(L=;dQr~n|D5d15D`c8 zC^)T=9eCS?_97_ai3m$JiTuNl_ikTK6lZc=OH!% zW+#>aJ^!R^5QS9Z8WkPk<*0tL3BLvXo`%mdrq2!;3)e5Bybu3)fD2*^Fj!fh6 z&V0T)viaV9kC(gNx>(1Me|^a2sjYYSEN0j>yvS)-UwoIFyOn>kDsq)Pm2Em7cD9P+ zV3Kt#bsGa$OZis^wy9mI#Rw#d*O_G28hOi4mPRny7o~X*8!IGVLfGg{j2x%CFheY9 zpIk5yp)_y|)@V3EIJIN5aJ9knQ>!jc{|pOfnELh3E(-P{*f8wB{- z=sG!M7gp4;m)q~>_a||P`OiLXhH(Gfr{3ZD{ol4vS*lc_1|TCxgBV01`XUZuX|~SJ zB=9H!%pUr_O=Z{037)ZS$=h zT%8gRpW)=OqrOr!xvEmLzXr~)UW;=`K`{2U5YAI6(aaYG8Z#w0@uHptH~GPfMbp?# zou^1$X%UwzPM7;pFyyN2gt!no6!5ZZK|4anjwtVJ4GZC3GYO2N@8CDrzPAcxaVY~_ zy@F{?VpP$vhfUtK=LI%cy;u!P?{P&a2c}uaj2Yj~!5j!41B|qng)bAZ7hr>W9Zl)> zPNB(KvGakfMQ;7hQ6CqWjf};MeulYh-)&eqjm@yNeGY3OEm12btL_TrcD7`Vh}WT; zt$)48P<8-mWc34x_`a$pGTuO}>=-;eSfvD_$h)ANd*_Q96 z|E9F{G)6bS!HhZ~DmBweuhUq;p?gnM>U@#caGgm$T~%$Ly*l;EOZrHbp4`5*56?!n z9SfM0RjkX8ws05edU~8BjlF`21r*AV>8Trp;U??dK~=B5aU%j?&@%z zeaKzWZTi|lxH=?r_scPYbJBMx#o)BovbH#lo7Q-(T{eV;3VMoR{&4%A|BB63TbO-W zw^Wi!qWV%bL4(00{n7*Xmml1e;h2()l8un!uUpda&H?RM8i{A<0%II|G0(2e;LKI@ zBbIuE-XL;7LA!>u!r^uO`&!zXd9r$S)LSyvQiCrOlss6Ip24_TaPCWcCL_f z{U0tkTqsQq_9nAKY58lmmc8?sdlHW_cGWM)y}UMPx3FZP{Xx%U`^thr|2|qNHRO<5 z%O#BSI{8^OCekB(X)mpL2(LI&XBTkjYI~;5fXb>Phyy3;BXu3(-lZ#w5!)+coZS4b z2ghjLo}DY&B*FG6&rKCWbIFN4iW*8`G~@03PhDy5`9nNHO7PMnBIwtX!4DiD%|S1_ zfj@4erbS3Drezlf(4Vy2dlwSWRpL|h)5@Dsf|((cZs0Knb`~L{4nR!{HPo8ZQW3|R ztK8`YW6qjgFU9aTl}iB#zYQA!pF zV{89)-K8b;;_%h}Z+4g6FovX<#n=3E)~LH7T0$61A5&*W-q`rGWvP3I`tn;#W4yv^ ze8(iu?ct!}@xe#=Ztyul`a$I~=3LzMeM`@nH#?aj20!lb_1b**FU_*jvkB~h`4~(Z zIUQvE)pcYv(#gwDa#6PTR$S3#H7x#NrHnrt@N1b@IKObUBjmqa-xZfFEiqsm5Buy0 zfzj=^d1Zb(Iqw<)s_j;2S1;Y8mY7tTxbGP--w@efb{OVg#dWa`4KN zQo;mao8#lmT03aMX)!OI!!AHeTQ}L?d{t;eI;Efb8_sSBfccXF1J3F0k{( zGHkfO?!a!Wtx{C;g z^&g21L2{P$PNt_Vj9e_96Ub z(y9S+f=R2(2zC`2ScQZ+d_)LTZj~MZ&*xdR30Ab@tYsi^z@H0XL5M?VfjVNOrynq% zK>Xjus54w%B3z8iK=*IIoh zkZUxsiXrmeO2cROK%YP1x3+W4Zh%(&Qj5tY+%epS=LFB#ZbufNL&vF?(?iiboL16O z=QhkvCB{fplEDvM2P+L}1IXMS%)$X+;h5n&H;UjgT} zbEyB5kioBIh)r;gUK|q3!fk@sFC@|DK$LvyvG0SYQ{m5cJ(x(>EEl`JstFU)qMpV_ zC6xF7K6?7ZL|KBHK!Z|+#U!lm8+r{r!<#(A`uCqw!Q~$t#N{g%eHrCv>z@Ix_Z#U& zT;qe5W6vDN5-i0-O~(@4-fCV+0HJ-=c!Fde~1vJEK2JBtN~DV19uDXoeWNIRY^>E$2(;JA9Q`Jh^l>gHtqyQ%C_#89x~eh3BdQ;ot#uc4vw{l5+iY%FW)C3$dfkxhZn{lxtCgrgHWx za*Z9OMt1^8kAxz{xJ45el6|>a05&m?ZH31!rE?$UA}lE6jwsk9_AyoU@=?L#tlALA z5q@?gKO_|~Xj^SfIrMGrF$Y>RCFH7;pu0mP&Qb|+xhMujd=F7>}wze{Zz%6+icMN0m$RNZ+lEMc}JqZ1PvK3Gf`_kX%!ccwubq2?)2T zoa&f@Aof*haIU`gzs$_u+Zs$H)uePe1IJa^rwD{!j-CO&-`esZys5UI_%CMr`)7Gg z;~T|iW=g;->k-4<1l3M#>lcS@&9^T|m(>P8NA_X&(Mo#aBYh^A2x@xnBRz%Ba|%Z) zzK;A_%Ll|pOJ>s618>!E-dK4N=b+tloG99%^lcX0$MI0<91kG;V3kDj&sliktymz5 zj|CvmTl%Dl0sU6$t}Z)&uKN7D@o38%|I32l%quj44_vB#S3iRBlwx)@vEOtENHTm1 zHEo_WT0g+P>Yn0BAGM3mv|_&Rt;RJ`PmGT>P9DxYFU?%HEfZyAc9v&eP-R}z-YitX zh^Yzy11L%SmGyE?E}v}y@#v>E>yEs%!mn2UMhLVew$qyx1?&(OM^^<5N7WAZLJONY zxfuv+1h=hcLnve^U5^m*a3G80MSe|Z@LgDIwiA?ZU!e8;jm67ZpIu0HWM66EPz{+`M$$=F3^! zFk4Bsg#-WK#ZqL{P*55?(v4yaWKlS&{TVADFL>b#B zNlx$`iDz(~Dzw5UUJNeVxP4Xn+X7%8`3o%J+}?dj>|I?ALB$yM7}8xZBt$&6;VEXzOzly3puJvzTGMP zkMN(Dq@{beaoqp#To4;-^OK*~p0IkMbp!NuG(mi2r??=VaHZZw-c9+G*o!e|2nKN#W)9F<(2iPC8$joHEV7eLSC+jan%{k_3rVVbpJgT6!wr zap~j`y!or;f0LfPA6{0bBh}YADr8jj!`JFajtZE4_QR_pJ}i^($16*qO$CPT3OqaH z!b|^Xp05yp9?sZdcw(1HoiMy|hhd%2aCrvml^Z~ujy0Z+b1aB6_F_9v$0ts&&6-Xq zoKECTC)G_SpPgRUI-PQNdj0V9h7Z#lXQww2;Tfj!OseBdn&(V<=uAf9OlH>1=E9jQ z-VCd5Cj0D6PV3B;yE9vdXL8|!Q+u}9e8d~So!Q|yo9{WhGjw)W;;d&VvfWXC6Vpe` z;04eTNX$)cdr&YuyYIv7{@GcWSd@H;y)m!wC%KTzKttNF&SRKp3R@jB@()s{Dmx3S z1g)wAbw%I=7p<%;ih+!&9A`N(A=NgZ2v^ags!jZPFze@`!rdEYXHeZjF#~lTLs)X6 zhns#l1M$q8tRv1zjOR`|&eabeGURrg3f*(+>`!NaU`7FL$Jt2;@w%X>;qF}H@Lbb} zIY|skr*d|U0sq@QvoTJLbPo2Bp-FVk#ll~$ykBi~#c6f;WEJnmrC&Gi7I!@Q)$Yi* zOG1r0IBUl+`UtO^N3=$GJx71tDg1qx_giskIshouO~Om}aEecGgPKZatA9Tt{uwa- zGZ+J%_WX02x3a#Cvw@5!$gu6v@ewjkj7iSp)<2_n|9pOFHOKb*0KfJtLW}MV$2<+P zPtH~Ceib_ZE^+?-KZT&2SOEi-p0AUnk4BcxCrkJJJ3RmO!~D1HGvdP8!@P-5SHS?ZZb(|>&^+AEsxk3?b^m>d-tq}V_EK|MdKRulO!mZ-m&H9_fu-S@0y zDJM>Ln`)>ZJnrR!2QF$Xs5$9k5WDr`r2T!dTh`;o>>myfk9ikP`j*L!dnDR=Q;Tz^ zsE<#D*SY0>np*T!y87(urkrV~XN~Jx^S^$Yb{>(X-K}=nGPC%_m7L+`+|M&#pCEwG F{{atF+}!{G delta 5709 zcmWlbc|6mN1INGn?)qAeInt2K5$kd#Br*5gXPP5T$Vgd=mZR@?hgpu04!TT+C1s%} z6*ZEp!=gi_nyBQdr`6-Bbv^q%zrQ|zeg68qUhmKQP~zc4VnCq3>-yw5v^($(0M@7U zUm`AmTGCStL@HCO)#l$Zs7_tnlz{@VK<9IfEB{qHYc!Gnhk?#U4H!i{N6lD|nSOOMoYuWG zm2LmP$6Z%z@7&s35oYvoW5>dWcT!rCKIQvO#yWa;ROsmYr5;R|FRHb3UbY|#UV_pw zE;Y4e{QWj$t5;F6dFzh1kz%!V2x~%gXRs->-ugwiY<^Q~oKfD@%*)fYx|Vw7q2$5U zRy5*6+ev0$iXB`PCJ*S4%!`-HE+M?C%VEu@ z9)7%Tz8o|n>}`%TARNdc)1t0+*o0>UoC?!QUN?TISJ0DrBzccoM3H(-o$qT{=#%Wa zrupvKqxpw|#;D7XQm3z};G3Jw`^Qql+@XdbxPs)&(oW;?&^mb zd*ZiDZWM*rKiBWW4-uoJ0wP4zc9j8WA;jhUB1KKtV>i*2Wc*Rrc?~@xnNeoL)vyM& zuFTo@@$#hR6c3V=!&&FgB(FjZvmW?iS4w~tx>mIfd_Ct$;FKLv)H-OgXX;Ww>lsPA zhY8G9-6pKF06v>pRu+;+mY*!tqb<#yeJ~`J8syw*BPx+DtI@qJXN-vog(PVH4!*`~ z86yDEv}X(*t;jq=jt5EqPBL|ZNt2CBmJ->AcuAyg znVz|G{_Bct!iYuL-aH0^@|dD#E(-bs9)9yEu}#X=TR$brZC>J3jnbqiALSd`r#_RI zrc}|LjAQw79Kp&eYt9Q7`ZFC9ekO8}Ubvq0*pdwOgR|I#rLLo3L~QV%WUKf%X7i-~ zeA>xUTP}7H$+~cnRW;2q@KA-t{#9JqM>gvgV&&6nbees36fH2xo8;{>a%98J2W<*w zt8LCON*&ABCY5$LxCbe4hl6i;$xvQ<|7=W}Ek$fsSSY@&st#V(Fq@9CLJttc3vmu& z+x;OOrIyY=Y@aeaD>|q0{`}}7aF6?7cFt0uo~`a8pZhT1R#W=Xbjcr=%mx0A-+>Zr zrpo5DKj{SH)Na95bFB!jbb}C%_-aqqGTok;d?Ye{4k$6U<)pcZ`y=(Q^SVv2rJr!5 zZ1>cvZoD2d$&RqC60X)RJiIbJeUxQ_${e;Q zc@!!e1Z(JlMV21?Oc5`-WJTInLHv<%t|9XX`Ht4L_trWd)*_#h#ec)6l7`owRap=} zUVSA5!1+$=z_>cAX{`kl@2y2&(>BfgR++R`=l^-h?q~ zvq8A)^N-q-h;4N-4fJfUV;x(Rp=W$zPTBPyinLp;?_UmE3WN=NNkB!B5NDp?L)lmP~yoPZJS}Fs#1!nj= zQ)X87V%sgBx&pX{ug{E98>S9pYg+ z%*$)xNFQen=S$W1vX~p7=Wo{^%mWw5XKML2l32O8oT{m2A@KuEi;Eo}HqiF>J05=u z={+>xiPy#ZOp&iRDH?X|Lt~eXvv|p>!{#Lv8vfr098cH)%^QQQmpjOvPlPLyv)w1M8;v>m8H}7N@F~SKOAft_t9X<=APm~8M z(cI-9zK;jg|MQuujvXhXG(xIx^6u(%{L|Q$=1@>|OIzNKq-7-scRxY-dtGYU2#cCr zmGP4Y5VRR%0&PtJ@?xsO^7?E34Pprl#!(TVPyn=%5l9A{NSkCB#8CK#ggy;#BHCj6 zFfq_?)bi+fvYl%#$AFz_636s6pOPV#gCL^G@oJwkSvAe5Hjke@+s?4iNM7e#e*2m~ z+@Rd*l;l&fHhCuXe&o;|nDW}FhZq3fWi9e!gG)>|j#?Ujf!372ChD9%Xh`ZCf?Zz| zJDIn2yBV|s+rPD42WWT;r7$3We$A0793slEDvf`5*8YCbipmD5SQ=$>@z*6=D!p!K z>GF}I>Z@-KKHd?W=%^S8L^YE_-nxNl(KWt`oa4PAd=oXgRtUkt;Mdp3yD)-u9Twm4 zlp-{2piX}~@tvzyfKw~0ECKhxnELrzZT>RfF-^`Xs@h>)_;l2Rlv@&iBkyU%KmIh4 ze%Fd}+Q5N1YnEYtB-YV19NPOV@6-Ym$Yzz;t9|ep_OfN(yRVRu*Wz2qbIkU$$KE>4 zvYrNxhN^dK+rS_8S}N*5{aA8L%373>Z<IcD(`1z&}}m-RIh`MtvD@?3u;4 z#pos`@i!kq&>n1s|Fc=npldU(R^oMfn>;}vR9v~gco$G^ivTj}`>r+9@?I(myWouq zv&TAr05r@QGm+Jx0iH;#_$walIYv&hJyWISa_ZYAP5GdH_d}wKv{Wyg|C-fI@?*lw ze9L-r+BK7qSN6uYND~t8>1r0+D$z#+edaEgiYoQZ^3{D7waAX;6|log_64_IykeYz zU*yS!QdwNxDzLZY5J$I!^4T*it3B!)h+2o-b`Co|50jDy*Pw_{zq)5+^7yl_k{jPV z3pG>4n)_`$x8I1on{gt3Fe*OM174id;a!BI*RQ&~Q=V6p5gS&#sGh;%gwZK_L$p$IjA!Fqx zNq<(f=`J7L^V>{W`YzyeA-L)aV-g&SxWZ6!f*Dvn+By#*R(<~PNfRQ@zqX@}tQo6t zF;}{X0j`z^^!x>Sz;nQ0KWeG{dXEL4l`7CLkt4A5t@&F&u~qL-8R8OwDL<)aQZ-O| ze!yn^fT!kw*Xn^f<$!PEfWN`*Itk}hb;5{7T;tLC zS}a>TCa_8f#CctWXxHfOSGO*3Tgu#gS)hXqXCo6N$zeC+!v;ZhSDD>>(^7$fvZ3Qn zvFRjA$A0l269SwPvEpu6#kN~zkKe;Utr)!y%Pq9nivz3pn;!u2Ku-mgn?ZK(Fdnvz za9>V!fEox9ETMp9xt3~gj}ScOh_Uup)=YvPM8KnV#O6JMqhM=vaaduAshqH(2W>9J;OF`T0EmJ3 zR3=8d78o1_mMQ_CUt>Vgg%=1G?H3_z)U%%Mc44E_r328W!y5M_gkE`AOayAVyf0sl z!A79q(-#U@a`d{ZFs;Uz#`R`jwP45_Ar4+uypsxOPDW}5Os`?ggg7%oyFkH9dWv&l-m+l=+EldK zi9W<*p*2}{tGZEp=1_)Jyi`#@JPa7x%Yk3{C?h#wq*`oLtAVWUF(x9xM9_Q=TuyiA z`x2OO!?4g@K$Sve!w1`j>quxDS3UtTbp((*;!>|4d>+N>VNB>_%{x!ZL%$T19g<#p zd48mVcJEQvz0FNQ|8_2KfC%)UQVmyKjM3xh=}ijqW2}8FCN8L%2y6d&om3fl?&IIk zj+NHngCNq1J6D95Wva(Es@P@HP3>Ke*ZwTKOb0;ro$e!Re{}}O_upxh2j4f5`TV@| zpYa2~@D%w8n9<^MvYJ`|`lSH)5RL)4WXPBB-@byTyXVf`tq@@#5nnB!Vn7+TRYWgK zgB)boisJ5Fzv0-J>*$?w9Q|nM(TZBLh0s(;%Tolrj_I#s;4#A!#&bN12qY_!@<_Ko zV-Q=D#UHy)8Y;c*HOG_&W032Z4Qb55>b$hS*L@De5^>*C`D_JmL;@_50(dAPPBA7H z@%k)$b(AQ>7f|OqIeJrq@cil^Cu}si#6g9D{p8^HvIM=_=hu3l_q=%C#!Toce%`n7 z`IWQ>#xm3^N{y9kjgAsYO@%^g(JP?U3GT(30JxcjGRJ~;RP2MD2$MWGB=I9L$0@`u z4=SQE+Wc*l(2i7ymW&w4qs(0mm3q+v^29&o#cDb^;0b|mC&D{P#(1^Y^NuWrHOWSX z?3eD3Zty0f?UmPS=K|s>F+R(CR#HykBwF~h<9LM9Y)v{CXjrYs8z zVqhr$DFXmd<|ao}{Mg!Jo{I1723G$AW%q8Z@q7Ao`bH9)|FVD^kJTlS;?N#Xk5oKq zQ#}$vJVuj*k^pcw>ZZcQhqeDzY=3c6gMlQI1Wq)bK+%+VR`ah;Yf!^;=bO=sZ9y7u zIjrBYIk%^iq}D1P|8gR*WWV719&R_LC41EKf~@oCmN;$^SY(eL%nI26O><71XhnG? z2=S|ffGTX<@0~(GddyBcC6P3vhJ}nuHs-kdQ?%s1&6$)8c!cjvtgQDIEHHT?qI0IM zSTiPuAmko^FmAHwc82gbP!-*<@_g&cx8LuZjsJ%Qg5(7^ScphlQ^k`g7YQf#0&4bA zDM5-&puSdHB#7mxE1?N4^aSgyG&6SE&|%(1s@aTc^}?k$=Vfma1aEkZH~md-+K6wj zEPq>}f)QgCf{iEBMZJwO{!Sss6vBJ=Y}ZjHnwyRTzY!emqLtRWkz9^Hldq#fCesX_ zz7QIBV6syY<|HqBYD)n5%(+Aa$3kP6d{@d#DnqyinP4qrzIg_UMK23O=05lltSIi3 zZvO7J2yNOM^_S3LTp+)=D<*iO@arS^A!g4jXD;G{a50i{ONux*^0>awBLQOudmv?} za0YY0A>(wQW%v7K9?#-T@$E91Bm)f&#SbWA34}fCZ>5+(eank&2lM|Iu z$reztp?{_gFS!yyjLu9ii9}31AWN3DbZPLZ`|qco&tER|R9PYxINg3#9c&pV`CaDW zc19B~84-btVJCmh# z<%4$1@}c3BV^5!-{vLxlFF zUoK8UIdbsav~~2ZoCG58@E??*7X7v@Itc3!{tP#3Q_&F$F2EcP!Z%+e=wG| z*Kg~B@6Ani^)vHMUq+-mo)&&Kys}`_t{Kwu*|1%6NyscO;-imQZj@bawC6ujcHLZ` z+}P~g&2TQgGB>V1H@-bL;bv~)Snifrxk;aLx2kg)I(f-vaNah%ycEy8)bPBtt$EwC z^U~owW@TPReO_jJ-j17jSz~!SU*+xkl$U+{pQgiqTD~tP{mk3rxo>ayzTB<*^0N1> z4*+(1>TP9TucMWCQxT}PI8GkA@6$e5y${i0bIern?VKdxw;z;m0vgx98(gJ8g^@Yx zaOGBRJG<;A%q|~eSG2P+N-V!cCqErpLgBBHm2BQ`>CNU`RjudV!^q_dB2 z^R14?U8O*nvJ#gdy&xLmBFw2X%Rg(E-{_ey{$#l1^v33`KbmfSUj^Vy$b7454jn1E zc2*^Rm4ES5e!DvVY&e%n;qO%d%x;W{f*&sX0dJ6U=~Rrgvfyfc!L|0EDaWyKDtymS zLGP=dH{KQ8@Dy6nK~gV&lLDoO;BS}U%@O=yXTfk~;YfYq=+HmjfIx>{d@dhTRFCz) zfd1ZE_`s}a+^%RMUDzC6)KcHu*v;QU#Nwpf-t95LQhvDo{KQz%i&sUTUb^J-HmAdi z%_?41@YD1d|BZ1wi9cR$J@7XBz`JkzFtUNtcB`3-~uq_WRwZ1E16flqGp5 zD)*hNR|X7^%{~z%O#Z9PhQGnE>Rm2d%`TK2xEq3m(@>czj4uFfQRDPhGnM3iK4FPt z)|}FOK&WRE)yDj6F<5TwlJjBiv!~^7l|{he)fr!`MvtuA+|fmEe8Q`BOdoB##5!hc zo47ZW`TlPkxyZXjZ%yU`Wn3I|+;Qi;fhD-rsb$btxlr@h8WX)WQ{6f5#xLynX8W=y K-wuZXHvA7eh13lI diff --git a/js/tiny_mce/themes/advanced/img/iframe.gif b/js/tiny_mce/themes/advanced/img/iframe.gif new file mode 100644 index 0000000000000000000000000000000000000000..410c7ad084db698e9f35e3230233aa4040682566 GIT binary patch literal 600 zcmZ?wbhEHb6krfwcoxm@|NsB$<##6SeDUYszh8g<{{H*-%a7k_-3KZc-T3+YPwBiX zzyAIE^Y`z!U%$Wp{QdX;|FQ*Fw;jIy{pasbUw?o3{yVB>Q_sRgx-G9iegFLZcfrha#d9w;%sU=Zx~6K$tw~$%z4`#O^Y@3Z zKV#~)MpSKh@#b63l#}6=>yq2|{`&JLqwny)|NnC)o%r$l&-Y)yKYjo8?#quSuRaGB zt_&<%`RV)bl#YEr{`~p)?RU|v^Y1_Z`u*?Ux8J`*N>>+5JlMAOZr+qr@y$D{mfVhO z+zt#7208-8pDc_F4ABfaAUi>E!oa?@A-bu#r8Qd6oKeb*Lx9UTz)0QBL@+vxY38ii zvqGa87c5+~h&?)zVa3W-D;=U$88}^qMBJ^ERU|z17!;#97+4%Rd1XcXJq#>t8KR;E z7zr5i6BgH5y=gAD)sAQlGB zh8au?j!n~E(Pks?@!j1fR&j*RWY8GF(-=x H6d0@lT&58X literal 0 HcmV?d00001 diff --git a/js/tiny_mce/themes/advanced/img/pagebreak.gif b/js/tiny_mce/themes/advanced/img/pagebreak.gif new file mode 100644 index 0000000000000000000000000000000000000000..acdf4085f3068c4c0a1d6855f4b80dae8bac3068 GIT binary patch literal 325 zcmV-L0lNN2Nk%w1VPpUd0J9GO`>v<{=;ru;boX6P{`2zsmyZ3>&HK5t_;hIbi-G;z z+4`cI{KdfcXj}GCLjV8&A^8LW000jFEC2ui0Av6R000E?@X1N5y*TU5yZ>M)j$|1M z4Ouvb$pHu>IW8BZq|n;U0s@T!VM5~w1_+1X!EiVl!&PITYdjT!ffYfpt{jAfv%qvh zA63WUHSlr7LkeyaV4(pM0f50(II?RD4RtMg4-E+tFhdAy5{3c=0}3Bg9Y8`B2To20 zR%SO62L%9}0H+dzoKB$+2TOwzUrwi{XiBM^4V#>63q3!LsU3u93zH8CdwqY%62;1g z0g8ze$k93lWExp`CUe|K4qOWk17ZeJ0|5pDP6+}};{>bI@lOWj=kf}r2sHp7w9-Ie XK%9UG6W(*AX-vY05F<*&5CH%?Gwy&_ literal 0 HcmV?d00001 diff --git a/js/tiny_mce/themes/advanced/img/quicktime.gif b/js/tiny_mce/themes/advanced/img/quicktime.gif new file mode 100644 index 0000000000000000000000000000000000000000..8f10e7aa6b6ab40ee69a1a41a961c092168d6fda GIT binary patch literal 301 zcmV+|0n+|QNk%w1VGsZi0Q4UK+~)L6v+~s9^fsC5ZpZP=*zu3F=Jxpf8k_5u%JNv6 z=md-84VLU4w)kSE=yI&-yw>b=v+SqE?+kq47pC+YrR?bJ^yu>Zyvpn;hTp*6^mM!O zu+8!}sO$`q%8%`=C5EEn#1d#z95FHtK5(^#(cp^e+Y!d=4FCrFbY9A3U z4-O0-4kHJPJ2(jk13n5879s!!3Q`V>8VwW`9my3H#|R8ZD+fdx0E-+693cQZ;!k;* literal 0 HcmV?d00001 diff --git a/js/tiny_mce/themes/advanced/img/realmedia.gif b/js/tiny_mce/themes/advanced/img/realmedia.gif new file mode 100644 index 0000000000000000000000000000000000000000..fdfe0b9ac05869ae845fdd828eaad97cc0c69dbc GIT binary patch literal 439 zcmV;o0Z9HwNk%w1VI=?(0K^{vQcz8xz}f&njBB06v9GQ`Jv%NdDHCI&z`wqZw$(Lw zuFTBL!Pe#<92tv>h)9OE1Xh}vnVEHSaeb-GByg#tqM_B*)YRkdSdqTu&}n`s(k;lb>H+`#+Q6|3c{>OLTv23;utm>DSfy zuOD3adm!iUuGar)4FAhzel5=UwZ7*6(K(+k@BP_g{o}}@k7u_2k7W2iGwlom!+#Z( z|Hj5w_4MwTo8QaHxm#EFYX1DUOO|}vvgQBb!_ST${rmj+`+Fep|C$j4HGtwz7FGrZ zO$Hs1VIV&_u+2R%#bJV$RKJIcL*N7vss0Y-EsB{gGlSJaTr>sRLKbLj5HMTpyK;)l zJcfpaMYltBZdEK6Kht6+BPy*VtthFMtIoqFC=#Tu$e^eaDXCC7U0vOYOJjNk(;P!VagC#fQ*?7otVO)-#9rK#nB%ry4`E_DHQ Wm01j~^6E13^D1O7+^=wCum%9s<%z=p literal 0 HcmV?d00001 diff --git a/js/tiny_mce/themes/advanced/img/trans.gif b/js/tiny_mce/themes/advanced/img/trans.gif new file mode 100644 index 0000000000000000000000000000000000000000..388486517fa8da13ebd150e8f65d5096c3e10c3a GIT binary patch literal 43 ncmZ?wbhEHbWMp7un7{x9ia%KxMSyG_5FaGNz{KRj$Y2csb)f_x literal 0 HcmV?d00001 diff --git a/js/tiny_mce/themes/advanced/img/video.gif b/js/tiny_mce/themes/advanced/img/video.gif new file mode 100644 index 0000000000000000000000000000000000000000..3570104077a3b3585f11403c8d4c3fc9351f35d2 GIT binary patch literal 597 zcmZ?wbhEHb6krfwc$UTx9v<%P?Ok48Ze?YanwpxCkzrwBk(ZYzB_&l;Qw!gmM(Ep^QBwbzIoSdAh>*2n> zz9l6k0Xw#(?);y5^ls9w|LObxXI*si^YfcEYu3*P8J(S-PEJlaNB-yTd}C^Ax@_69 zzP`Ryt5)S5`=P3;TDk9SbaeFk_3NiTjGA~aFd-pf@}tlxQ>GLb7jM|Gp`oFHlaq7F zk|nvhxjsHV=g+oST3Rl6T(N1>rn0iK*Ed>3MMVn>3vF#}**q!otE>Sy|^jDoRUBoBANRc=wyaJged$+}u3x zK}ld>puWET{||NozXdO-0f3nK$V8iNkVNKl+Guy1NeYie$3 zZB}=&Zex!RYq8YfVwgNdMpdFkN|rU!Fha}0m66q>CDxczOhH^pM9qvxw1p`;Rftzu zQJ&9}g>iErlc2ORw;aC_=l*6UJ=st%r*ISVV2jgDT<)w>rXHGL<21Kdo z#uyug^O^t z0hZGrt*x!>$1C!zn`W5@`ts6_uMW)2%<0NUEKIo?SIPPE=}U0}7Z(?JcX!y=*;bF< zCWz-=h7+2ao9)(dOHM;+X=xs9)%!~xc&ICMZdRYdUQ2$^@9y(6X3NCIz{cM7f^Z=Q z1_tQ95kgl8b%R%OiYTIo7LSdE^@}A^8LW002J#EC2ui01p5U000KOz@O0K01zUifeIyT9%!RzMDgehG|mwLz+Eh; z7Z~iE zrX?OfJ^>XeDJK)xJuWOB3_l1N0Ra>g4Gk^=ED0V6LI?>4;Q|6OB{LplLMRLg8U5-E J?0y6R06W6!pgRBn literal 0 HcmV?d00001 diff --git a/js/tiny_mce/themes/advanced/js/about.js b/js/tiny_mce/themes/advanced/js/about.js index 5cee9ed863..5b35845761 100644 --- a/js/tiny_mce/themes/advanced/js/about.js +++ b/js/tiny_mce/themes/advanced/js/about.js @@ -66,6 +66,7 @@ function insertHelpIFrame() { html = ''; document.getElementById('iframecontainer').innerHTML = html; document.getElementById('help_tab').style.display = 'block'; + document.getElementById('help_tab').setAttribute("aria-hidden", "false"); } } diff --git a/js/tiny_mce/themes/advanced/js/anchor.js b/js/tiny_mce/themes/advanced/js/anchor.js index 7fe7810558..04f41e0cac 100644 --- a/js/tiny_mce/themes/advanced/js/anchor.js +++ b/js/tiny_mce/themes/advanced/js/anchor.js @@ -19,15 +19,21 @@ var AnchorDialog = { update : function() { var ed = this.editor, elm, name = document.forms[0].anchorName.value; + if (!name || !/^[a-z][a-z0-9\-\_:\.]*$/i.test(name)) { + tinyMCEPopup.alert('advanced_dlg.anchor_invalid'); + return; + } + tinyMCEPopup.restoreSelection(); if (this.action != 'update') ed.selection.collapse(1); elm = ed.dom.getParent(ed.selection.getNode(), 'A'); - if (elm) + if (elm) { + elm.setAttribute('name', name); elm.name = name; - else + } else ed.execCommand('mceInsertContent', 0, ed.dom.createHTML('a', {name : name, 'class' : 'mceItemAnchor'}, '')); tinyMCEPopup.close(); diff --git a/js/tiny_mce/themes/advanced/js/charmap.js b/js/tiny_mce/themes/advanced/js/charmap.js index 8c5aea1721..bb1869558c 100644 --- a/js/tiny_mce/themes/advanced/js/charmap.js +++ b/js/tiny_mce/themes/advanced/js/charmap.js @@ -173,7 +173,7 @@ var charmap = [ ['ý', 'ý', true, 'y - acute'], ['þ', 'þ', true, 'thorn'], ['ÿ', 'ÿ', true, 'y - diaeresis'], - ['Α', 'Α', true, 'Alpha'], + ['Α', 'Α', true, 'Alpha'], ['Β', 'Β', true, 'Beta'], ['Γ', 'Γ', true, 'Gamma'], ['Δ', 'Δ', true, 'Delta'], @@ -258,8 +258,8 @@ var charmap = [ ['⌋', '⌋', false,'right floor'], ['⟨', '〈', false,'left-pointing angle bracket'], ['⟩', '〉', false,'right-pointing angle bracket'], - ['◊', '◊', true,'lozenge'], - ['♠', '♠', false,'black spade suit'], + ['◊', '◊', true, 'lozenge'], + ['♠', '♠', true, 'black spade suit'], ['♣', '♣', true, 'black club suit'], ['♥', '♥', true, 'black heart suit'], ['♦', '♦', true, 'black diamond suit'], @@ -275,19 +275,46 @@ var charmap = [ tinyMCEPopup.onInit.add(function() { tinyMCEPopup.dom.setHTML('charmapView', renderCharMapHTML()); + addKeyboardNavigation(); }); +function addKeyboardNavigation(){ + var tableElm, cells, settings; + + cells = tinyMCEPopup.dom.select("a.charmaplink", "charmapgroup"); + + settings ={ + root: "charmapgroup", + items: cells + }; + cells[0].tabindex=0; + tinyMCEPopup.dom.addClass(cells[0], "mceFocus"); + if (tinymce.isGecko) { + cells[0].focus(); + } else { + setTimeout(function(){ + cells[0].focus(); + }, 100); + } + tinyMCEPopup.editor.windowManager.createInstance('tinymce.ui.KeyboardNavigation', settings, tinyMCEPopup.dom); +} + function renderCharMapHTML() { var charsPerRow = 20, tdWidth=20, tdHeight=20, i; - var html = ''; + var html = '
    '+ + '
    '; var cols=-1; for (i=0; i' - + '' + + '' + charmap[i][1] + ''; if ((cols+1) % charsPerRow == 0) @@ -301,7 +328,8 @@ function renderCharMapHTML() { html += ''; } - html += '
     
    '; + html += '
    '; + html = html.replace(/
    ' + // TODO: VoiceOver doesn't seem to support legend as a label referenced by labelledby. + h += '
    ' + ''; for (i=0; i' - + '' - + ''; + + ''; + if (tinyMCEPopup.editor.forcedHighContrastMode) { + h += ''; + } + h += ''; + h += ''; if ((i+1) % 18 == 0) h += ''; } - h += '
    '; + h += '
    '; el.innerHTML = h; el.className = 'generated'; + + paintCanvas(el); + enableKeyboardNavigation(el.firstChild); } +function paintCanvas(el) { + tinyMCEPopup.getWin().tinymce.each(tinyMCEPopup.dom.select('canvas.mceColorSwatch', el), function(canvas) { + var context; + if (canvas.getContext && (context = canvas.getContext("2d"))) { + context.fillStyle = canvas.getAttribute('data-color'); + context.fillRect(0, 0, 10, 10); + } + }); +} function generateNamedColors() { var el = document.getElementById('namedcolors'), h = '', n, v, i = 0; @@ -178,11 +238,27 @@ function generateNamedColors() { for (n in named) { v = named[n]; - h += '' + h += ''; + if (tinyMCEPopup.editor.forcedHighContrastMode) { + h += ''; + } + h += ''; + h += ''; + i++; } el.innerHTML = h; el.className = 'generated'; + + paintCanvas(el); + enableKeyboardNavigation(el); +} + +function enableKeyboardNavigation(el) { + tinyMCEPopup.editor.windowManager.createInstance('tinymce.ui.KeyboardNavigation', { + root: el, + items: tinyMCEPopup.dom.select('a', el) + }, tinyMCEPopup.dom); } function dechex(n) { @@ -190,10 +266,10 @@ function dechex(n) { } function computeColor(e) { - var x, y, partWidth, partDetail, imHeight, r, g, b, coef, i, finalCoef, finalR, finalG, finalB; + var x, y, partWidth, partDetail, imHeight, r, g, b, coef, i, finalCoef, finalR, finalG, finalB, pos = tinyMCEPopup.dom.getPos(e.target); - x = e.offsetX ? e.offsetX : (e.target ? e.clientX - e.target.x : 0); - y = e.offsetY ? e.offsetY : (e.target ? e.clientY - e.target.y : 0); + x = e.offsetX ? e.offsetX : (e.target ? e.clientX - pos.x : 0); + y = e.offsetY ? e.offsetY : (e.target ? e.clientY - pos.y : 0); partWidth = document.getElementById('colors').width / 6; partDetail = detail / 2; diff --git a/js/tiny_mce/themes/advanced/js/image.js b/js/tiny_mce/themes/advanced/js/image.js index 6423d90809..6c2489a168 100644 --- a/js/tiny_mce/themes/advanced/js/image.js +++ b/js/tiny_mce/themes/advanced/js/image.js @@ -18,7 +18,7 @@ var ImageDialog = { e = ed.selection.getNode(); - this.fillFileList('image_list', 'tinyMCEImageList'); + this.fillFileList('image_list', tinyMCEPopup.getParam('external_image_list', 'tinyMCEImageList')); if (e.nodeName == 'IMG') { f.src.value = ed.dom.getAttrib(e, 'src'); @@ -39,7 +39,7 @@ var ImageDialog = { fillFileList : function(id, l) { var dom = tinyMCEPopup.dom, lst = dom.get(id), v, cl; - l = window[l]; + l = typeof(l) === 'function' ? l() : window[l]; if (l && l.length > 0) { lst.options[lst.options.length] = new Option('', ''); @@ -77,7 +77,7 @@ var ImageDialog = { args.style = this.styleVal; tinymce.extend(args, { - src : f.src.value, + src : f.src.value.replace(/ /g, '%20'), alt : f.alt.value, width : f.width.value, height : f.height.value @@ -87,10 +87,16 @@ var ImageDialog = { if (el && el.nodeName == 'IMG') { ed.dom.setAttribs(el, args); + tinyMCEPopup.editor.execCommand('mceRepaint'); + tinyMCEPopup.editor.focus(); } else { - ed.execCommand('mceInsertContent', false, '', {skip_undo : 1}); - ed.dom.setAttribs('__mce_tmp', args); - ed.dom.setAttrib('__mce_tmp', 'id', ''); + tinymce.each(args, function(value, name) { + if (value === "") { + delete args[name]; + } + }); + + ed.execCommand('mceInsertContent', false, tinyMCEPopup.editor.dom.createHTML('img', args), {skip_undo : 1}); ed.undoManager.add(); } diff --git a/js/tiny_mce/themes/advanced/js/link.js b/js/tiny_mce/themes/advanced/js/link.js index f67a5bc828..53ff409e79 100644 --- a/js/tiny_mce/themes/advanced/js/link.js +++ b/js/tiny_mce/themes/advanced/js/link.js @@ -31,7 +31,7 @@ var LinkDialog = { }, update : function() { - var f = document.forms[0], ed = tinyMCEPopup.editor, e, b; + var f = document.forms[0], ed = tinyMCEPopup.editor, e, b, href = f.href.value.replace(/ /g, '%20'); tinyMCEPopup.restoreSelection(); e = ed.dom.getParent(ed.selection.getNode(), 'A'); @@ -39,7 +39,6 @@ var LinkDialog = { // Remove element if there is no href if (!f.href.value) { if (e) { - tinyMCEPopup.execCommand("mceBeginUndoLevel"); b = ed.selection.getBookmark(); ed.dom.remove(e, 1); ed.selection.moveToBookmark(b); @@ -49,19 +48,17 @@ var LinkDialog = { } } - tinyMCEPopup.execCommand("mceBeginUndoLevel"); - // Create new anchor elements if (e == null) { ed.getDoc().execCommand("unlink", false, null); - tinyMCEPopup.execCommand("CreateLink", false, "#mce_temp_url#", {skip_undo : 1}); + tinyMCEPopup.execCommand("mceInsertLink", false, "#mce_temp_url#", {skip_undo : 1}); tinymce.each(ed.dom.select("a"), function(n) { if (ed.dom.getAttrib(n, 'href') == '#mce_temp_url#') { e = n; ed.dom.setAttribs(e, { - href : f.href.value, + href : href, title : f.linktitle.value, target : f.target_list ? getSelectValue(f, "target_list") : null, 'class' : f.class_list ? getSelectValue(f, "class_list") : null @@ -70,7 +67,7 @@ var LinkDialog = { }); } else { ed.dom.setAttribs(e, { - href : f.href.value, + href : href, title : f.linktitle.value, target : f.target_list ? getSelectValue(f, "target_list") : null, 'class' : f.class_list ? getSelectValue(f, "class_list") : null diff --git a/js/tiny_mce/themes/advanced/js/source_editor.js b/js/tiny_mce/themes/advanced/js/source_editor.js index 279328614c..84546ad52e 100644 --- a/js/tiny_mce/themes/advanced/js/source_editor.js +++ b/js/tiny_mce/themes/advanced/js/source_editor.js @@ -44,19 +44,13 @@ function toggleWordWrap(elm) { setWrap('off'); } -var wHeight=0, wWidth=0, owHeight=0, owWidth=0; - function resizeInputs() { - var el = document.getElementById('htmlSource'); + var vp = tinyMCEPopup.dom.getViewPort(window), el; - if (!tinymce.isIE) { - wHeight = self.innerHeight - 65; - wWidth = self.innerWidth - 16; - } else { - wHeight = document.body.clientHeight - 70; - wWidth = document.body.clientWidth - 16; - } + el = document.getElementById('htmlSource'); - el.style.height = Math.abs(wHeight) + 'px'; - el.style.width = Math.abs(wWidth) + 'px'; + if (el) { + el.style.width = (vp.w - 20) + 'px'; + el.style.height = (vp.h - 65) + 'px'; + } } diff --git a/js/tiny_mce/themes/advanced/langs/en.js b/js/tiny_mce/themes/advanced/langs/en.js index 69694b1f9f..6e58481874 100644 --- a/js/tiny_mce/themes/advanced/langs/en.js +++ b/js/tiny_mce/themes/advanced/langs/en.js @@ -1,62 +1 @@ -tinyMCE.addI18n('en.advanced',{ -style_select:"Styles", -font_size:"Font size", -fontdefault:"Font family", -block:"Format", -paragraph:"Paragraph", -div:"Div", -address:"Address", -pre:"Preformatted", -h1:"Heading 1", -h2:"Heading 2", -h3:"Heading 3", -h4:"Heading 4", -h5:"Heading 5", -h6:"Heading 6", -blockquote:"Blockquote", -code:"Code", -samp:"Code sample", -dt:"Definition term ", -dd:"Definition description", -bold_desc:"Bold (Ctrl+B)", -italic_desc:"Italic (Ctrl+I)", -underline_desc:"Underline (Ctrl+U)", -striketrough_desc:"Strikethrough", -justifyleft_desc:"Align left", -justifycenter_desc:"Align center", -justifyright_desc:"Align right", -justifyfull_desc:"Align full", -bullist_desc:"Unordered list", -numlist_desc:"Ordered list", -outdent_desc:"Outdent", -indent_desc:"Indent", -undo_desc:"Undo (Ctrl+Z)", -redo_desc:"Redo (Ctrl+Y)", -link_desc:"Insert/edit link", -unlink_desc:"Unlink", -image_desc:"Insert/edit image", -cleanup_desc:"Cleanup messy code", -code_desc:"Edit HTML Source", -sub_desc:"Subscript", -sup_desc:"Superscript", -hr_desc:"Insert horizontal ruler", -removeformat_desc:"Remove formatting", -custom1_desc:"Your custom description here", -forecolor_desc:"Select text color", -backcolor_desc:"Select background color", -charmap_desc:"Insert custom character", -visualaid_desc:"Toggle guidelines/invisible elements", -anchor_desc:"Insert/edit anchor", -cut_desc:"Cut", -copy_desc:"Copy", -paste_desc:"Paste", -image_props_desc:"Image properties", -newdocument_desc:"New document", -help_desc:"Help", -blockquote_desc:"Blockquote", -clipboard_msg:"Copy/Cut/Paste is not available in Mozilla and Firefox.\r\nDo you want more information about this issue?", -path:"Path", -newdocument:"Are you sure you want clear all contents?", -toolbar_focus:"Jump to tool buttons - Alt+Q, Jump to editor - Alt-Z, Jump to element path - Alt-X", -more_colors:"More colors" -}); \ No newline at end of file +tinyMCE.addI18n('en.advanced',{"underline_desc":"Underline (Ctrl+U)","italic_desc":"Italic (Ctrl+I)","bold_desc":"Bold (Ctrl+B)",dd:"Definition Description",dt:"Definition Term ",samp:"Code Sample",code:"Code",blockquote:"Block Quote",h6:"Heading 6",h5:"Heading 5",h4:"Heading 4",h3:"Heading 3",h2:"Heading 2",h1:"Heading 1",pre:"Preformatted",address:"Address",div:"DIV",paragraph:"Paragraph",block:"Format",fontdefault:"Font Family","font_size":"Font Size","style_select":"Styles","anchor_delta_height":"","anchor_delta_width":"","charmap_delta_height":"","charmap_delta_width":"","colorpicker_delta_height":"","colorpicker_delta_width":"","link_delta_height":"","link_delta_width":"","image_delta_height":"","image_delta_width":"","more_colors":"More Colors...","toolbar_focus":"Jump to tool buttons - Alt+Q, Jump to editor - Alt-Z, Jump to element path - Alt-X",newdocument:"Are you sure you want clear all contents?",path:"Path","clipboard_msg":"Copy/Cut/Paste is not available in Mozilla and Firefox.\nDo you want more information about this issue?","blockquote_desc":"Block Quote","help_desc":"Help","newdocument_desc":"New Document","image_props_desc":"Image Properties","paste_desc":"Paste (Ctrl+V)","copy_desc":"Copy (Ctrl+C)","cut_desc":"Cut (Ctrl+X)","anchor_desc":"Insert/Edit Anchor","visualaid_desc":"show/Hide Guidelines/Invisible Elements","charmap_desc":"Insert Special Character","backcolor_desc":"Select Background Color","forecolor_desc":"Select Text Color","custom1_desc":"Your Custom Description Here","removeformat_desc":"Remove Formatting","hr_desc":"Insert Horizontal Line","sup_desc":"Superscript","sub_desc":"Subscript","code_desc":"Edit HTML Source","cleanup_desc":"Cleanup Messy Code","image_desc":"Insert/Edit Image","unlink_desc":"Unlink","link_desc":"Insert/Edit Link","redo_desc":"Redo (Ctrl+Y)","undo_desc":"Undo (Ctrl+Z)","indent_desc":"Increase Indent","outdent_desc":"Decrease Indent","numlist_desc":"Insert/Remove Numbered List","bullist_desc":"Insert/Remove Bulleted List","justifyfull_desc":"Align Full","justifyright_desc":"Align Right","justifycenter_desc":"Align Center","justifyleft_desc":"Align Left","striketrough_desc":"Strikethrough","help_shortcut":"Press ALT-F10 for toolbar. Press ALT-0 for help","rich_text_area":"Rich Text Area","shortcuts_desc":"Accessability Help",toolbar:"Toolbar"}); \ No newline at end of file diff --git a/js/tiny_mce/themes/advanced/langs/en_dlg.js b/js/tiny_mce/themes/advanced/langs/en_dlg.js index 9d124d7db6..42c9a13c85 100644 --- a/js/tiny_mce/themes/advanced/langs/en_dlg.js +++ b/js/tiny_mce/themes/advanced/langs/en_dlg.js @@ -1,51 +1 @@ -tinyMCE.addI18n('en.advanced_dlg',{ -about_title:"About TinyMCE", -about_general:"About", -about_help:"Help", -about_license:"License", -about_plugins:"Plugins", -about_plugin:"Plugin", -about_author:"Author", -about_version:"Version", -about_loaded:"Loaded plugins", -anchor_title:"Insert/edit anchor", -anchor_name:"Anchor name", -code_title:"HTML Source Editor", -code_wordwrap:"Word wrap", -colorpicker_title:"Select a color", -colorpicker_picker_tab:"Picker", -colorpicker_picker_title:"Color picker", -colorpicker_palette_tab:"Palette", -colorpicker_palette_title:"Palette colors", -colorpicker_named_tab:"Named", -colorpicker_named_title:"Named colors", -colorpicker_color:"Color:", -colorpicker_name:"Name:", -charmap_title:"Select custom character", -image_title:"Insert/edit image", -image_src:"Image URL", -image_alt:"Image description", -image_list:"Image list", -image_border:"Border", -image_dimensions:"Dimensions", -image_vspace:"Vertical space", -image_hspace:"Horizontal space", -image_align:"Alignment", -image_align_baseline:"Baseline", -image_align_top:"Top", -image_align_middle:"Middle", -image_align_bottom:"Bottom", -image_align_texttop:"Text top", -image_align_textbottom:"Text bottom", -image_align_left:"Left", -image_align_right:"Right", -link_title:"Insert/edit link", -link_url:"Link URL", -link_target:"Target", -link_target_same:"Open link in the same window", -link_target_blank:"Open link in a new window", -link_titlefield:"Title", -link_is_email:"The URL you entered seems to be an email address, do you want to add the required mailto: prefix?", -link_is_external:"The URL you entered seems to external link, do you want to add the required http:// prefix?", -link_list:"Link list" -}); \ No newline at end of file +tinyMCE.addI18n('en.advanced_dlg', {"link_list":"Link List","link_is_external":"The URL you entered seems to be an external link. Do you want to add the required http:// prefix?","link_is_email":"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?","link_titlefield":"Title","link_target_blank":"Open Link in a New Window","link_target_same":"Open Link in the Same Window","link_target":"Target","link_url":"Link URL","link_title":"Insert/Edit Link","image_align_right":"Right","image_align_left":"Left","image_align_textbottom":"Text Bottom","image_align_texttop":"Text Top","image_align_bottom":"Bottom","image_align_middle":"Middle","image_align_top":"Top","image_align_baseline":"Baseline","image_align":"Alignment","image_hspace":"Horizontal Space","image_vspace":"Vertical Space","image_dimensions":"Dimensions","image_alt":"Image Description","image_list":"Image List","image_border":"Border","image_src":"Image URL","image_title":"Insert/Edit Image","charmap_title":"Select Special Character", "charmap_usage":"Use left and right arrows to navigate.","colorpicker_name":"Name:","colorpicker_color":"Color:","colorpicker_named_title":"Named Colors","colorpicker_named_tab":"Named","colorpicker_palette_title":"Palette Colors","colorpicker_palette_tab":"Palette","colorpicker_picker_title":"Color Picker","colorpicker_picker_tab":"Picker","colorpicker_title":"Select a Color","code_wordwrap":"Word Wrap","code_title":"HTML Source Editor","anchor_name":"Anchor Name","anchor_title":"Insert/Edit Anchor","about_loaded":"Loaded Plugins","about_version":"Version","about_author":"Author","about_plugin":"Plugin","about_plugins":"Plugins","about_license":"License","about_help":"Help","about_general":"About","about_title":"About TinyMCE","anchor_invalid":"Please specify a valid anchor name.","accessibility_help":"Accessibility Help","accessibility_usage_title":"General Usage","":""}); diff --git a/js/tiny_mce/themes/advanced/link.htm b/js/tiny_mce/themes/advanced/link.htm index 7565b9ae8b..5d9dea9b8c 100644 --- a/js/tiny_mce/themes/advanced/link.htm +++ b/js/tiny_mce/themes/advanced/link.htm @@ -18,34 +18,33 @@
    - - - - - - - - - - - - - - - - - - - - - - -
    - - - - -
     
    + + + + + + + + + + + + + + + + + + + + + +
    + + + + +
     
    diff --git a/js/tiny_mce/themes/advanced/shortcuts.htm b/js/tiny_mce/themes/advanced/shortcuts.htm new file mode 100644 index 0000000000..20ec2f5a34 --- /dev/null +++ b/js/tiny_mce/themes/advanced/shortcuts.htm @@ -0,0 +1,47 @@ + + + + {#advanced_dlg.accessibility_help} + + + + +

    {#advanced_dlg.accessibility_usage_title}

    +

    Toolbars

    +

    Press ALT-F10 to move focus to the toolbars. Navigate through the buttons using the arrow keys. + Press enter to activate a button and return focus to the editor. + Press escape to return focus to the editor without performing any actions.

    + +

    Status Bar

    +

    To access the editor status bar, press ALT-F11. Use the left and right arrow keys to navigate between elements in the path. + Press enter or space to select an element. Press escape to return focus to the editor without changing the selection.

    + +

    Context Menu

    +

    Press shift-F10 to activate the context menu. Use the up and down arrow keys to move between menu items. To open sub-menus press the right arrow key. + To close submenus press the left arrow key. Press escape to close the context menu.

    + +

    Keyboard Shortcuts

    + + + + + + + + + + + + + + + + + + + + + +
    KeystrokeFunction
    Control-BBold
    Control-IItalic
    Control-ZUndo
    Control-YRedo
    + + diff --git a/js/tiny_mce/themes/advanced/skins/default/content.css b/js/tiny_mce/themes/advanced/skins/default/content.css index 36f38aba29..2fd94a1f9c 100644 --- a/js/tiny_mce/themes/advanced/skins/default/content.css +++ b/js/tiny_mce/themes/advanced/skins/default/content.css @@ -1,6 +1,7 @@ body, td, pre {color:#000; font-family:Verdana, Arial, Helvetica, sans-serif; font-size:10px; margin:8px;} body {background:#FFF;} body.mceForceColors {background:#FFF; color:#000;} +body.mceBrowserDefaults {background:transparent; color:inherit; font-size:inherit; font-family:inherit;} h1 {font-size: 2em} h2 {font-size: 1.5em} h3 {font-size: 1.17em} @@ -8,10 +9,11 @@ h4 {font-size: 1em} h5 {font-size: .83em} h6 {font-size: .75em} .mceItemTable, .mceItemTable td, .mceItemTable th, .mceItemTable caption, .mceItemVisualAid {border: 1px dashed #BBB;} -a.mceItemAnchor {display:inline-block; width:11px !important; height:11px !important; background:url(img/items.gif) no-repeat 0 0;} +a.mceItemAnchor {display:inline-block; -webkit-user-select:all; -webkit-user-modify:read-only; -moz-user-select:all; -moz-user-modify:read-only; width:11px !important; height:11px !important; background:url(img/items.gif) no-repeat center center} +span.mceItemNbsp {background: #DDD} td.mceSelected, th.mceSelected {background-color:#3399ff !important} img {border:0;} -table {cursor:default} +table, img, hr, .mceItemAnchor {cursor:default} table td, table th {cursor:text} ins {border-bottom:1px solid green; text-decoration: none; color:green} del {color:red; text-decoration:line-through} @@ -33,3 +35,16 @@ scrollbar-track-color:#F5F5F5; img:-moz-broken {-moz-force-broken-image-icon:1; width:24px; height:24px} font[face=mceinline] {font-family:inherit !important} +*[contentEditable]:focus {outline:0} + +.mceItemMedia {border:1px dotted #cc0000; background-position:center; background-repeat:no-repeat; background-color:#ffffcc} +.mceItemShockWave {background-image:url(../../img/shockwave.gif)} +.mceItemFlash {background-image:url(../../img/flash.gif)} +.mceItemQuickTime {background-image:url(../../img/quicktime.gif)} +.mceItemWindowsMedia {background-image:url(../../img/windowsmedia.gif)} +.mceItemRealMedia {background-image:url(../../img/realmedia.gif)} +.mceItemVideo {background-image:url(../../img/video.gif)} +.mceItemAudio {background-image:url(../../img/video.gif)} +.mceItemEmbeddedAudio {background-image:url(../../img/video.gif)} +.mceItemIframe {background-image:url(../../img/iframe.gif)} +.mcePageBreak {display:block;border:0;width:100%;height:12px;border-top:1px dotted #ccc;margin-top:15px;background:#fff url(../../img/pagebreak.gif) no-repeat center top;} diff --git a/js/tiny_mce/themes/advanced/skins/default/img/buttons.png b/js/tiny_mce/themes/advanced/skins/default/img/buttons.png index 7dd58418ba7cfe58ae7efdf174e0b223fe3aa6a0..1e53560e0aa7bb1b9a0373fc2f330acab7d1d51f 100644 GIT binary patch delta 2525 zcmV<32_p8&8NC>gBe7Up0)GjqNklW6KNnY z5Dmy?f+RtfI3miXgUZ$l3N2OYxbJnZTdVCj)}{MCZD&hL34hpp-*fJkN9^6-rttRd_kS0lKeglM98q|x zSXg)|clwtWRQ;MY1qEx?toa>1R4`M3mjVKlOJfdD-oUKKN?soPS!gqP#L^XT4aOn{ z<|RX4N|~K3sI#VE=T7#D$4&spi{!Bt^$m?o{QUC{rr%gvO7{Aqylm?0S+zX;7S)5x zYH`$|q9m`!4|np5D1R_fYSKe+^>SWz0Y7FK1ysuqN>D&2@AB2F{;N0@haxPiQCh0& zhw4lG5r0WZQ4t;*Lqq;NdgWD1{6j!LBHsQN0n-H)T^-WjVH5Nl`Db5R>T*R~PM05_ zz&}62x_GwK&EQ;@DGPWTGSzxvULAk)dDV>VGDjevLnadS|zjKJgNr z`bf8#+LA20zq{MdV;qyd)>%W;u2b1cxN5qol8KVq;TnzZb$VIutgWfp%2Ix?b*q1c zp^rpdy`U!2+}vDCtV5;l-Wsivb|!ywlycE%FI3t1UC}6o*QwN+^e&e(+6tmh&IV$* zwKr1BS*PP_Wq6=`xrq%|~+T_C5$Ej&(aBhffVASe$q7B6G zIup%|XhSd0Mu0Qgpy>-sR}42bHT5AGzmv^*wAx7)^Unf>~5$T9%lO1CJm!P4FXMOHKz$z zM=wnfkZ6UWhyVA>%nC?#J^af}srE1Jej#Va#*O+2?4ajYUZim8X-e~b+=1AA=!;Li z{LVWs=YIfLPf;Us>ZKfUYN0sSZPfdXhr^3GA$+*;eW5Y@_20p3+g{(S@;An7+c>-4 zzH{fVD(lz(Woh*EW|q5i=WXKKaHDYeKe^uY^@!eVA} zU?3JNw_3}~%v4||MYYOBD+pBBK=CiB2=GM=ZK74QsnEWGf%6%rzj!7P2v}t?l}eTC zl$E#0Ct|T!SuA#?$iibMaNC?>Kiw?CEKd zzJCvBq=2zAM1~_he8t?66 zfn&!4^f)$wyn6yk5sZ(EV*dOH)4w{O@AEl)KA*H@%a$t8Lb%;&pMYYFvGMV7tDQ2w z^uMR<7J&^Nb)p7@Ek2(-Hpb#JGc#^?{(Oh7_Y#hyQXS{kEm_{u>HP%tcr7sFmVeV; zGlCm7l_N*oN4?%UH-)z_WRu>~d7~W&AKja>6GxBYOY^mRsgg|aEiHo5uHm)287)vU zr_lbNqZGYW2PCP^q)&h2+XLDKi)GP5+_T)9>fh5og-X&n zYuzV%<`96qIK=6}-Q;lqal;oUona*U5Yva9&^5?HCzQCBYQePB^} z1G^QI{#~E1|9S|__B%kDaVhkfLtwn|hPKge|6hpk+D-7KxzMN35)ad9$LArF{=NSG z{!d2}qobpb=6s;j0hH0v{xnBWfu(a4gfKE85W&$|9{#7pap|+sgr+|~pMU?=SC_nV zeSOzIPT)Rt_LyM34W+BD~pYLNtW_A6!EA8z!ZnXCe z{-Cr!8XWBV0fzy~;NTt~fi0#gpAQZWzL(Ahi@v_WS(w3%#Nc3J&U@vYs=s*VZae6k zo>dfO@8IF5}l ze2%BrIIn|UoVTuFde>WwT^d-P#5C>en7nrF8d*jxYW`2>4Sop9r3$7^9D z0|tV3X(>MKmaTjdTktrvwvFjGojrtexg~ch8%eTiL$?HEDrQFQ(o|zg&NduBtLhIO zr!H+##_1SX!$S-&LZ~82VT&b<_XeVjKkdaT)8&MZn3s@_Ho}t76Fe-_-RMhe8N;d)Q7$ zgh%o6N^}7$y zR~4_W4h0it*_&N5iO~AmA~pIUX&|1-eOHt8eKM$d&WQn~arrTISYK3<2LI5tZb~ra4Vor00000NkvXXu0mjf6xaGA delta 2666 zcmV-w3YGP}7|I!tBQpR5XF*Lt006JZHwB960000PbVXQnQ*UN;cVTj606}DLVr3vn zZDD6+Qe|Oed2z{QJh35N0)GktNklOuzigg*>~3$W~EbK zkJI3gRag&Pk;36h;(u!ibKpZf$s%$Un3eX0^;fTCr3YA`hg=wxWdVv6yUWeGa`md^ zYPZydB%+pJHp})s^~DyqrMS4T5Pd~@dMsJ!Oj3(2J%GGL`1xNDC>B(BwMYI1lfb@) zTzj)wuXpQpdJ9~Ee*nbvVdZyBfC9gXDU5y?>5fdiK8l)c-n4+B;1rlzK9#2!fM3|C1>(w;0$9;EYl!lG3Qi{9fw zVrhld3VXd?=YR169vz_+(dP}js|kB#^j=K3^ni>w0`h8!(x#?9g}qC!cX=BCuM6&( zm{HZxpeuFbq|$0R$Ae@IeR~u%VLT0CqICm0PlIHiXU^_(xm;m9ufe020DG5mHvqWk zv8uF52_Ex?yhN-=D+`4b_He8EJfV;`4BcMHBKDz>n13aP=+L^rCKL*n%DC}jfrgM* zj~-Vb3=*%x-h&8rgsVIa9UY-C+KkIp)zOjKN-D+E(b3VTO9zdHI=n;%MjatfU&@~N z!#M213$q)l`uecF*CmlkfbI_0Z>#|qtPY)A$N-G8753)rKWsDEX0yWHoW0$crGIJn z#nJPOdwtjmu*fKZ%u>cXgG^6d`tnH)&sRp%WhF|=p(lZ|$`~53maBoH z-%@TRgV0nnG@}+l^J1|}>)3v2-fFed9Fs^S%4AB)nz{2npRdH{JKxN49BoCRYH@$W zp?{o#0q%5JIs7UqY368gX)vp#Y<}rDwr^itVY<4S*`KWv351npCs7VWCVpmtHLEXu zgA50~9E00sJ|D+6!*ZnMSyvXvi2Z8Ld4^_ha?A+2j12SC`joL+yT)&F2`hXO|9YNa z*qbWVI0T^SJ`BU%+!()E`5c@1%FZU(@qg22pO=Ztdper4`U2YzuI5l&vnHuBOb-uF zPos-DGCVvyLM1h$3^2-!T1Q7KRa7#WWNC4O22B!vYlf|7&sx#<>?r6Bqo71nMn)JW zXLS(UA79NenY1R8iEU|VsRWu421C+BkuaJb9vK;-HArJh{(D@bqHt4N$HFYWvR_B)x^!$Fg+_ z`{WO%UZ9OqsWw!Id#Zcm(cU%)JAcDM6xkEU)v0D5_CCZ?wnbbe7$VoYFM4M|%;M#Z zkKa=%?1Mq=>``DA?q2fIqem1%I66bA2rpl1L`+99GgO_7II?{7p{Q1!g4w*n{(;F9 zz3u>JqFNw{S$2G-#W~ceQ_@6@=6^`$5Ht$N(5&OjQ$+qKnbdymP}qMIjekbJ9108# z4LzPk&qNS~@IymU5|pGwz$FNxA&0ar0v@vs%@FZFwc6#ch60lPrPZA8zFQV%Ba!Q$ z2jCz?AD{>mABo%u27~f$#FXoiNTioa2Ms=s%z!aWFqu9F$&75jxYE{k<3?K~{tMss zI3ADuf;JP4^6_{t&ItyUDSuzbA;{V5}yfT`Het49+(wY7DfA|`Mc;*i%iY% zJbyGkHT9SmAYu90l#T3zM4$`yYck1CO+7QEoPR~#FdBU}^$>IqqBbzN&M)7cUm}0C zmtwKiyWjq4Tlx0xYAlBBFJFS!$^1&rwz4u0uag(>I&rT5c#<&IK7UBw$JdSzwzm(C zy@10B!n?p>!u!{t`|$gi!xAt&2Hj|0``EQ>*O1F#`}goNoV@Vrv(H{-0C0s@d86~f z`sMWuaC!X#6e1Zeqtt3TQ}(o4O{twvP-&J|k+(mMyb40z{=L7CtX7KmKXIxl+Sa~WQrB0pi^&f|09 zya82dI7=<>51&YtmLS~*j+Ryi+Qh`}R%~CpFai5Au!clBEPqQ&O#`aZI25KttkOis z1efk`Zb7u4IESjV4GwmPfid_p&J9l1GOSaja&pS)3G6@e;EDw5##NmZJ(A5x{!DLc>`uo9qQq30%Q$+e$2XEbV!Mk8BEAO(yeX`~ck zG*oGzF(xS|s(;c@Q_H5RG+3C?$caikUABMkn2}UztOUAam0CfY%0g_a(o)e-rLls* zVi{Q@ckDgcUZv#`lt$ykr3KC~@`9KIS!7!jFC$fHB9)aeM#SP0752#=jDCQZt2B*D z<23qtrM&atqW=vISyu-0YmF9m_mF9QYPRUh|*6$cNCff^OZwnwCe*8?} zDlHH&Zm!a_ShZD^7O=@h5AO&BWcwon=vMjdc84th2b6{2?RFmLq*rMUyWQ^JqDphv zZA1+#wSU_Kwt*u`dmh~UJRd-l1<4+7h3rL6fo0f*%~hH)0QO`acm(`{!hRRBhjBPU zvfaT80=BmqB~l5{Z8xS{<26faV!-c8*&jKw>zhArE8pw_Q(|8Mt$|(J%mA38e`(J3 zC*o_kwYE0Zs;zBB$Gpu{Y~792WS(eJTl)lFhAXt~RmOlB(02E9v#?C$X5eP{z{CIq|Noy+{K>+~z`(?y1LA{Z9GJviddkHn8j1n{4_XQ! literal 70 zcmZ?wbhEHb{{R300000000000 z000000000000000000000000X`2+<305$+D00000ECE^oKnDLnl0#7_8jr}Na>;Bu zpU|juO08P2*sL~N;aI-luy{-^o6qR9dd+UT-|)D6PL~%bkGy`*@B9CNfr5jCg@%WS ziHeJijgEhf1p$(il$Dm3l{K21oSmMZprN9pq@|{(sHv)}tgWu4k(jcxnXk6DxVgH! zyuGeAz`?@9#Kp$P$jQpf%+1cv(9zP<)Ya26Dh1Zt+}+;a;Njxq$|x+_0PICJXU$+M@=|DQm87Klf*sL`WHlPX=x zw5ijlP@_tnO0}xht5~zfqQw=HudiUkiXBU~6~nV=)3!^{fP~t&aN|}}@q(Y+yLj^k zYq5XAuHL_Z&+ZFMxNyS3h7&7Z%($`RWC{+>)y?~x9{J;g9{%XyugJE7?LYr&b+zv=g^}|pH98H z_3PNPYv0bjyLavdR!$gCzP$PK=+moT&%S@X`}gqU%b!obzWw|7?K5y7puhkB00t=F zfCLt3;DHDxsNjMOHt67k5Jo6rgj;oG;Ur~csNsejc9{Q4g%?`r;fN%bXksrQhRD^4 zEVk(4i=L#Y;*2!bXyc7I=BVS2Jobp=V?YK8&I}7EAmouqPLo0lekJMTlf;w&Lz90{ zR_P=PE>LOZmO%>O00UfxnIvLjmiZW&W~QkanrgNg7@Ka!Dd(JY)@kRRc;=aq0|XcV z`m}aW!rkr-_>81sY;K8V*mTKy$ zsHUpws;su^>Z`EED(kGY)@tjmxZZzSfNK@>>g%t-1}p5a#1?DpvB)N??6S-@>+G}8 zMk_6}1$=Sbwb*8>?Y7)@>+QGThAZy4TttntPickF-h#~^!L zz<3mtZ1Tw{r>yeIEVu0P%P_|*^UO5YZ1c@H=d3f%TlDPn&p-z)^w2~XEd_sBC9U+* zOgHWH(@;n4l#DwLYW3AvvqH6wS$FOA*PLvfBiLk@EvVQynyvQQC!&2L+i=INq1!gf zZTH>2&|M?meE0o|-aZB{_~3tp7jF3BI_jzTK?o4wz~Yb()PW3IMsE2#F`!WS<(#Xt zeOJVI_t1MV?tlC*KSAq?EnqnCGNaK1848R8}obc#0N8c z@x&jGJn+gd&wKOE-wu8BwNFp|_1I^x{r22<@BR1ShcEv4;BupU|juO08P2*sK-{DI5ae zuy{-^o6qR9dd+UT-|)D6POr}wh?;)S@B9CNfr5jCg@%WSiHeJijgF6w1|lLhBbJw# znVOrNot~edp`xRtrKYE-sj91}1~(+Iv9f=&w6(UkxVgH!yuH4^z`?@9#Kpu0B_PVn z%+1cv(9zP<)YaD4*xB0K+}+;a-UcBd9p&cd=;`X~?CtLF@bU8V^!4`l`1$(y2IUw3 z00RmfNU)&6g9sBUT*$DY!-o(fN}NbhqJaw@FlgM!v7^V2AVZ2ANwTELlPFWFT*-g3 zrOTHs4QR|jv!>0PICJXU$+M@=|DQmE3LQ$csL`WHlO8odQ-jl|P@_tnO0}xht5~yY z-O9DA*RNp1iX9u)fCLE>(yCp{wyoQ@aO29IOSi7wyLj{J-OIPHU%m#?QW#9Qu;Igq z6DwZKxUu8MkRwZ;Ou4e<%a|=2*sy)y?~x9{J;g9{%{ytwh>$dfBy&b;|@1Gp_npH98H_3PNPYv0bjyZ7(l!;2qJ zzP$PK)+46zyJRL1_8GHE8V0AGPATfCs76X^sZplt>Z^dR%IcG_ z)@rMqvd((zuDp7gE33T*D{LLV&T8zj$R?}ovdlK?EUU#nEA6z@R%?Ilwb%-P!?xUZ z>+QGThAZy4w0tntPicT58d7HlKo2~ZR?V`{^+HA)y_qZ>-J@?&s=dJhN>ka^c1AGUL zHvxVNZg|`(!hQJSOFyYN*03sFQqp z>a1_Pdh4)D{Ce!PCp=4g?YI+sd+xmNyL<1z+xvU)#J4+q@yI8y{PN5<@BH)7M=$;K r)K_o)_1I^x{r22<@BR1ShcEv4aEJp$8AWOiVm@@lvn$rkBQx*((~wsDsti)R;g_q_hM}13`8R z%WhE?HvHLHm=)%o_xbs5_kGLD@ND0HKK6+LR9qKlBIArR%La&c!H|Fanm&VkVwq2b z6veEvozK@1tz4~PDOfjs8H#3Dwd(eAxFPokvl|z;9<9QDxC_4zt4&aoeHTKh8Fo0 zLV@66`D25_aF_4Ff*CyQ?z1f#&OoctYS5z5qSZJV>lW_Q zFCkZ_z~Nz8VOo53`hS6V>ZLT9eRgHd;}d*74}ikgBzqaP+))Se~}=;ILEfx$KtWs zfF58lRUXlUgGb=tQ6dTynv>A{0|oZ9jU!aL5i{Za=t@8wX^aDeei6KN@W{cpB2H1T z4Nf;Fo2Rz{vt;aS6rnM6qo`|Dx}b4%&q#Zn&~Gbn?%=ttFiOE?;Bxc#Jj5~{Bm{F$ z1xb|5jRtgsB$|I2>}pLD`puAmmITa$^+C z;9<7}M;k4mp+{+;0mFpGz+!`!Gdq*PR#0hLR8fS2eOlG{wdc|7bat~+}#x@QKK2OT2!9E0};PT@0! zkHg2_d>wyA7VeHMzz7S7GO{qTfY+;oWR6qt10MJ#8Te7<^T-F!76H81n{ zMH^UkZ4X5exh^?7GGCK+jU`#PR9zB<8tocu7-rS*Eecf0DLFf1uETPWbf6;vJv&dZ zkl=ObT{3$qay-gz3PpLCD1OI#WR8EG0_9Ow$7Fwc2cUd74v2~(tYIUWTNDf=* zgmFMccYq2Ypm?d08Kh`{Tujb}F)2|}f&!wfB1AQY654d6)%6DH136EeIK*;LSbc-Xv#(-Ok`JAj%3=LGaqRKt?EI!@J-_hZd!_ zggBwr7R7JglKJ~C9vHOb2FY2y08E?*CZ>N#O}t0ytN1uWW`u$p)8uZDvoo8V(dkf; z4?}QfIuI57bORZoKrA^SXSwzTJ|+rS6_mB;Eml3bpMu5|xyEZ`>}`w0-JgCGG>Rz&zY zR>@Z=V+S#$oCqWV4&#hV@X{}mag=|tfWv@*3!K3?+@so@qV<1eCE|Qi^~|*EpkUSj&0=o+V`{W6hG*+M+5bL)?F%wm8IE z7AzS<8T%eZ6X#j+B3>J>k{P3n4N-9hCqio4s!;^L`EfFH6O^zYIHOK90CgBuBRJiM zmkY^kql{IP1Ocj&7~?P~s>a}@hRlDzObJV36x0BM3aDc(dWls}7|Ph6{!EQS6hu@= zIo9&Fl6mI@C2ak-D8k$*Dxe?&bEA6X6h6oJ?EVk>E^TIJem(5~0000eq7#CI)kVp&dhaEO-g|J7 zDA`T4Z}0ccIcJ`k`De~~&-2bR6AarT?|2XZsh76JD*zzB|DWRlxgVGTfXv7R0(tVp z+0)nam9wW8mktEN<>lk)83z#7%0lEzGy2ROI@2C&ebFA>fqVT$WQy=M(gwX?PLCNZ+fSM*=SbjF2 zDNslN*p9xi-v*wD0Ji)H#NG@LmXFN|#se(Vxf$>ZQvt5~j&T}*vm#J6`844%022r3 zo%H(TfdvsjT*ur=8~D-$bWPBZ*8?OJfVgp7v>-qb4A>6w@B{!+IRL%pnYq$0p&FWf z5Uy4^^-5@7`N!dwBqH9#=H{ZD0uwq+LiDnB_v~`jM0)}eIV_Ul($pL0V*pT?%7DxF z{3c+6s%BzBHW@`_BXZbDboapCe*1QJveHWp05*L?r*4JC8d$>=iNf4(iyxj4I6o&Z z^ur`N)l;i|1qu(A%+J04tIp%1cj(2%-JP8U-9B|oyAkt{TX>I6m-)@hKcPz3n9Gfh zE#7cRgmt(U0d}Kj>_WeUeeykNl>N$Hiq>@#`Q0_g%!581do&D0zimwCove|YbS_!) zAYT2wVE@DEJBO`x;VXQVbs6A`s|sFlsz{mj3;f@D{4_A%t}P4zoV9v(e-|LZ4|9&( zobAd#_fae;#qR^2B)j<*j0B973gT8>-F1ne8yNKAk30JzwuWeQWb{)$%yNiHZ6(t@OoqA?-XAl=rTFhD9vK>Zw|f^xhaf=dEa~*S(TGyi}t@& zI#VyDs6YnAyM8beOZB}J7aizTuX`j+n)YI}?m-nr--7m!XI~$ z$`bgibJ=l%wPE@&!v*a!E_PmN+9gti94UU3aS#84v@*R$z82a)ea!Zf60tCCVV*g< zl>1^d!Eum2IbOPWh$KJy-94SU&(LYQY0YU56P^R%@;qs6R-UxsB?q^9V)=AoE}^05 zo&25N9jP6*9quzrvV40r@6TA%Edi4eS6!A}nqBE#BFAh=u-T_vlhv{!D6I@4<(YbQ z5z3_2IJWG*v|q0LcqF5aUP*Ov#tf@7lQYJp_J?l^?P><6Vb(YE>CpqNA6Y-}FZ?Me zV;CX_^RDZ3A57FIXA^gt^@)D`Wh^W%~H)#uwkb2DN72aI_{WSCkjY zL!@-2%o|M3)16d_x3j&mAAAi-_XUty=b;H}^rwPbf^4PI`e+G9D>p<%W>zMwQoGW) zQtPN)w&oa_U)uh-)#56g3OuOGZ^f?#PnjUh-#1+}T}6^184Z*bE^9<9+)CJrS~dJT zJ&-5a1s%`B3iLgZYk}?if7wICZlE{rf03736fmL&3SOdRiV0?=ei?61B@VH{s~cTiCuH8Zo8xX;$;CCUug_fYmndw)=qlz$ZQ;K5jlegF}OpToVIysbHz1 zzM}r%_}F;y_*{-mp0dQeip`5bFI~_NLnP?FwL;CU=H5<ao^gb}YV@I3c&f0LJMrN}HKhL{^FRy^o1~dsklIGw9~JC# z$v4fL(SkF<(XS%U;}YZbB1T2CR7CUG23CjKcXWL(y{$KcrZ4mJ8uEIM%a&_TWKUd9 zVpl$nNq-b9Kv+n<`qn!H*)ZOnQYHVGA?~(b6>0=A3WtB)=Mj4$mTF!-|D~zKzXD@- z%B1pJ#pmO;%C?f+n&g^=&6r$Q(@@i>QLkr#P4qeh!E@(u7j!oTAj2Nw!SUn6!Qqzo zuCPIGxI2?o6_y=;*DoG}lM2yb25?G5b;NFWHMjE+;@@}G+?f6BiPQ-fs^}-7Kjyv> z+wIvU_J&gMN5@8+$EYTI#lcvoB(h}ur&b`Z);0cWWNNJC!6l63YejTrKq?iYu1ZUC zEz&Kj1ENkAW3#Y)n9fT*c6m`yjjYGZj`i!~^VqHYE%NPPrfjhxYp=kfiJpsSBxAWK zov=bCLQOx;@KO3tFb$}TG3nviegB8aWPv>#J@5x&Q?gTW3lJ+1r{S44$ROzbzo~69 zfxP`vCBnPtDE{(yZTgh@t9c3qjJdD5+$EJ2+9e|2r@r%6ui!RFL%|Q>drq%Ms~n^2 zZ~HwGo5VOXh=<)8-c=D=h>)SwOMgJ03Cx3t5%q9KbEnSn_!0Tc$^xPBMM(@qC_Ya#`fRGAf7ds$Q-l#5G^lI+dE+9FfBl2$8vllvhYB^1{yxhwtqe6unG-tFq8j|C z<2L`)UxZSY((LVrfh`^{jli7)4Cy+ClTu;nby9HECh^QauQ}ixVy^1BXSterrny&^ z_LeP1V=7Z~higICTFd`Xe~?zP2-H1?4!bNJM9ieY&nA=dGx9@1MgOeWE*GPM5&JJD z)?KMRPg`hwH>V%pCGIP>DNb0OHT#~`FYn$rZ#K`ZRj_eC^gUF%O`9O&vE)`o!V&U4HrV-&iPN>rRg{1RT@iL+8qIY_WAV-RVk5W000Q! zU;{7!)Fr>UGET#-xn5|S7yv-fLjVYi1b{zxxcLA8{KNs^&=vp`vH^g}^Sw>KW*s*x z0fKl>M*|EESW2HqSh2x5x_;FlMbw{#z?!Xxb^P!U?hP=H8YyOQF)d=+$Ew@rMHTAl4iHTYE0SO6V9J(;W z9FdzCNXf?h_xbsGUYkpqt!JJ?<&{;V!(p`Ol ztyDSZw48y5&^SpGzve4rv!w%9=+J5q??}9UWBi1zM;-oI$1r!kr0qvI7TJxiwiy^0@Blyn z1On-He{7*#jFD%>;Wk$h-5sa+g7!&*L!#!x|2Pp*- ziXSEKv3b!yrWELqtE~GCdrD;W42LBsF66$BWPYppDOHvg7il95oYIEd)*WOs@-TU$ zqXtf#&Mbz%tO&h zO%^eZh#hx~9UU1N8#cGdK~pSO2cR>C8P*7_%>0@wKxiljqmAceQ4(~oVJqL~G!tgf-vDN;bl50PFhP&uocVxE^353}ii64% z;SLU(tWWX@THZ)@qF&Ev-EIQ#4lOZh1XmnganvJi8Vyn~4A+{H8%-tCoTWh_HHNjL z<8ucMmuPg;q2J-vpX=koE%sefzd$!kpO)hc4-C%oeU zq0vG9#~9u2lThkyJLb}+_N)U(9M##cV~k_+6w_qnkli-vksF}{GZO(y#|6(zvs~cJ z1KV~=oDA5;?{aq7Zm}Wd?1+Cn}}Hp@*-OkIBN?{F>5 zV4T+}m6zs`6#gwG3Nss%BM7jUr}%}o06iIDWC}rKx2*s(84-k=0qb2(#q4q0W+m0Q z{RFoc9Wg9JPZ6zNR4BRfEBkx7G`7-*W<^dGhu6=oCiOm=ZRnJHI`&JWeEw1Us=)tPEJlS1vxl`Z%l1&Ho#+JV<)MosZ}L}ODJqQ zxcmeuZxL^;6qG(tsd_)9?7&Ku>oBWaDxysV1CsLgoBsqRZ{Lo1vk!WcpOce+ki7m^ z>~m-}W$fPGo)b^Q)S|XDT~6Lxjp9_awgyXwm+>J1#mC$Bl>>Tqb`}c)M8TC{5S}$9W_m=nf z@b&eDvV{IMsTVT0#iEfr1(BLF-mUG?sF~l4OKT65GcllE>JW>=wMI);SAOXdYC3|{ zEUmcR-CbIZ=LM2)#4~=28}QQI>G0m>Y4i3OiQzs$edJ^YgXtR?E$(j=H)IoEH%x3=SpS zvFp0!U8d{rbHB&TKb@b$*==15C=_OGZE0y1uDA=nwRR6k-46`?1)4lhRGPedyB}Dv zKfB?{e>G9txEWwYLjaCL+?mou?GtiyGHt0}2WCuNn3mhD(X*$Jt6R}T1srM-s)${_ za(_gckjT}y9l_OSwlNWoYbF36-^jIhRBu!dXSgwZ`?amH3bh@M#|o&e)3aMz#P>h7 zN2K)I*JWfZ=nA9i=}iA@8&FYEO^0!^vtPLMn&a*g=xvR#+6lY`ARvA62(r!>rz2ANhRI*qH;b7AwQ7nN8@TH#Ws&GcSz85IM7W=p+9@ zRI74y*H;0$iA=)}@wHcH>)3TL_cxy(uBoX>Tjg*4KU!L#8?ha{ezmgtG#WajIMLFG m$9hVqYNgZs*94epU`y6#e2-qmp8{7)K diff --git a/js/tiny_mce/themes/advanced/skins/o2k7/img/button_bg_black.png b/js/tiny_mce/themes/advanced/skins/o2k7/img/button_bg_black.png index 8996c7493e8a58c9c40845cbe8abdc3e6730716d..7fc57f2bc2d63a3ad6fbf98b663f336539f011ec 100644 GIT binary patch delta 618 zcmV-w0+s!k9g78!83+Ra0086$;ro#x7k>cEP)t-s0002HySo4Y08>*_a&~-=k(7~{ zq?n(no1(0rrmv!@vZk!Hsjs-Lv%9dizO=c)x4gu=z{b14$GpMFzQf7C#LL0P%)`gc z#L3Ub%FxKn(aFuz%g@yRt3Us-P5-h`|Fl&9wO9YQTmQIT|GHxTyJi2qY5%`%|9`=9 z|HF3w#e4t9fd9#a|I3O0&5r-ub^qLY|J{H8--Q3*iT~k_|KpSYd|L3Cr z=%)YbtN-h;|LwH@@45f*zW?&W|MbWI_00eO|F&Wq@&Et;0(4SNQveU|I_8w5<1)+Sxjy>khxmTZ^UMAPuDhl=nJ%xacLVXE zN31WKSh0P^hawhnw)jf}#5_+GPu2e^5c51aiV00%6xVrr#$!*$V|i{OPJb2GYE{RI zUk${E_f%7X#d{l5O*06LqA`1W{A{z-KcE#qKx)lR6a_=R{wHo}S^x<1c*Pfemr)l5 zdENxVv+rLvA{OzVh(9zy{H9446TMvi`r@7X*kwhrq7{r{OWVwhm!6C_^4vrmA^ryB ziMy8mgg|d`WSser1qE6f|5sqVKRBqtZ!0b!I11C^6r?oOYvK8FsKs4z{;LqNh=&nh z+Biyl2i(R3MA5bJ5E*aeRfzb1#Gw{<#77AJa-hW>&Xefx-T2hw#sB~S07*qoM6N<$ Eg3||6=l}o! delta 3727 zcmV;A4sh{{1(+R>83+ad007h25y_Du7k>&*X+uL$P-t&-Z*ypGa3D!TLm+T+Z)Rz1 zWdHzp+MQEpR8#2|J@?-9LQ9B%luK_?6$l_wLW_VDktQl32@pz%A)(n7QNa;KMFbnj zpojyGj)066Q7jCK3fKqaA)=0hqlk*i`{8?|Yu3E?=FR@K*FNX0^PRKL2fzpnmVZby zQ8j=JsX`tR;Dg7+#^K~HK!FM*Z~zbpvt%K2{UZSY_f59&ghTmgWD0l;*TI7e|ZE3OddDgXd@nX){&BsoQa zTL>+22Uk}v9w^R97b_GtVFF>AKrX_0nHe&HG!NkO%m4tOkrff(gY*4(&VLTB&dxTD zwhmt{>c0m6B4T3W{^ifBa6kY6;dFk{{wy!E8h|?nfNlPwCGG@hUJIag_lst-4?wj5 zpy}FI^KkfnJUm6Akh$5}<>chpO2k52Vaiv1{%68pz*qfj`F=e7_x0eu;v|7GU4cgg z_~63K^h~83&yop*V%+ABM}Pdc3;+Bb(;~!4V!2o<6ys46agIcqjPo+3B8fthDa9qy z|77CdEc*jK-!%ZRYCZvbku9iQV*~a}ClFY4z~c7+0P?$U!PF=S1Au6Q;m>#f??3%V zpd|o+W=WE9003S@Bra6Svp>fO002awfhw>;8}z{#EWidF!3EsG3xE7zHiSYX#KJ-l zLJDMn9CBbOtb#%)hRv`YDqt_vKpix|QD}yfa1JiQRk#j4a1Z)n2%fLC6Rb zVIkUx0b+_+BaR3cnT7Zv!AJxWizFb)h!jyGOOZ85F;a?DAXP{m@;!0_Ifqlp|(=5QHQ7# zGr)$3XMd?XsE4X&sBct1q<&fbi3VB2 zOv6t@q*0);U*o*SAPZv|vv@2aYYnT0b%8a+Cb7-ge0D0knEf5Qi#@8Tp*ce{N;6lp zQuCB%KL_KOarm5cP6_8IrP_yNQcbz0DW*G2J50yT z%*~?B)|oY%Ju%lZ=bPu7*PGwBU|M)uEVih&xMfMQuC{HqePL%}7iYJ{uEXw=y_0>q zeSeMpJqHbk*$%56S{;6Kv~mM9!g3B(KJ}#RZ#@)!hR=4N)wtYw9={>5&K zw=W)*2gz%*kgNq+Eef_mrsz~!DAy_nvS(#iX1~pe$~l&+o-57m%(KedkbgIv@1Ote z62cPUlD4IWOIIx&SmwQ~YB{nzae3Pc;}r!fhE@iwJh+OsDs9zItL;~pu715HdQEGA zUct(O!LkCy1<%NCg+}G`0PgpNm-?d@-hMgNe6^V+j6x$b<6@S<$+<4_1hi}Ti zncS4LsjI}fWY1>OX6feMEq|U{4wkBy=9dm`4cXeX4c}I@?e+FW+b@^RDBHV(wnMq2 zzdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i&_B8C(+grT%{XWUQ+f@NoP1R=A zW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01)YTo*JycSU)_*JOM-ImyzW$x> zcP$Mz4ONYt#^NJzM0w=t_X*$k9t}F$c8q(h;Rn+nb{%IOFKR-X@|s4QQ=0o*Vq3aT z%s$c9>fU<%N829{oHRUHc}nwC$!Xf@g42^{^3RN&m7RTlF8SPG+oHC6=VQ*_Y7cMk zx)5~X(nbG^=R3SR&VO9;xODQe+vO8ixL2C5I$v$-bm~0*lhaSfyPUh4uDM)mx$b(s zwR>jw=^LIm&fWCAdGQwi*43UlJ>9+YdT;l|_x0Zv-F|W>{m#p~*>@-It-MdXU-Urj zLD@syht)q@{@mE_+<$7occAmp+(-8Yg@e!jk@b%cLj{kSkAKUC4TkHUI6gT!;y-fz z>HMcd&t%Ugo)`Y2{>!cx7B7DI)$7;J(U{Spm-3gBzioV_{p!H$8L!*M!p0uH$#^p{ zUi4P`?ZJ24cOCDe-w#jZd?0@)|7iKK^;6KN`;!@ylm7$*nDhK&GcDTy000DMK}|sb z0I`mI`%#ks0Dk}=V@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z000BCNkl%`Zo(Y1Aj?R|BwL*lGFS%byy z6<_PB?8gMKt#y$3n#Pv>Rr3?i;C|+)$6!Au23uMOnXhSV*-V3Q@xhOu1K)$MU%g`FFW0215ys%%osV6|Va)Tq5mdhUN6U~J-AM~#pUO%r%hKW> zB7eusX0zWr)GVVYyceilo$(Yz}V4nP`(d_C!HvOqs3Ng zayGsi@d6C|wnfB2sVXyr<6toNU~muS7Jp%5!G7Wx>|A`%&Y)*EBQp3H z2j^7#-9YRw^5q6F@uMxC7i4t%>0n!!hpD9ts>D$Uth zU#de?8&_dbEojxLRFxCA$%)qMsCas#?K7L3ocQuk8=uZsWj;k%IYs!L-$f*s!GD8v zieLt#8SD=Oz(@w;URN@MZQ$V7nZbQx@b0(d0Q~o0U;$RD%K5zvB-b1urssJhJBPP$ z>+c;#%1@;#JE-3(oBmc<*HFJzMwzRv(wbTrRaXZ%Z590fKye~-XQQ}NT^<@TI z+2lmEK+fpwvFbapEke{2MBvsQY@+%ygNODMkt7E5DS{b1R6Up(%q@Z$j0YI0)Y`Tr tDd0rp<~3DXQ_F!8)>XlC0|M~B0RV7?*50eEtCs)(002ovPDHLkV1i_X4a@)l diff --git a/js/tiny_mce/themes/advanced/skins/o2k7/img/button_bg_silver.png b/js/tiny_mce/themes/advanced/skins/o2k7/img/button_bg_silver.png index bd5d2550c06d83c1141b78c5af7217e89559fc32..c0dcc6cac2003f7405fff3ea5df7bcf276243596 100644 GIT binary patch delta 2072 zcmV+z2bL86o&f`_+2dMjNDlznH&TyHd_URN{##*Ml(GE~%a^)cgqM3rM6 zy{bondNjd$e}A~n3(q}gq4ulxA1GMAU&Onw&G(G!`w1>rDpD?2qyj2sDOZq}vmEsd z@}xM$9V2sUnzinX>{lh^hXjQ<)2Nkx*%9e)zkkSD0CoV{iZJ)DasnENC6 zO3eKu1`Fo?3BIrvYh2$;@cjI|BpLJbiDbD&l4LPSRIcEBvIVYz{%C^DeJ19f%sg1I zUXySwx9qiQ?A_Q?s9^K`6JE!_ z_bH;j`G1t4avR;FV7+1Waz2(|Uc;urnCmH`!L@zx6WnSxrP*vrv(=JThRQ)R8*>hQ zYQ71wvFg#UXH>6?^{X=WplEV0ExjMW`9Q(y{i(G;wl>F@)APzl%kNj^PNyxMPDk49 z4x$C>ZH`GBSan^CCfK|VA>41MV6M?-$Q72-@G>9egXySHG$4qGvBl01NZ;Wk5IlbdYjLYHBI{RmBx^I0qP8}!GuogJYk_!zN2?;k z|>^JhLf(MIk(!X*D?q~AKKA_i6@c2i;>U72h zvW9WSnu*;0QO^O4*~dEac!Gy3nZTYC^nc|OJm-VIVBBrPr70OKP9f&UTqC03W&Esn zg1&r$XMOM&jBoqFjW1=Rb4L0Z8{5Df=WS8%`SX1EkC7(aeCzViXS5bXT0 z2Y0X`bDP|q>h>IK@dQKM?V6iGFQ4GmFMaSAjBn?D{e;CvrjbbpH> zSZYT^etAG~1(C3BM?2T-$2jhfh{bQl9C+a~3q2=)T?-eCKNy!z0Io?;*Pz!=a9Vwv zk|v7WZ;3Q>ikAq_KzC(jyDTxBt>P zqC_AX^<0AcU6Jcg6Rxes1igNO1%HB#yVN1hs1uGO3tlT{0;dchz^bkOo>TB)1FX$4 z@fJL4d?<45u`#3BM*(u9hDaVqbP-Q5?#$)y-K>@K37&i2hg^a|xM0@N8(;9sjL6Mt zH|t5bU_9FW@rn=V^%IOos`TpNOv*EeI^P_#_N>k=WLEVf0@R`jzIjpP)_)~8>(yci z#xDlx+^gjJD>70sgX1hm1V$g{P-jmx!T9!HnQ=1eH*yGG6iMFnW8nE0u<=`0n!IH& z%5c2D`Dk-1N-F?tx)JaWF4V-Q(z_RnP(IMpbK;H$@Emx17@r^@NAX(zIt z6hrXnr`JS&eq9i){L&j3gULPiTx7w^UMJk?>Rd4d4}W-CMi)#}?T}S7#@|21u}O5n zgI>wO*OIQx7DMphi+=+GAY~h>er9{^LFRm|5z!O#$Y`T|%E|EN<>CouPJxzizZh3= zDECwMgVgrwg-IR-gkeh`tcmwk2}cXHfQ1*&W$KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=00004XF*Lt006O$eEU(80000WV@Og>004R=004l4008;_004mL004C` z008P>0026e000+nl3&F}000UJNklsV|cYlY&q4zsSqY(=s&=iZ>cW|Jxv$MmG9zDvBot+(KW~|_7TD<6H{cAkN3K|aG zcrux+oO=g@0Ya4~{cSpH8q)S^C!RJ4&uZ})*9T~k*=eimkK)R(5Tt1);2fRB?TM})FJ3}Q5GXUb8Lk){FF_3Wx z=iJIUQx^{h5)s+dB4+IF?&d(;*xttO?zotV+(ty0F%iZKw08@d%-%zcDVt0A1Sf#Z z!$i550O_}rB?3rfgC(+rO4veFh#~^8Hy-20_BQs$V=WfUeQHPBy1+Q2J>DBT_nOd; zg%BBtmQXJ}Okfa+99U7I2a5MoP!1ME*2}cE*xTO+2~p?`YNwur5G5ud?C!LPP@KOm{BB z#+D0KgT?0I#e@*i(sz&zP75$15rJ3S|2QI{O)t_@pSjQy1b~?`xvW2-u@wmfCWc_c z)13c;fEO-bc3xDzZd;8l6W9w42 zW<`}u<0ZaeC@-Kijls+P(6(Jz6qgLJDBi=ii9)_II9f8%T4>=@up99}kB^U{(Z*SusdxZ;9thpIi5|i%i*z7$Q42&#_|A+Sh|kzjQ25$CH1E&}eROqug=T&4vm5K@rO%+N(%@`EF2u{BU z2Qnvsn%2zW93s5e7jCOIXrWnutK12sf9DVsIqQl!K zhoN|xb6z?(C(OXNeio?91RU=H0IvOZk=t(m;#^_H#4j{gps5xS*M76PNc)#Z#YNxG ziV!N#^i_O(Q*TlwRk0u{wdK&9Q_#P8{fY~KbKOP@%m6t7Q#fr z6(&y=#~QAtQ!vp0V0x&ttt5csqa28v*RKG_N5#EX2FeV0Jl4nlM=tICkBWPb$GWXo z+@j2iWlp6fpqDxQv_v1P%!ka5<0Jz&aqHWHjHo#FZ)M=#a_bw+j+mn}px!qeOTe}Q zY!hrb(x+I|44{$-^&C&VxQs>Iq8|c?*8SU-N@Z4=h=ATktzooG$I%jf zM^9NqP`kBL{y`K^Q5aM2ED-e}OpJ=ndH@$KuK0e+hz~1_tWL#>s0WxKT0l$@h4C8d{u@yJ}>WAE3Fd(@KviX7h9|z;NfuiI1EEH+t=KPgTM3Aw^tS8%B9EP z^FIQ3GY}A4!0e!2MEv!BH|*f^eOPO4vn>O8GOk!rNj0Vl^C`K{oB$@Vy3__9;QQZu zsT%p6nI|0@Px#?8vwI>zRXr6&qMS=T9077Cn~IJ)5MpqpvI zv*Y?!y^a9vbgF8avwNRNI6?R_xcWD=lsNK`)oC{XE_1H47NP>Y@&~8_J8U8J9{vFU zaP8Kjt$p*SAL^5fKL#$m)&YR~8wE(E1t_M@-d$N4PclpTv(-N;ISMKwaxkx7!L@h) z?Ofv@=u{EZUkCL!fwc{Yb$ys~>ThPh>-m8-eczl@e;u@L&NuaSNK0aRs52VW1Ma=! zW`6IzQ`|$`4H1F0bD)atgv*kf_TDsm_bxeM`GEfvi4`EMXxeA*;pX*U0iV6+){Bk+ z%x6ReTe}PSOr2szl;!Ry`!)GE+Z)6|z#gj^AWExl?*DF~j;SAfiH08s_s-@COXCse zt1K&4O)3x&A>UQ0%;Mqbl!n9UBEXfN+*(&e#D=3fmi?ZrZcfuxmO$jOUxm5?aMX7! zfjl>t#TnF+1%XC-2r6=Uu4xg9gQtKvc9uisyY6kqWefa}R)j}lGU^Bi!vc^> z9-S($V$V6bv=SaqzQ4o2%)s%DcQ3_4T)pHJ_8n&Rr~9ROG-5#LxX)wLf6NO|fq`uOj7mdgJI0H2v4wf~~Kng9R* M07*qoM6N<$f@g#ZbN~PV diff --git a/js/tiny_mce/themes/advanced/skins/o2k7/ui.css b/js/tiny_mce/themes/advanced/skins/o2k7/ui.css index a6253976af..0916c34e83 100644 --- a/js/tiny_mce/themes/advanced/skins/o2k7/ui.css +++ b/js/tiny_mce/themes/advanced/skins/o2k7/ui.css @@ -4,8 +4,8 @@ .o2k7Skin table td {vertical-align:middle} /* Containers */ -.o2k7Skin table {background:#E5EFFD} -.o2k7Skin iframe {display:block; background:#FFF} +.o2k7Skin table {background:transparent} +.o2k7Skin iframe {display:block;} .o2k7Skin .mceToolbar {height:26px} /* External */ @@ -19,7 +19,8 @@ .o2k7Skin table.mceLayout tr.mceLast td {border-bottom:1px solid #ABC6DD} .o2k7Skin table.mceToolbar, .o2k7Skin tr.mceFirst .mceToolbar tr td, .o2k7Skin tr.mceLast .mceToolbar tr td {border:0; margin:0; padding:0} .o2k7Skin .mceIframeContainer {border-top:1px solid #ABC6DD; border-bottom:1px solid #ABC6DD} -.o2k7Skin .mceStatusbar {display:block; font-family:'MS Sans Serif',sans-serif,Verdana,Arial; font-size:9pt; line-height:16px; overflow:visible; color:#000; height:20px} +.o2k7Skin td.mceToolbar{background:#E5EFFD} +.o2k7Skin .mceStatusbar {background:#E5EFFD; display:block; font-family:'MS Sans Serif',sans-serif,Verdana,Arial; font-size:9pt; line-height:16px; overflow:visible; color:#000; height:20px} .o2k7Skin .mceStatusbar div {float:left; padding:2px} .o2k7Skin .mceStatusbar a.mceResize {display:block; float:right; background:url(../../img/icons.gif) -800px 0; width:20px; height:20px; cursor:se-resize; outline:0} .o2k7Skin .mceStatusbar a:hover {text-decoration:underline} @@ -50,19 +51,19 @@ .o2k7Skin .mceSeparator {display:block; background:url(img/button_bg.png) -22px 0; width:5px; height:22px} /* ListBox */ -.o2k7Skin .mceListBox {margin-left:3px} +.o2k7Skin .mceListBox {padding-left: 3px} .o2k7Skin .mceListBox, .o2k7Skin .mceListBox a {display:block} .o2k7Skin .mceListBox .mceText {padding-left:4px; text-align:left; width:70px; border:1px solid #b3c7e1; border-right:0; background:#eaf2fb; font-family:Tahoma,Verdana,Arial,Helvetica; font-size:11px; height:20px; line-height:20px; overflow:hidden} .o2k7Skin .mceListBox .mceOpen {width:14px; height:22px; background:url(img/button_bg.png) -66px 0} .o2k7Skin table.mceListBoxEnabled:hover .mceText, .o2k7Skin .mceListBoxHover .mceText, .o2k7Skin .mceListBoxSelected .mceText {background:#FFF} .o2k7Skin table.mceListBoxEnabled:hover .mceOpen, .o2k7Skin .mceListBoxHover .mceOpen, .o2k7Skin .mceListBoxSelected .mceOpen {background-position:-66px -22px} .o2k7Skin .mceListBoxDisabled .mceText {color:gray} -.o2k7Skin .mceListBoxMenu {overflow:auto; overflow-x:hidden} +.o2k7Skin .mceListBoxMenu {overflow:auto; overflow-x:hidden; margin-left:3px} .o2k7Skin .mceOldBoxModel .mceListBox .mceText {height:22px} .o2k7Skin select.mceListBox {font-family:Tahoma,Verdana,Arial,Helvetica; font-size:12px; border:1px solid #b3c7e1; background:#FFF;} /* SplitButton */ -.o2k7Skin .mceSplitButton, .o2k7Skin .mceSplitButton a, .o2k7Skin .mceSplitButton span {display:block; height:22px} +.o2k7Skin .mceSplitButton, .o2k7Skin .mceSplitButton a, .o2k7Skin .mceSplitButton span {display:block; height:22px; direction:ltr} .o2k7Skin .mceSplitButton {background:url(img/button_bg.png)} .o2k7Skin .mceSplitButton a.mceAction {width:22px} .o2k7Skin .mceSplitButton span.mceAction {width:22px; background-image:url(../../img/icons.gif)} @@ -105,6 +106,7 @@ .o2k7Skin .mceNoIcons .mceMenuItemSelected a {background:url(../default/img/menu_arrow.gif) no-repeat -6px center} .o2k7Skin .mceMenu span.mceMenuLine {display:none} .o2k7Skin .mceMenuItemSub a {background:url(../default/img/menu_arrow.gif) no-repeat top right;} +.o2k7Skin .mceMenuItem td, .o2k7Skin .mceMenuItem th {line-height: normal} /* Progress,Resize */ .o2k7Skin .mceBlocker {position:absolute; left:0; top:0; z-index:1000; opacity:0.5; -ms-filter:'alpha(opacity=30)'; filter:alpha(opacity=50); background:#FFF} diff --git a/js/tiny_mce/themes/advanced/skins/o2k7/ui_black.css b/js/tiny_mce/themes/advanced/skins/o2k7/ui_black.css index 153f0c38a6..50c9b76a2d 100644 --- a/js/tiny_mce/themes/advanced/skins/o2k7/ui_black.css +++ b/js/tiny_mce/themes/advanced/skins/o2k7/ui_black.css @@ -1,6 +1,6 @@ /* Black */ .o2k7SkinBlack .mceToolbar .mceToolbarStart span, .o2k7SkinBlack .mceToolbar .mceToolbarEnd span, .o2k7SkinBlack .mceButton, .o2k7SkinBlack .mceSplitButton, .o2k7SkinBlack .mceSeparator, .o2k7SkinBlack .mceSplitButton a.mceOpen, .o2k7SkinBlack .mceListBox a.mceOpen {background-image:url(img/button_bg_black.png)} -.o2k7SkinBlack table, .o2k7SkinBlack .mceMenuItemTitle a, .o2k7SkinBlack .mceMenuItemTitle span.mceText, .o2k7SkinBlack .mceStatusbar div, .o2k7SkinBlack .mceStatusbar span, .o2k7SkinBlack .mceStatusbar a {background:#535353; color:#FFF} +.o2k7SkinBlack td.mceToolbar, .o2k7SkinBlack td.mceStatusbar, .o2k7SkinBlack .mceMenuItemTitle a, .o2k7SkinBlack .mceMenuItemTitle span.mceText, .o2k7SkinBlack .mceStatusbar div, .o2k7SkinBlack .mceStatusbar span, .o2k7SkinBlack .mceStatusbar a {background:#535353; color:#FFF} .o2k7SkinBlack table.mceListBoxEnabled .mceText, o2k7SkinBlack .mceListBox .mceText {background:#FFF; border:1px solid #CBCFD4; border-bottom-color:#989FA9; border-right:0} .o2k7SkinBlack table.mceListBoxEnabled:hover .mceText, .o2k7SkinBlack .mceListBoxHover .mceText, .o2k7SkinBlack .mceListBoxSelected .mceText {background:#FFF; border:1px solid #FFBD69; border-right:0} .o2k7SkinBlack .mceExternalToolbar, .o2k7SkinBlack .mceListBox .mceText, .o2k7SkinBlack div.mceMenu, .o2k7SkinBlack table.mceLayout, .o2k7SkinBlack .mceMenuItemTitle a, .o2k7SkinBlack table.mceLayout tr.mceFirst td, .o2k7SkinBlack table.mceLayout, .o2k7SkinBlack .mceMenuItemTitle a, .o2k7SkinBlack table.mceLayout tr.mceLast td, .o2k7SkinBlack .mceIframeContainer {border-color: #535353;} diff --git a/js/tiny_mce/themes/advanced/skins/o2k7/ui_silver.css b/js/tiny_mce/themes/advanced/skins/o2k7/ui_silver.css index 7fe3b45e12..960a8e4755 100644 --- a/js/tiny_mce/themes/advanced/skins/o2k7/ui_silver.css +++ b/js/tiny_mce/themes/advanced/skins/o2k7/ui_silver.css @@ -1,5 +1,5 @@ /* Silver */ .o2k7SkinSilver .mceToolbar .mceToolbarStart span, .o2k7SkinSilver .mceButton, .o2k7SkinSilver .mceSplitButton, .o2k7SkinSilver .mceSeparator, .o2k7SkinSilver .mceSplitButton a.mceOpen, .o2k7SkinSilver .mceListBox a.mceOpen {background-image:url(img/button_bg_silver.png)} -.o2k7SkinSilver table, .o2k7SkinSilver .mceMenuItemTitle a {background:#eee} +.o2k7SkinSilver td.mceToolbar, .o2k7SkinSilver td.mceStatusbar, .o2k7SkinSilver .mceMenuItemTitle a {background:#eee} .o2k7SkinSilver .mceListBox .mceText {background:#FFF} .o2k7SkinSilver .mceExternalToolbar, .o2k7SkinSilver .mceListBox .mceText, .o2k7SkinSilver div.mceMenu, .o2k7SkinSilver table.mceLayout, .o2k7SkinSilver .mceMenuItemTitle a, .o2k7SkinSilver table.mceLayout tr.mceFirst td, .o2k7SkinSilver table.mceLayout, .o2k7SkinSilver .mceMenuItemTitle a, .o2k7SkinSilver table.mceLayout tr.mceLast td, .o2k7SkinSilver .mceIframeContainer {border-color: #bbb} diff --git a/js/tiny_mce/themes/advanced/source_editor.htm b/js/tiny_mce/themes/advanced/source_editor.htm index 5957bbd178..3c6d65808a 100644 --- a/js/tiny_mce/themes/advanced/source_editor.htm +++ b/js/tiny_mce/themes/advanced/source_editor.htm @@ -6,7 +6,7 @@
    -
    {#advanced_dlg.code_title}
    +
    @@ -17,8 +17,8 @@
    - - + +
    diff --git a/js/tiny_mce/themes/simple/editor_template.js b/js/tiny_mce/themes/simple/editor_template.js index ed89abc067..4b3209cc92 100644 --- a/js/tiny_mce/themes/simple/editor_template.js +++ b/js/tiny_mce/themes/simple/editor_template.js @@ -1 +1 @@ -(function(){var a=tinymce.DOM;tinymce.ThemeManager.requireLangPack("simple");tinymce.create("tinymce.themes.SimpleTheme",{init:function(c,d){var e=this,b=["Bold","Italic","Underline","Strikethrough","InsertUnorderedList","InsertOrderedList"],f=c.settings;e.editor=c;c.onInit.add(function(){c.onNodeChange.add(function(h,g){tinymce.each(b,function(i){g.get(i.toLowerCase()).setActive(h.queryCommandState(i))})});c.dom.loadCSS(d+"/skins/"+f.skin+"/content.css")});a.loadCSS((f.editor_css?c.documentBaseURI.toAbsolute(f.editor_css):"")||d+"/skins/"+f.skin+"/ui.css")},renderUI:function(h){var e=this,i=h.targetNode,b,c,d=e.editor,f=d.controlManager,g;i=a.insertAfter(a.create("span",{id:d.id+"_container","class":"mceEditor "+d.settings.skin+"SimpleSkin"}),i);i=g=a.add(i,"table",{cellPadding:0,cellSpacing:0,"class":"mceLayout"});i=c=a.add(i,"tbody");i=a.add(c,"tr");i=b=a.add(a.add(i,"td"),"div",{"class":"mceIframeContainer"});i=a.add(a.add(c,"tr",{"class":"last"}),"td",{"class":"mceToolbar mceLast",align:"center"});c=e.toolbar=f.createToolbar("tools1");c.add(f.createButton("bold",{title:"simple.bold_desc",cmd:"Bold"}));c.add(f.createButton("italic",{title:"simple.italic_desc",cmd:"Italic"}));c.add(f.createButton("underline",{title:"simple.underline_desc",cmd:"Underline"}));c.add(f.createButton("strikethrough",{title:"simple.striketrough_desc",cmd:"Strikethrough"}));c.add(f.createSeparator());c.add(f.createButton("undo",{title:"simple.undo_desc",cmd:"Undo"}));c.add(f.createButton("redo",{title:"simple.redo_desc",cmd:"Redo"}));c.add(f.createSeparator());c.add(f.createButton("cleanup",{title:"simple.cleanup_desc",cmd:"mceCleanup"}));c.add(f.createSeparator());c.add(f.createButton("insertunorderedlist",{title:"simple.bullist_desc",cmd:"InsertUnorderedList"}));c.add(f.createButton("insertorderedlist",{title:"simple.numlist_desc",cmd:"InsertOrderedList"}));c.renderTo(i);return{iframeContainer:b,editorContainer:d.id+"_container",sizeContainer:g,deltaHeight:-20}},getInfo:function(){return{longname:"Simple theme",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",version:tinymce.majorVersion+"."+tinymce.minorVersion}}});tinymce.ThemeManager.add("simple",tinymce.themes.SimpleTheme)})(); \ No newline at end of file +(function(){var a=tinymce.DOM;tinymce.ThemeManager.requireLangPack("simple");tinymce.create("tinymce.themes.SimpleTheme",{init:function(c,d){var e=this,b=["Bold","Italic","Underline","Strikethrough","InsertUnorderedList","InsertOrderedList"],f=c.settings;e.editor=c;c.contentCSS.push(d+"/skins/"+f.skin+"/content.css");c.onInit.add(function(){c.onNodeChange.add(function(h,g){tinymce.each(b,function(i){g.get(i.toLowerCase()).setActive(h.queryCommandState(i))})})});a.loadCSS((f.editor_css?c.documentBaseURI.toAbsolute(f.editor_css):"")||d+"/skins/"+f.skin+"/ui.css")},renderUI:function(h){var e=this,i=h.targetNode,b,c,d=e.editor,f=d.controlManager,g;i=a.insertAfter(a.create("span",{id:d.id+"_container","class":"mceEditor "+d.settings.skin+"SimpleSkin"}),i);i=g=a.add(i,"table",{cellPadding:0,cellSpacing:0,"class":"mceLayout"});i=c=a.add(i,"tbody");i=a.add(c,"tr");i=b=a.add(a.add(i,"td"),"div",{"class":"mceIframeContainer"});i=a.add(a.add(c,"tr",{"class":"last"}),"td",{"class":"mceToolbar mceLast",align:"center"});c=e.toolbar=f.createToolbar("tools1");c.add(f.createButton("bold",{title:"simple.bold_desc",cmd:"Bold"}));c.add(f.createButton("italic",{title:"simple.italic_desc",cmd:"Italic"}));c.add(f.createButton("underline",{title:"simple.underline_desc",cmd:"Underline"}));c.add(f.createButton("strikethrough",{title:"simple.striketrough_desc",cmd:"Strikethrough"}));c.add(f.createSeparator());c.add(f.createButton("undo",{title:"simple.undo_desc",cmd:"Undo"}));c.add(f.createButton("redo",{title:"simple.redo_desc",cmd:"Redo"}));c.add(f.createSeparator());c.add(f.createButton("cleanup",{title:"simple.cleanup_desc",cmd:"mceCleanup"}));c.add(f.createSeparator());c.add(f.createButton("insertunorderedlist",{title:"simple.bullist_desc",cmd:"InsertUnorderedList"}));c.add(f.createButton("insertorderedlist",{title:"simple.numlist_desc",cmd:"InsertOrderedList"}));c.renderTo(i);return{iframeContainer:b,editorContainer:d.id+"_container",sizeContainer:g,deltaHeight:-20}},getInfo:function(){return{longname:"Simple theme",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",version:tinymce.majorVersion+"."+tinymce.minorVersion}}});tinymce.ThemeManager.add("simple",tinymce.themes.SimpleTheme)})(); \ No newline at end of file diff --git a/js/tiny_mce/themes/simple/editor_template_src.js b/js/tiny_mce/themes/simple/editor_template_src.js index 4b862d49d6..01ce87c58a 100644 --- a/js/tiny_mce/themes/simple/editor_template_src.js +++ b/js/tiny_mce/themes/simple/editor_template_src.js @@ -19,6 +19,7 @@ var t = this, states = ['Bold', 'Italic', 'Underline', 'Strikethrough', 'InsertUnorderedList', 'InsertOrderedList'], s = ed.settings; t.editor = ed; + ed.contentCSS.push(url + "/skins/" + s.skin + "/content.css"); ed.onInit.add(function() { ed.onNodeChange.add(function(ed, cm) { @@ -26,8 +27,6 @@ cm.get(c.toLowerCase()).setActive(ed.queryCommandState(c)); }); }); - - ed.dom.loadCSS(url + "/skins/" + s.skin + "/content.css"); }); DOM.loadCSS((s.editor_css ? ed.documentBaseURI.toAbsolute(s.editor_css) : '') || url + "/skins/" + s.skin + "/ui.css"); diff --git a/js/tiny_mce/themes/simple/img/icons.gif b/js/tiny_mce/themes/simple/img/icons.gif index 16af141ff0eea376a889b1e8d28e9c1cacaaab16..6fcbcb5dedf16a5fa1d15c2aa127bceb612f1e71 100644 GIT binary patch literal 806 zcmZ?wbhEHbJi#Es@KuxH{{08bHy>kQU|4+kcV*u?6Yu2z|Nk2X_I-c9E~4fBsnb_x z&7Ngzq1inB@Woqi-rnfiGF7yw9zM z^p;~=3MY4TT)2AY!iC=7fBwej_wPS(UDm1P!}|}9?`#au+qhx#R?Fl=KuakHia%Lc z85lGfbU;Rd{N%v)|G<<24;`ug6HAIt=2*?Yu%d)ZG@><(acbwmDSj%f4MMZ#%vkt3 z%+g~)i8kP^LLB_(WJHBo z)4eilyozQ8a&qqSt%<6xpa0;xA7k;M?mchbzI*>+`RnL~M?L!{MDwZt;o_B2F2$0=pQSpQ!u@RcUGT{(44KaY91N#ws_nDH9G%Qf=ZF z5o_THWH`G~`GwyilS^z$ZvV~I`dh4Lx_8c>?R@8gr-07UIgFjp0y#A&c{B)cE>2kS zL5I1;i$zoEA)6qV`HGJvVWE!{8MZ6ST|PC}d%Kid7{KiD{l18xziSGKuWtj9AkWy-*`}#c~0`Lrjq> z-;O-o=3A#@&dst%_SasuJq0xZW;OwR3vM!diY%Es?;J~Pp}LYununP(i|XxU>#u=* zSvNC^0?cJ=S?=UK4&2DdcCO^BsHxjWc4vR-Z64x&8r#>V9!JMd4O!Z*d@mNrgX=jUy;0|T>ZntHjDU$=-I8y`|tN~Y9 literal 1440 zcmV;R1z-9{Nk%w1VaNa!0QUd@Ib*`7v&H}b0P*i`B{WZ*I4YI8{iDPCZ*XyWj;?N! z&ooP8CcKTM%}ImAk&d@bUef&=iA% zhPA3sm56OYcjMRI^s}jof~E0n!SIozxs`y)bZpaM%~elOt(xIz_1F@`xREtxwxO@X zElsNLx;f_MIwnTOux@bk@5r<-;@s){f~fMSskU>S&vlpdmZGk)n^Ks084*pfMo5}`Y)@uBrt7q^ z_xb)XxI@^-XhLVQWPPfUtMQSg&Xb6UQhU2=S3pa1!Lhs1Kwz1)!P59aI6r5pthLM4 zE-ud4`aC>8zybolqcQ$sRq*)W>+kl^)!br%x2LJkVv+Dui1Oh7|6z>ag023Luhg%= z;=sbh)RP30t>V}2$H?fg=;-%zOTU8v0MO8l85I$+z}bYP#G9DS_#hs}n3hj*tissz zAwYQh{QX~VkH5&*9YTcu{{H^`{_yYcW|;u|{Qilm^upTyi?sd!nVG)6{{LrW`s5%! zQETJeu@Y0sB3Qy+jGVB@-BWO)C1U{h_4v@?@UEu*S8lPiucH6>|4vxO2|0#LaF@v0 z@ZaCy@c8hkxVXaB{z|fT@U~;Hv$d$T$J*xpqPpE8TH+G^0=vlI+KzIEuZN}B@UYO} z&dtoGp5{=vw)ErQRcDJbQgSxGf8JYL_`X^{uFH_9uqY`f`}_Of)zF}&w4mVd!0r05 zoM3>k!2kdMA^8LW00930EC2ui0LTCo000R80RIUbNU)&6g9sBUT*$DY!-o(fN}Ncs zqQ#3CGiuz(v7^V2AVW@EluM+^lPFV4B&kwhfCK?JZW}S8Lq-x8D(>2KU{_0tBn}8A zagb%p1Q&P-6u`78(}*2LRH3FT;{^Z%ooYoWl!#X%K7Tem@Rf*AgGMWAZK^N;uLKrJ zgqs_6Dufyfw<06~AZA3KR{>nO09GJYj!yq2Mo2^;St0-;UpPxp*8_}f6+Z>JE!?&a z+dPb*b~hYDra%-%J`C}|)xna%9$;-yz|zc`Z6DmMS)p07fiENwe4t=jwVQEwY|=zo zxL=@s=&E6Qp)TGXT_1)l*emUVx)m?~6;Hl)c5^2e&KlH&3v877L9rfP;Fp}T4iu?af8AOXd@5C0(U&fK3z8Av u{UxbK=c.length){for(e=0,b=g.length;e=c.length||g[e]!=c[e]){f=e+1;break}}}if(g.length=g.length||g[e]!=c[e]){f=e+1;break}}}if(f==1){return h}for(e=0,b=g.length-(f-1);e=0;c--){if(f[c].length==0||f[c]=="."){continue}if(f[c]==".."){b++;continue}if(b>0){b--;continue}h.push(f[c])}c=e.length-b;if(c<=0){g=h.reverse().join("/")}else{g=e.slice(0,c).join("/")+"/"+h.reverse().join("/")}if(g.indexOf("/")!==0){g="/"+g}if(d&&g.lastIndexOf("/")!==g.length-1){g+=d}return g},getURI:function(d){var c,b=this;if(!b.source||d){c="";if(!d){if(b.protocol){c+=b.protocol+"://"}if(b.userInfo){c+=b.userInfo+"@"}if(b.host){c+=b.host}if(b.port){c+=":"+b.port}}if(b.path){c+=b.path}if(b.query){c+="?"+b.query}if(b.anchor){c+="#"+b.anchor}b.source=c}return b.source}})})();(function(){var a=tinymce.each;tinymce.create("static tinymce.util.Cookie",{getHash:function(d){var b=this.get(d),c;if(b){a(b.split("&"),function(e){e=e.split("=");c=c||{};c[unescape(e[0])]=unescape(e[1])})}return c},setHash:function(j,b,g,f,i,c){var h="";a(b,function(e,d){h+=(!h?"":"&")+escape(d)+"="+escape(e)});this.set(j,h,g,f,i,c)},get:function(i){var h=document.cookie,g,f=i+"=",d;if(!h){return}d=h.indexOf("; "+f);if(d==-1){d=h.indexOf(f);if(d!=0){return null}}else{d+=2}g=h.indexOf(";",d);if(g==-1){g=h.length}return unescape(h.substring(d+f.length,g))},set:function(i,b,g,f,h,c){document.cookie=i+"="+escape(b)+((g)?"; expires="+g.toGMTString():"")+((f)?"; path="+escape(f):"")+((h)?"; domain="+h:"")+((c)?"; secure":"")},remove:function(e,b){var c=new Date();c.setTime(c.getTime()-1000);this.set(e,"",c,b,c)}})})();tinymce.create("static tinymce.util.JSON",{serialize:function(e){var c,a,d=tinymce.util.JSON.serialize,b;if(e==null){return"null"}b=typeof e;if(b=="string"){a="\bb\tt\nn\ff\rr\"\"''\\\\";return'"'+e.replace(/([\u0080-\uFFFF\x00-\x1f\"])/g,function(g,f){c=a.indexOf(f);if(c+1){return"\\"+a.charAt(c+1)}g=f.charCodeAt().toString(16);return"\\u"+"0000".substring(g.length)+g})+'"'}if(b=="object"){if(e.hasOwnProperty&&e instanceof Array){for(c=0,a="[";c0?",":"")+d(e[c])}return a+"]"}a="{";for(c in e){a+=typeof e[c]!="function"?(a.length>1?',"':'"')+c+'":'+d(e[c]):""}return a+"}"}return""+e},parse:function(s){try{return eval("("+s+")")}catch(ex){}}});tinymce.create("static tinymce.util.XHR",{send:function(g){var a,e,b=window,h=0;g.scope=g.scope||this;g.success_scope=g.success_scope||g.scope;g.error_scope=g.error_scope||g.scope;g.async=g.async===false?false:true;g.data=g.data||"";function d(i){a=0;try{a=new ActiveXObject(i)}catch(c){}return a}a=b.XMLHttpRequest?new XMLHttpRequest():d("Microsoft.XMLHTTP")||d("Msxml2.XMLHTTP");if(a){if(a.overrideMimeType){a.overrideMimeType(g.content_type)}a.open(g.type||(g.data?"POST":"GET"),g.url,g.async);if(g.content_type){a.setRequestHeader("Content-Type",g.content_type)}a.setRequestHeader("X-Requested-With","XMLHttpRequest");a.send(g.data);function f(){if(!g.async||a.readyState==4||h++>10000){if(g.success&&h<10000&&a.status==200){g.success.call(g.success_scope,""+a.responseText,a,g)}else{if(g.error){g.error.call(g.error_scope,h>10000?"TIMED_OUT":"GENERAL",a,g)}}a=null}else{b.setTimeout(f,10)}}if(!g.async){return f()}e=b.setTimeout(f,10)}}});(function(){var c=tinymce.extend,b=tinymce.util.JSON,a=tinymce.util.XHR;tinymce.create("tinymce.util.JSONRequest",{JSONRequest:function(d){this.settings=c({},d);this.count=0},send:function(f){var e=f.error,d=f.success;f=c(this.settings,f);f.success=function(h,g){h=b.parse(h);if(typeof(h)=="undefined"){h={error:"JSON Parse error."}}if(h.error){e.call(f.error_scope||f.scope,h.error,g)}else{d.call(f.success_scope||f.scope,h.result)}};f.error=function(h,g){e.call(f.error_scope||f.scope,h,g)};f.data=b.serialize({id:f.id||"c"+(this.count++),method:f.method,params:f.params});f.content_type="application/json";a.send(f)},"static":{sendRPC:function(d){return new tinymce.util.JSONRequest().send(d)}}})}());(function(m){var k=m.each,j=m.is,i=m.isWebKit,d=m.isIE,a=/^(H[1-6R]|P|DIV|ADDRESS|PRE|FORM|T(ABLE|BODY|HEAD|FOOT|H|R|D)|LI|OL|UL|CAPTION|BLOCKQUOTE|CENTER|DL|DT|DD|DIR|FIELDSET|NOSCRIPT|MENU|ISINDEX|SAMP)$/,e=g("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected"),f=g("src,href,style,coords,shape"),c={"&":"&",'"':""","<":"<",">":">"},n=/[<>&\"]/g,b=/^([a-z0-9],?)+$/i,h=/<(\w+)((?:\s+\w+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)(\s*\/?)>/g,l=/(\w+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g;function g(q){var p={},o;q=q.split(",");for(o=q.length;o>=0;o--){p[q[o]]=1}return p}m.create("tinymce.dom.DOMUtils",{doc:null,root:null,files:null,pixelStyles:/^(top|left|bottom|right|width|height|borderWidth)$/,props:{"for":"htmlFor","class":"className",className:"className",checked:"checked",disabled:"disabled",maxlength:"maxLength",readonly:"readOnly",selected:"selected",value:"value",id:"id",name:"name",type:"type"},DOMUtils:function(u,q){var p=this,o;p.doc=u;p.win=window;p.files={};p.cssFlicker=false;p.counter=0;p.boxModel=!m.isIE||u.compatMode=="CSS1Compat";p.stdMode=u.documentMode===8;p.settings=q=m.extend({keep_values:false,hex_colors:1,process_html:1},q);if(m.isIE6){try{u.execCommand("BackgroundImageCache",false,true)}catch(r){p.cssFlicker=true}}if(q.valid_styles){p._styles={};k(q.valid_styles,function(t,s){p._styles[s]=m.explode(t)})}m.addUnload(p.destroy,p)},getRoot:function(){var o=this,p=o.settings;return(p&&o.get(p.root_element))||o.doc.body},getViewPort:function(p){var q,o;p=!p?this.win:p;q=p.document;o=this.boxModel?q.documentElement:q.body;return{x:p.pageXOffset||o.scrollLeft,y:p.pageYOffset||o.scrollTop,w:p.innerWidth||o.clientWidth,h:p.innerHeight||o.clientHeight}},getRect:function(s){var r,o=this,q;s=o.get(s);r=o.getPos(s);q=o.getSize(s);return{x:r.x,y:r.y,w:q.w,h:q.h}},getSize:function(r){var p=this,o,q;r=p.get(r);o=p.getStyle(r,"width");q=p.getStyle(r,"height");if(o.indexOf("px")===-1){o=0}if(q.indexOf("px")===-1){q=0}return{w:parseInt(o)||r.offsetWidth||r.clientWidth,h:parseInt(q)||r.offsetHeight||r.clientHeight}},getParent:function(q,p,o){return this.getParents(q,p,o,false)},getParents:function(z,v,s,y){var q=this,p,u=q.settings,x=[];z=q.get(z);y=y===undefined;if(u.strict_root){s=s||q.getRoot()}if(j(v,"string")){p=v;if(v==="*"){v=function(o){return o.nodeType==1}}else{v=function(o){return q.is(o,p)}}}while(z){if(z==s||!z.nodeType||z.nodeType===9){break}if(!v||v(z)){if(y){x.push(z)}else{return z}}z=z.parentNode}return y?x:null},get:function(o){var p;if(o&&this.doc&&typeof(o)=="string"){p=o;o=this.doc.getElementById(o);if(o&&o.id!==p){return this.doc.getElementsByName(p)[1]}}return o},getNext:function(p,o){return this._findSib(p,o,"nextSibling")},getPrev:function(p,o){return this._findSib(p,o,"previousSibling")},select:function(q,p){var o=this;return m.dom.Sizzle(q,o.get(p)||o.get(o.settings.root_element)||o.doc,[])},is:function(q,o){var p;if(q.length===undefined){if(o==="*"){return q.nodeType==1}if(b.test(o)){o=o.toLowerCase().split(/,/);q=q.nodeName.toLowerCase();for(p=o.length-1;p>=0;p--){if(o[p]==q){return true}}return false}}return m.dom.Sizzle.matches(o,q.nodeType?[q]:q).length>0},add:function(s,v,o,r,u){var q=this;return this.run(s,function(y){var x,t;x=j(v,"string")?q.doc.createElement(v):v;q.setAttribs(x,o);if(r){if(r.nodeType){x.appendChild(r)}else{q.setHTML(x,r)}}return !u?y.appendChild(x):x})},create:function(q,o,p){return this.add(this.doc.createElement(q),q,o,p,1)},createHTML:function(v,p,s){var u="",r=this,q;u+="<"+v;for(q in p){if(p.hasOwnProperty(q)){u+=" "+q+'="'+r.encode(p[q])+'"'}}if(m.is(s)){return u+">"+s+""}return u+" />"},remove:function(o,p){return this.run(o,function(r){var q,s;q=r.parentNode;if(!q){return null}if(p){while(s=r.firstChild){if(!m.isIE||s.nodeType!==3||s.nodeValue){q.insertBefore(s,r)}else{r.removeChild(s)}}}return q.removeChild(r)})},setStyle:function(r,o,p){var q=this;return q.run(r,function(v){var u,t;u=v.style;o=o.replace(/-(\D)/g,function(x,s){return s.toUpperCase()});if(q.pixelStyles.test(o)&&(m.is(p,"number")||/^[\-0-9\.]+$/.test(p))){p+="px"}switch(o){case"opacity":if(d){u.filter=p===""?"":"alpha(opacity="+(p*100)+")";if(!r.currentStyle||!r.currentStyle.hasLayout){u.display="inline-block"}}u[o]=u["-moz-opacity"]=u["-khtml-opacity"]=p||"";break;case"float":d?u.styleFloat=p:u.cssFloat=p;break;default:u[o]=p||""}if(q.settings.update_styles){q.setAttrib(v,"_mce_style")}})},getStyle:function(r,o,q){r=this.get(r);if(!r){return false}if(this.doc.defaultView&&q){o=o.replace(/[A-Z]/g,function(s){return"-"+s});try{return this.doc.defaultView.getComputedStyle(r,null).getPropertyValue(o)}catch(p){return null}}o=o.replace(/-(\D)/g,function(t,s){return s.toUpperCase()});if(o=="float"){o=d?"styleFloat":"cssFloat"}if(r.currentStyle&&q){return r.currentStyle[o]}return r.style[o]},setStyles:function(u,v){var q=this,r=q.settings,p;p=r.update_styles;r.update_styles=0;k(v,function(o,s){q.setStyle(u,s,o)});r.update_styles=p;if(r.update_styles){q.setAttrib(u,r.cssText)}},setAttrib:function(q,r,o){var p=this;if(!q||!r){return}if(p.settings.strict){r=r.toLowerCase()}return this.run(q,function(u){var t=p.settings;switch(r){case"style":if(!j(o,"string")){k(o,function(s,x){p.setStyle(u,x,s)});return}if(t.keep_values){if(o&&!p._isRes(o)){u.setAttribute("_mce_style",o,2)}else{u.removeAttribute("_mce_style",2)}}u.style.cssText=o;break;case"class":u.className=o||"";break;case"src":case"href":if(t.keep_values){if(t.url_converter){o=t.url_converter.call(t.url_converter_scope||p,o,r,u)}p.setAttrib(u,"_mce_"+r,o,2)}break;case"shape":u.setAttribute("_mce_style",o);break}if(j(o)&&o!==null&&o.length!==0){u.setAttribute(r,""+o,2)}else{u.removeAttribute(r,2)}})},setAttribs:function(q,r){var p=this;return this.run(q,function(o){k(r,function(s,t){p.setAttrib(o,t,s)})})},getAttrib:function(r,s,q){var o,p=this;r=p.get(r);if(!r||r.nodeType!==1){return false}if(!j(q)){q=""}if(/^(src|href|style|coords|shape)$/.test(s)){o=r.getAttribute("_mce_"+s);if(o){return o}}if(d&&p.props[s]){o=r[p.props[s]];o=o&&o.nodeValue?o.nodeValue:o}if(!o){o=r.getAttribute(s,2)}if(/^(checked|compact|declare|defer|disabled|ismap|multiple|nohref|noshade|nowrap|readonly|selected)$/.test(s)){if(r[p.props[s]]===true&&o===""){return s}return o?s:""}if(r.nodeName==="FORM"&&r.getAttributeNode(s)){return r.getAttributeNode(s).nodeValue}if(s==="style"){o=o||r.style.cssText;if(o){o=p.serializeStyle(p.parseStyle(o),r.nodeName);if(p.settings.keep_values&&!p._isRes(o)){r.setAttribute("_mce_style",o)}}}if(i&&s==="class"&&o){o=o.replace(/(apple|webkit)\-[a-z\-]+/gi,"")}if(d){switch(s){case"rowspan":case"colspan":if(o===1){o=""}break;case"size":if(o==="+0"||o===20||o===0){o=""}break;case"width":case"height":case"vspace":case"checked":case"disabled":case"readonly":if(o===0){o=""}break;case"hspace":if(o===-1){o=""}break;case"maxlength":case"tabindex":if(o===32768||o===2147483647||o==="32768"){o=""}break;case"multiple":case"compact":case"noshade":case"nowrap":if(o===65535){return s}return q;case"shape":o=o.toLowerCase();break;default:if(s.indexOf("on")===0&&o){o=(""+o).replace(/^function\s+\w+\(\)\s+\{\s+(.*)\s+\}$/,"$1")}}}return(o!==undefined&&o!==null&&o!=="")?""+o:q},getPos:function(A,s){var p=this,o=0,z=0,u,v=p.doc,q;A=p.get(A);s=s||v.body;if(A){if(d&&!p.stdMode){A=A.getBoundingClientRect();u=p.boxModel?v.documentElement:v.body;o=p.getStyle(p.select("html")[0],"borderWidth");o=(o=="medium"||p.boxModel&&!p.isIE6)&&2||o;A.top+=p.win.self!=p.win.top?2:0;return{x:A.left+u.scrollLeft-o,y:A.top+u.scrollTop-o}}q=A;while(q&&q!=s&&q.nodeType){o+=q.offsetLeft||0;z+=q.offsetTop||0;q=q.offsetParent}q=A.parentNode;while(q&&q!=s&&q.nodeType){o-=q.scrollLeft||0;z-=q.scrollTop||0;q=q.parentNode}}return{x:o,y:z}},parseStyle:function(r){var u=this,v=u.settings,x={};if(!r){return x}function p(D,A,C){var z,B,o,y;z=x[D+"-top"+A];if(!z){return}B=x[D+"-right"+A];if(z!=B){return}o=x[D+"-bottom"+A];if(B!=o){return}y=x[D+"-left"+A];if(o!=y){return}x[C]=y;delete x[D+"-top"+A];delete x[D+"-right"+A];delete x[D+"-bottom"+A];delete x[D+"-left"+A]}function q(y,s,o,A){var z;z=x[s];if(!z){return}z=x[o];if(!z){return}z=x[A];if(!z){return}x[y]=x[s]+" "+x[o]+" "+x[A];delete x[s];delete x[o];delete x[A]}r=r.replace(/&(#?[a-z0-9]+);/g,"&$1_MCE_SEMI_");k(r.split(";"),function(s){var o,t=[];if(s){s=s.replace(/_MCE_SEMI_/g,";");s=s.replace(/url\([^\)]+\)/g,function(y){t.push(y);return"url("+t.length+")"});s=s.split(":");o=m.trim(s[1]);o=o.replace(/url\(([^\)]+)\)/g,function(z,y){return t[parseInt(y)-1]});o=o.replace(/rgb\([^\)]+\)/g,function(y){return u.toHex(y)});if(v.url_converter){o=o.replace(/url\([\'\"]?([^\)\'\"]+)[\'\"]?\)/g,function(y,z){return"url("+v.url_converter.call(v.url_converter_scope||u,u.decode(z),"style",null)+")"})}x[m.trim(s[0]).toLowerCase()]=o}});p("border","","border");p("border","-width","border-width");p("border","-color","border-color");p("border","-style","border-style");p("padding","","padding");p("margin","","margin");q("border","border-width","border-style","border-color");if(d){if(x.border=="medium none"){x.border=""}}return x},serializeStyle:function(v,p){var q=this,r="";function u(s,o){if(o&&s){if(o.indexOf("-")===0){return}switch(o){case"font-weight":if(s==700){s="bold"}break;case"color":case"background-color":s=s.toLowerCase();break}r+=(r?" ":"")+o+": "+s+";"}}if(p&&q._styles){k(q._styles["*"],function(o){u(v[o],o)});k(q._styles[p.toLowerCase()],function(o){u(v[o],o)})}else{k(v,u)}return r},loadCSS:function(o){var q=this,r=q.doc,p;if(!o){o=""}p=q.select("head")[0];k(o.split(","),function(s){var t;if(q.files[s]){return}q.files[s]=true;t=q.create("link",{rel:"stylesheet",href:m._addVer(s)});if(d&&r.documentMode){t.onload=function(){r.recalc();t.onload=null}}p.appendChild(t)})},addClass:function(o,p){return this.run(o,function(q){var r;if(!p){return 0}if(this.hasClass(q,p)){return q.className}r=this.removeClass(q,p);return q.className=(r!=""?(r+" "):"")+p})},removeClass:function(q,r){var o=this,p;return o.run(q,function(t){var s;if(o.hasClass(t,r)){if(!p){p=new RegExp("(^|\\s+)"+r+"(\\s+|$)","g")}s=t.className.replace(p," ");s=m.trim(s!=" "?s:"");t.className=s;if(!s){t.removeAttribute("class");t.removeAttribute("className")}return s}return t.className})},hasClass:function(p,o){p=this.get(p);if(!p||!o){return false}return(" "+p.className+" ").indexOf(" "+o+" ")!==-1},show:function(o){return this.setStyle(o,"display","block")},hide:function(o){return this.setStyle(o,"display","none")},isHidden:function(o){o=this.get(o);return !o||o.style.display=="none"||this.getStyle(o,"display")=="none"},uniqueId:function(o){return(!o?"mce_":o)+(this.counter++)},setHTML:function(q,p){var o=this;return this.run(q,function(v){var r,t,s,z,u,r;p=o.processHTML(p);if(d){function y(){while(v.firstChild){v.firstChild.removeNode()}try{v.innerHTML="
    "+p;v.removeChild(v.firstChild)}catch(x){r=o.create("div");r.innerHTML="
    "+p;k(r.childNodes,function(B,A){if(A){v.appendChild(B)}})}}if(o.settings.fix_ie_paragraphs){p=p.replace(/

    <\/p>|]+)><\/p>|/gi,' 

    ')}y();if(o.settings.fix_ie_paragraphs){s=v.getElementsByTagName("p");for(t=s.length-1,r=0;t>=0;t--){z=s[t];if(!z.hasChildNodes()){if(!z._mce_keep){r=1;break}z.removeAttribute("_mce_keep")}}}if(r){p=p.replace(/

    ]+)>|

    /ig,'

    ');p=p.replace(/<\/p>/gi,"
    ");y();if(o.settings.fix_ie_paragraphs){s=v.getElementsByTagName("DIV");for(t=s.length-1;t>=0;t--){z=s[t];if(z._mce_tmp){u=o.doc.createElement("p");z.cloneNode(false).outerHTML.replace(/([a-z0-9\-_]+)=/gi,function(A,x){var B;if(x!=="_mce_tmp"){B=z.getAttribute(x);if(!B&&x==="class"){B=z.className}u.setAttribute(x,B)}});for(r=0;r]+)\/>|/gi,"");if(q.keep_values){if(/)/g,"\n");t=t.replace(/^[\r\n]*|[\r\n]*$/g,"");t=t.replace(/^\s*(\/\/\s*|\]\]>|-->|\]\]-->)\s*$/g,"");return t}r=r.replace(/]+|)>([\s\S]*?)<\/script>/gi,function(s,x,t){if(!x){x=' type="text/javascript"'}x=x.replace(/src=\"([^\"]+)\"?/i,function(y,z){if(q.url_converter){z=p.encode(q.url_converter.call(q.url_converter_scope||p,p.decode(z),"src","script"))}return'_mce_src="'+z+'"'});if(m.trim(t)){v.push(o(t));t=""}return""+t+""});r=r.replace(/]+|)>([\s\S]*?)<\/style>/gi,function(s,x,t){if(t){v.push(o(t));t=""}return""+t+""});r=r.replace(/]+|)>([\s\S]*?)<\/noscript>/g,function(s,x,t){return""})}r=r.replace(//g,"");function u(s){return s.replace(h,function(y,z,x,t){return"<"+z+x.replace(l,function(B,A,E,D,C){var F;A=A.toLowerCase();E=E||D||C||"";if(e[A]){if(E==="false"||E==="0"){return}return A+'="'+A+'"'}if(f[A]&&x.indexOf("_mce_"+A)==-1){F=p.decode(E);if(q.url_converter&&(A=="src"||A=="href")){F=q.url_converter.call(q.url_converter_scope||p,F,A,z)}if(A=="style"){F=p.serializeStyle(p.parseStyle(F),A)}return A+'="'+E+'" _mce_'+A+'="'+p.encode(F)+'"'}return B})+t+">"})}r=u(r);r=r.replace(/MCE_SCRIPT:([0-9]+)/g,function(t,s){return v[s]})}return r},getOuterHTML:function(o){var p;o=this.get(o);if(!o){return null}if(o.outerHTML!==undefined){return o.outerHTML}p=(o.ownerDocument||this.doc).createElement("body");p.appendChild(o.cloneNode(true));return p.innerHTML},setOuterHTML:function(r,p,s){var o=this;function q(u,t,x){var y,v;v=x.createElement("body");v.innerHTML=t;y=v.lastChild;while(y){o.insertAfter(y.cloneNode(true),u);y=y.previousSibling}o.remove(u)}return this.run(r,function(u){u=o.get(u);if(u.nodeType==1){s=s||u.ownerDocument||o.doc;if(d){try{if(d&&u.nodeType==1){u.outerHTML=p}else{q(u,p,s)}}catch(t){q(u,p,s)}}else{q(u,p,s)}}})},decode:function(p){var q,r,o;if(/&[\w#]+;/.test(p)){q=this.doc.createElement("div");q.innerHTML=p;r=q.firstChild;o="";if(r){do{o+=r.nodeValue}while(r=r.nextSibling)}return o||p}return p},encode:function(o){return(""+o).replace(n,function(p){return c[p]})},insertAfter:function(o,p){p=this.get(p);return this.run(o,function(r){var q,s;q=p.parentNode;s=p.nextSibling;if(s){q.insertBefore(r,s)}else{q.appendChild(r)}return r})},isBlock:function(o){if(o.nodeType&&o.nodeType!==1){return false}o=o.nodeName||o;return a.test(o)},replace:function(s,r,p){var q=this;if(j(r,"array")){s=s.cloneNode(true)}return q.run(r,function(t){if(p){k(m.grep(t.childNodes),function(o){s.appendChild(o)})}return t.parentNode.replaceChild(s,t)})},rename:function(r,o){var q=this,p;if(r.nodeName!=o.toUpperCase()){p=q.create(o);k(q.getAttribs(r),function(s){q.setAttrib(p,s.nodeName,q.getAttrib(r,s.nodeName))});q.replace(p,r,1)}return p||r},findCommonAncestor:function(q,o){var r=q,p;while(r){p=o;while(p&&r!=p){p=p.parentNode}if(r==p){break}r=r.parentNode}if(!r&&q.ownerDocument){return q.ownerDocument.documentElement}return r},toHex:function(o){var q=/^\s*rgb\s*?\(\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?\)\s*$/i.exec(o);function p(r){r=parseInt(r).toString(16);return r.length>1?r:"0"+r}if(q){o="#"+p(q[1])+p(q[2])+p(q[3]);return o}return o},getClasses:function(){var s=this,o=[],r,u={},v=s.settings.class_filter,q;if(s.classes){return s.classes}function x(t){k(t.imports,function(y){x(y)});k(t.cssRules||t.rules,function(y){switch(y.type||1){case 1:if(y.selectorText){k(y.selectorText.split(","),function(z){z=z.replace(/^\s*|\s*$|^\s\./g,"");if(/\.mce/.test(z)||!/\.[\w\-]+$/.test(z)){return}q=z;z=z.replace(/.*\.([a-z0-9_\-]+).*/i,"$1");if(v&&!(z=v(z,q))){return}if(!u[z]){o.push({"class":z});u[z]=1}})}break;case 3:x(y.styleSheet);break}})}try{k(s.doc.styleSheets,x)}catch(p){}if(o.length>0){s.classes=o}return o},run:function(u,r,q){var p=this,v;if(p.doc&&typeof(u)==="string"){u=p.get(u)}if(!u){return false}q=q||this;if(!u.nodeType&&(u.length||u.length===0)){v=[];k(u,function(s,o){if(s){if(typeof(s)=="string"){s=p.doc.getElementById(s)}v.push(r.call(q,s,o))}});return v}return r.call(q,u)},getAttribs:function(q){var p;q=this.get(q);if(!q){return[]}if(d){p=[];if(q.nodeName=="OBJECT"){return q.attributes}if(q.nodeName==="OPTION"&&this.getAttrib(q,"selected")){p.push({specified:1,nodeName:"selected"})}q.cloneNode(false).outerHTML.replace(/<\/?[\w:\-]+ ?|=[\"][^\"]+\"|=\'[^\']+\'|=[\w\-]+|>/gi,"").replace(/[\w:\-]+/gi,function(o){p.push({specified:1,nodeName:o})});return p}return q.attributes},destroy:function(p){var o=this;if(o.events){o.events.destroy()}o.win=o.doc=o.root=o.events=null;if(!p){m.removeUnload(o.destroy)}},createRng:function(){var o=this.doc;return o.createRange?o.createRange():new m.dom.Range(this)},nodeIndex:function(s,t){var o=0,q,r,p;if(s){for(q=s.nodeType,s=s.previousSibling,r=s;s;s=s.previousSibling){p=s.nodeType;if(t&&p==3){if(p==q||!s.nodeValue.length){continue}}o++;q=p}}return o},split:function(u,s,y){var z=this,o=z.createRng(),v,q,x;function p(A){var t,r=A.childNodes;if(A.nodeType==1&&A.getAttribute("_mce_type")=="bookmark"){return}for(t=r.length-1;t>=0;t--){p(r[t])}if(A.nodeType!=9){if(A.nodeType==3&&A.nodeValue.length>0){return}if(A.nodeType==1){r=A.childNodes;if(r.length==1&&r[0]&&r[0].nodeType==1&&r[0].getAttribute("_mce_type")=="bookmark"){A.parentNode.insertBefore(r[0],A)}if(r.length||/^(br|hr|input|img)$/i.test(A.nodeName)){return}}z.remove(A)}return A}if(u&&s){o.setStart(u.parentNode,z.nodeIndex(u));o.setEnd(s.parentNode,z.nodeIndex(s));v=o.extractContents();o=z.createRng();o.setStart(s.parentNode,z.nodeIndex(s)+1);o.setEnd(u.parentNode,z.nodeIndex(u)+1);q=o.extractContents();x=u.parentNode;x.insertBefore(p(v),u);if(y){x.replaceChild(y,s)}else{x.insertBefore(s,u)}x.insertBefore(p(q),u);z.remove(u);return y||s}},bind:function(s,o,r,q){var p=this;if(!p.events){p.events=new m.dom.EventUtils()}return p.events.add(s,o,r,q||this)},unbind:function(r,o,q){var p=this;if(!p.events){p.events=new m.dom.EventUtils()}return p.events.remove(r,o,q)},_findSib:function(r,o,p){var q=this,s=o;if(r){if(j(s,"string")){s=function(t){return q.is(t,o)}}for(r=r[p];r;r=r[p]){if(s(r)){return r}}}return null},_isRes:function(o){return/^(top|left|bottom|right|width|height)/i.test(o)||/;\s*(top|left|bottom|right|width|height)/i.test(o)}});m.DOM=new m.dom.DOMUtils(document,{process_html:0})})(tinymce);(function(a){function b(c){var N=this,e=c.doc,S=0,E=1,j=2,D=true,R=false,U="startOffset",h="startContainer",P="endContainer",z="endOffset",k=tinymce.extend,n=c.nodeIndex;k(N,{startContainer:e,startOffset:0,endContainer:e,endOffset:0,collapsed:D,commonAncestorContainer:e,START_TO_START:0,START_TO_END:1,END_TO_END:2,END_TO_START:3,setStart:q,setEnd:s,setStartBefore:g,setStartAfter:I,setEndBefore:J,setEndAfter:u,collapse:A,selectNode:x,selectNodeContents:F,compareBoundaryPoints:v,deleteContents:p,extractContents:H,cloneContents:d,insertNode:C,surroundContents:M,cloneRange:K});function q(V,t){B(D,V,t)}function s(V,t){B(R,V,t)}function g(t){q(t.parentNode,n(t))}function I(t){q(t.parentNode,n(t)+1)}function J(t){s(t.parentNode,n(t))}function u(t){s(t.parentNode,n(t)+1)}function A(t){if(t){N[P]=N[h];N[z]=N[U]}else{N[h]=N[P];N[U]=N[z]}N.collapsed=D}function x(t){g(t);u(t)}function F(t){q(t,0);s(t,t.nodeType===1?t.childNodes.length:t.nodeValue.length)}function v(W,X){var Z=N[h],Y=N[U],V=N[P],t=N[z];if(W===0){return G(Z,Y,Z,Y)}if(W===1){return G(Z,Y,V,t)}if(W===2){return G(V,t,V,t)}if(W===3){return G(V,t,Z,Y)}}function p(){m(j)}function H(){return m(S)}function d(){return m(E)}function C(Y){var V=this[h],t=this[U],X,W;if((V.nodeType===3||V.nodeType===4)&&V.nodeValue){if(!t){V.parentNode.insertBefore(Y,V)}else{if(t>=V.nodeValue.length){c.insertAfter(Y,V)}else{X=V.splitText(t);V.parentNode.insertBefore(Y,X)}}}else{if(V.childNodes.length>0){W=V.childNodes[t]}if(W){V.insertBefore(Y,W)}else{V.appendChild(Y)}}}function M(V){var t=N.extractContents();N.insertNode(V);V.appendChild(t);N.selectNode(V)}function K(){return k(new b(c),{startContainer:N[h],startOffset:N[U],endContainer:N[P],endOffset:N[z],collapsed:N.collapsed,commonAncestorContainer:N.commonAncestorContainer})}function O(t,V){var W;if(t.nodeType==3){return t}if(V<0){return t}W=t.firstChild;while(W&&V>0){--V;W=W.nextSibling}if(W){return W}return t}function l(){return(N[h]==N[P]&&N[U]==N[z])}function G(X,Z,V,Y){var aa,W,t,ab,ad,ac;if(X==V){if(Z==Y){return 0}if(Z0){N.collapse(V)}}else{N.collapse(V)}N.collapsed=l();N.commonAncestorContainer=c.findCommonAncestor(N[h],N[P])}function m(ab){var aa,X=0,ad=0,V,Z,W,Y,t,ac;if(N[h]==N[P]){return f(ab)}for(aa=N[P],V=aa.parentNode;V;aa=V,V=V.parentNode){if(V==N[h]){return r(aa,ab)}++X}for(aa=N[h],V=aa.parentNode;V;aa=V,V=V.parentNode){if(V==N[P]){return T(aa,ab)}++ad}Z=ad-X;W=N[h];while(Z>0){W=W.parentNode;Z--}Y=N[P];while(Z<0){Y=Y.parentNode;Z++}for(t=W.parentNode,ac=Y.parentNode;t!=ac;t=t.parentNode,ac=ac.parentNode){W=t;Y=ac}return o(W,Y,ab)}function f(Z){var ab,Y,X,aa,t,W,V;if(Z!=j){ab=e.createDocumentFragment()}if(N[U]==N[z]){return ab}if(N[h].nodeType==3){Y=N[h].nodeValue;X=Y.substring(N[U],N[z]);if(Z!=E){N[h].deleteData(N[U],N[z]-N[U]);N.collapse(D)}if(Z==j){return}ab.appendChild(e.createTextNode(X));return ab}aa=O(N[h],N[U]);t=N[z]-N[U];while(t>0){W=aa.nextSibling;V=y(aa,Z);if(ab){ab.appendChild(V)}--t;aa=W}if(Z!=E){N.collapse(D)}return ab}function r(ab,Y){var aa,Z,V,t,X,W;if(Y!=j){aa=e.createDocumentFragment()}Z=i(ab,Y);if(aa){aa.appendChild(Z)}V=n(ab);t=V-N[U];if(t<=0){if(Y!=E){N.setEndBefore(ab);N.collapse(R)}return aa}Z=ab.previousSibling;while(t>0){X=Z.previousSibling;W=y(Z,Y);if(aa){aa.insertBefore(W,aa.firstChild)}--t;Z=X}if(Y!=E){N.setEndBefore(ab);N.collapse(R)}return aa}function T(Z,Y){var ab,V,aa,t,X,W;if(Y!=j){ab=e.createDocumentFragment()}aa=Q(Z,Y);if(ab){ab.appendChild(aa)}V=n(Z);++V;t=N[z]-V;aa=Z.nextSibling;while(t>0){X=aa.nextSibling;W=y(aa,Y);if(ab){ab.appendChild(W)}--t;aa=X}if(Y!=E){N.setStartAfter(Z);N.collapse(D)}return ab}function o(Z,t,ac){var W,ae,Y,aa,ab,V,ad,X;if(ac!=j){ae=e.createDocumentFragment()}W=Q(Z,ac);if(ae){ae.appendChild(W)}Y=Z.parentNode;aa=n(Z);ab=n(t);++aa;V=ab-aa;ad=Z.nextSibling;while(V>0){X=ad.nextSibling;W=y(ad,ac);if(ae){ae.appendChild(W)}ad=X;--V}W=i(t,ac);if(ae){ae.appendChild(W)}if(ac!=E){N.setStartAfter(Z);N.collapse(D)}return ae}function i(aa,ab){var W=O(N[P],N[z]-1),ac,Z,Y,t,V,X=W!=N[P];if(W==aa){return L(W,X,R,ab)}ac=W.parentNode;Z=L(ac,R,R,ab);while(ac){while(W){Y=W.previousSibling;t=L(W,X,R,ab);if(ab!=j){Z.insertBefore(t,Z.firstChild)}X=D;W=Y}if(ac==aa){return Z}W=ac.previousSibling;ac=ac.parentNode;V=L(ac,R,R,ab);if(ab!=j){V.appendChild(Z)}Z=V}}function Q(aa,ab){var X=O(N[h],N[U]),Y=X!=N[h],ac,Z,W,t,V;if(X==aa){return L(X,Y,D,ab)}ac=X.parentNode;Z=L(ac,R,D,ab);while(ac){while(X){W=X.nextSibling;t=L(X,Y,D,ab);if(ab!=j){Z.appendChild(t)}Y=D;X=W}if(ac==aa){return Z}X=ac.nextSibling;ac=ac.parentNode;V=L(ac,R,D,ab);if(ab!=j){V.appendChild(Z)}Z=V}}function L(t,Y,ab,ac){var X,W,Z,V,aa;if(Y){return y(t,ac)}if(t.nodeType==3){X=t.nodeValue;if(ab){V=N[U];W=X.substring(V);Z=X.substring(0,V)}else{V=N[z];W=X.substring(0,V);Z=X.substring(V)}if(ac!=E){t.nodeValue=Z}if(ac==j){return}aa=t.cloneNode(R);aa.nodeValue=W;return aa}if(ac==j){return}return t.cloneNode(R)}function y(V,t){if(t!=j){return t==E?V.cloneNode(D):V}V.parentNode.removeChild(V)}}a.Range=b})(tinymce.dom);(function(){function a(g){var i=this,j="\uFEFF",e,h,d=g.dom,c=true,f=false;function b(){var n=g.getRng(),k=d.createRng(),m,o;m=n.item?n.item(0):n.parentElement();if(m.ownerDocument!=d.doc){return k}if(n.item||!m.hasChildNodes()){k.setStart(m.parentNode,d.nodeIndex(m));k.setEnd(k.startContainer,k.startOffset+1);return k}o=g.isCollapsed();function l(s){var u,q,t,p,A=0,x,y,z,r,v;r=n.duplicate();r.collapse(s);u=d.create("a");z=r.parentElement();if(!z.hasChildNodes()){k[s?"setStart":"setEnd"](z,0);return}z.appendChild(u);r.moveToElementText(u);v=n.compareEndPoints(s?"StartToStart":"EndToEnd",r);if(v>0){k[s?"setStartAfter":"setEndAfter"](z);d.remove(u);return}p=tinymce.grep(z.childNodes);x=p.length-1;while(A<=x){y=Math.floor((A+x)/2);z.insertBefore(u,p[y]);r.moveToElementText(u);v=n.compareEndPoints(s?"StartToStart":"EndToEnd",r);if(v>0){A=y+1}else{if(v<0){x=y-1}else{found=true;break}}}q=v>0||y==0?u.nextSibling:u.previousSibling;if(q.nodeType==1){d.remove(u);t=d.nodeIndex(q);q=q.parentNode;if(!s||y>0){t++}}else{if(v>0||y==0){r.setEndPoint(s?"StartToStart":"EndToEnd",n);t=r.text.length}else{r.setEndPoint(s?"StartToStart":"EndToEnd",n);t=q.nodeValue.length-r.text.length}d.remove(u)}k[s?"setStart":"setEnd"](q,t)}l(true);if(!o){l()}return k}this.addRange=function(l){var t,A,z=g.dom.doc,r=z.body,u,n,y,o,s,k,p,q,x,m;this.destroy();y=l.startContainer;o=l.startOffset;s=l.endContainer;k=l.endOffset;t=r.createTextRange();if(y==z||s==z){t=r.createTextRange();t.collapse();t.select();return}if(y.nodeType==1&&y.hasChildNodes()){q=y.childNodes.length-1;if(o>q){x=1;y=y.childNodes[q]}else{y=y.childNodes[o]}if(y.nodeType==3){o=0}}if(s.nodeType==1&&s.hasChildNodes()){q=s.childNodes.length-1;if(k==0){m=1;s=s.childNodes[0]}else{s=s.childNodes[Math.min(q,k-1)];if(s.nodeType==3){k=s.nodeValue.length}}}if(y==s&&y.nodeType==1){if(/^(IMG|TABLE)$/.test(y.nodeName)&&o!=k){t=r.createControlRange();t.addElement(y)}else{t=r.createTextRange();if(!y.hasChildNodes()&&y.canHaveHTML){y.innerHTML=j}t.moveToElementText(y);if(y.innerHTML==j){t.collapse(c);y.removeChild(y.firstChild)}}if(o==k){t.collapse(k<=l.endContainer.childNodes.length-1)}t.select();t.scrollIntoView();return}t=r.createTextRange();p=z.createElement("span");p.innerHTML=" ";if(y.nodeType==3){if(x){d.insertAfter(p,y)}else{y.parentNode.insertBefore(p,y)}t.moveToElementText(p);p.parentNode.removeChild(p);if(o>0){t.move("character",o)}}else{t.moveToElementText(y);if(x){t.collapse(f)}}if(y==s&&y.nodeType==3){try{t.moveEnd("character",k-o);t.select();t.scrollIntoView()}catch(v){}return}A=r.createTextRange();if(s.nodeType==3){s.parentNode.insertBefore(p,s);A.moveToElementText(p);p.parentNode.removeChild(p);A.move("character",k);t.setEndPoint("EndToStart",A)}else{A.moveToElementText(s);A.collapse(!!m);t.setEndPoint("EndToEnd",A)}t.select();t.scrollIntoView()};this.getRangeAt=function(){if(!e||!tinymce.dom.RangeUtils.compareRanges(h,g.getRng())){e=b();h=g.getRng()}try{e.startContainer.nextSibling}catch(k){e=b();h=null}return e};this.destroy=function(){h=e=null};if(g.dom.boxModel){(function(){var q=d.doc,l=q.body,n,o;q.documentElement.unselectable=c;function p(r,u){var s=l.createTextRange();try{s.moveToPoint(r,u)}catch(t){s=null}return s}function m(s){var r;if(s.button){r=p(s.x,s.y);if(r){if(r.compareEndPoints("StartToStart",o)>0){r.setEndPoint("StartToStart",o)}else{r.setEndPoint("EndToEnd",o)}r.select()}}else{k()}}function k(){d.unbind(q,"mouseup",k);d.unbind(q,"mousemove",m);n=0}d.bind(q,"mousedown",function(r){if(r.target.nodeName==="HTML"){if(n){k()}n=1;o=p(r.x,r.y);if(o){d.bind(q,"mouseup",k);d.bind(q,"mousemove",m);o.select()}}})})()}}tinymce.dom.TridentSelection=a})();(function(){var p=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,j=0,d=Object.prototype.toString,o=false,i=true;[0,0].sort(function(){i=false;return 0});var b=function(v,e,z,A){z=z||[];e=e||document;var C=e;if(e.nodeType!==1&&e.nodeType!==9){return[]}if(!v||typeof v!=="string"){return z}var x=[],s,E,H,r,u=true,t=b.isXML(e),B=v,D,G,F,y;do{p.exec("");s=p.exec(B);if(s){B=s[3];x.push(s[1]);if(s[2]){r=s[3];break}}}while(s);if(x.length>1&&k.exec(v)){if(x.length===2&&f.relative[x[0]]){E=h(x[0]+x[1],e)}else{E=f.relative[x[0]]?[e]:b(x.shift(),e);while(x.length){v=x.shift();if(f.relative[v]){v+=x.shift()}E=h(v,E)}}}else{if(!A&&x.length>1&&e.nodeType===9&&!t&&f.match.ID.test(x[0])&&!f.match.ID.test(x[x.length-1])){D=b.find(x.shift(),e,t);e=D.expr?b.filter(D.expr,D.set)[0]:D.set[0]}if(e){D=A?{expr:x.pop(),set:a(A)}:b.find(x.pop(),x.length===1&&(x[0]==="~"||x[0]==="+")&&e.parentNode?e.parentNode:e,t);E=D.expr?b.filter(D.expr,D.set):D.set;if(x.length>0){H=a(E)}else{u=false}while(x.length){G=x.pop();F=G;if(!f.relative[G]){G=""}else{F=x.pop()}if(F==null){F=e}f.relative[G](H,F,t)}}else{H=x=[]}}if(!H){H=E}if(!H){b.error(G||v)}if(d.call(H)==="[object Array]"){if(!u){z.push.apply(z,H)}else{if(e&&e.nodeType===1){for(y=0;H[y]!=null;y++){if(H[y]&&(H[y]===true||H[y].nodeType===1&&b.contains(e,H[y]))){z.push(E[y])}}}else{for(y=0;H[y]!=null;y++){if(H[y]&&H[y].nodeType===1){z.push(E[y])}}}}}else{a(H,z)}if(r){b(r,C,z,A);b.uniqueSort(z)}return z};b.uniqueSort=function(r){if(c){o=i;r.sort(c);if(o){for(var e=1;e":function(x,r){var u=typeof r==="string",v,s=0,e=x.length;if(u&&!/\W/.test(r)){r=r.toLowerCase();for(;s=0)){if(!s){e.push(v)}}else{if(s){r[u]=false}}}}return false},ID:function(e){return e[1].replace(/\\/g,"")},TAG:function(r,e){return r[1].toLowerCase()},CHILD:function(e){if(e[1]==="nth"){var r=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(e[2]==="even"&&"2n"||e[2]==="odd"&&"2n+1"||!/\D/.test(e[2])&&"0n+"+e[2]||e[2]);e[2]=(r[1]+(r[2]||1))-0;e[3]=r[3]-0}e[0]=j++;return e},ATTR:function(u,r,s,e,v,x){var t=u[1].replace(/\\/g,"");if(!x&&f.attrMap[t]){u[1]=f.attrMap[t]}if(u[2]==="~="){u[4]=" "+u[4]+" "}return u},PSEUDO:function(u,r,s,e,v){if(u[1]==="not"){if((p.exec(u[3])||"").length>1||/^\w/.test(u[3])){u[3]=b(u[3],null,null,r)}else{var t=b.filter(u[3],r,s,true^v);if(!s){e.push.apply(e,t)}return false}}else{if(f.match.POS.test(u[0])||f.match.CHILD.test(u[0])){return true}}return u},POS:function(e){e.unshift(true);return e}},filters:{enabled:function(e){return e.disabled===false&&e.type!=="hidden"},disabled:function(e){return e.disabled===true},checked:function(e){return e.checked===true},selected:function(e){e.parentNode.selectedIndex;return e.selected===true},parent:function(e){return !!e.firstChild},empty:function(e){return !e.firstChild},has:function(s,r,e){return !!b(e[3],s).length},header:function(e){return(/h\d/i).test(e.nodeName)},text:function(e){return"text"===e.type},radio:function(e){return"radio"===e.type},checkbox:function(e){return"checkbox"===e.type},file:function(e){return"file"===e.type},password:function(e){return"password"===e.type},submit:function(e){return"submit"===e.type},image:function(e){return"image"===e.type},reset:function(e){return"reset"===e.type},button:function(e){return"button"===e.type||e.nodeName.toLowerCase()==="button"},input:function(e){return(/input|select|textarea|button/i).test(e.nodeName)}},setFilters:{first:function(r,e){return e===0},last:function(s,r,e,t){return r===t.length-1},even:function(r,e){return e%2===0},odd:function(r,e){return e%2===1},lt:function(s,r,e){return re[3]-0},nth:function(s,r,e){return e[3]-0===r},eq:function(s,r,e){return e[3]-0===r}},filter:{PSEUDO:function(s,y,x,z){var e=y[1],r=f.filters[e];if(r){return r(s,x,y,z)}else{if(e==="contains"){return(s.textContent||s.innerText||b.getText([s])||"").indexOf(y[3])>=0}else{if(e==="not"){var t=y[3];for(var v=0,u=t.length;v=0)}}},ID:function(r,e){return r.nodeType===1&&r.getAttribute("id")===e},TAG:function(r,e){return(e==="*"&&r.nodeType===1)||r.nodeName.toLowerCase()===e},CLASS:function(r,e){return(" "+(r.className||r.getAttribute("class"))+" ").indexOf(e)>-1},ATTR:function(v,t){var s=t[1],e=f.attrHandle[s]?f.attrHandle[s](v):v[s]!=null?v[s]:v.getAttribute(s),x=e+"",u=t[2],r=t[4];return e==null?u==="!=":u==="="?x===r:u==="*="?x.indexOf(r)>=0:u==="~="?(" "+x+" ").indexOf(r)>=0:!r?x&&e!==false:u==="!="?x!==r:u==="^="?x.indexOf(r)===0:u==="$="?x.substr(x.length-r.length)===r:u==="|="?x===r||x.substr(0,r.length+1)===r+"-":false},POS:function(u,r,s,v){var e=r[2],t=f.setFilters[e];if(t){return t(u,s,r,v)}}}};var k=f.match.POS,g=function(r,e){return"\\"+(e-0+1)};for(var m in f.match){f.match[m]=new RegExp(f.match[m].source+(/(?![^\[]*\])(?![^\(]*\))/.source));f.leftMatch[m]=new RegExp(/(^(?:.|\r|\n)*?)/.source+f.match[m].source.replace(/\\(\d+)/g,g))}var a=function(r,e){r=Array.prototype.slice.call(r,0);if(e){e.push.apply(e,r);return e}return r};try{Array.prototype.slice.call(document.documentElement.childNodes,0)[0].nodeType}catch(l){a=function(u,t){var r=t||[],s=0;if(d.call(u)==="[object Array]"){Array.prototype.push.apply(r,u)}else{if(typeof u.length==="number"){for(var e=u.length;s";var e=document.documentElement;e.insertBefore(r,e.firstChild);if(document.getElementById(s)){f.find.ID=function(u,v,x){if(typeof v.getElementById!=="undefined"&&!x){var t=v.getElementById(u[1]);return t?t.id===u[1]||typeof t.getAttributeNode!=="undefined"&&t.getAttributeNode("id").nodeValue===u[1]?[t]:undefined:[]}};f.filter.ID=function(v,t){var u=typeof v.getAttributeNode!=="undefined"&&v.getAttributeNode("id");return v.nodeType===1&&u&&u.nodeValue===t}}e.removeChild(r);e=r=null})();(function(){var e=document.createElement("div");e.appendChild(document.createComment(""));if(e.getElementsByTagName("*").length>0){f.find.TAG=function(r,v){var u=v.getElementsByTagName(r[1]);if(r[1]==="*"){var t=[];for(var s=0;u[s];s++){if(u[s].nodeType===1){t.push(u[s])}}u=t}return u}}e.innerHTML="";if(e.firstChild&&typeof e.firstChild.getAttribute!=="undefined"&&e.firstChild.getAttribute("href")!=="#"){f.attrHandle.href=function(r){return r.getAttribute("href",2)}}e=null})();if(document.querySelectorAll){(function(){var e=b,s=document.createElement("div");s.innerHTML="

    ";if(s.querySelectorAll&&s.querySelectorAll(".TEST").length===0){return}b=function(x,v,t,u){v=v||document;if(!u&&v.nodeType===9&&!b.isXML(v)){try{return a(v.querySelectorAll(x),t)}catch(y){}}return e(x,v,t,u)};for(var r in e){b[r]=e[r]}s=null})()}(function(){var e=document.createElement("div");e.innerHTML="
    ";if(!e.getElementsByClassName||e.getElementsByClassName("e").length===0){return}e.lastChild.className="e";if(e.getElementsByClassName("e").length===1){return}f.order.splice(1,0,"CLASS");f.find.CLASS=function(r,s,t){if(typeof s.getElementsByClassName!=="undefined"&&!t){return s.getElementsByClassName(r[1])}};e=null})();function n(r,x,v,A,y,z){for(var t=0,s=A.length;t0){u=e;break}}}e=e[r]}A[t]=u}}}b.contains=document.compareDocumentPosition?function(r,e){return !!(r.compareDocumentPosition(e)&16)}:function(r,e){return r!==e&&(r.contains?r.contains(e):true)};b.isXML=function(e){var r=(e?e.ownerDocument||e:0).documentElement;return r?r.nodeName!=="HTML":false};var h=function(e,y){var t=[],u="",v,s=y.nodeType?[y]:y;while((v=f.match.PSEUDO.exec(e))){u+=v[0];e=e.replace(f.match.PSEUDO,"")}e=f.relative[e]?e+"*":e;for(var x=0,r=s.length;x=0;h--){k=g[h];if(k.obj===l){j._remove(k.obj,k.name,k.cfunc);k.obj=k.cfunc=null;g.splice(h,1)}}}},cancel:function(g){if(!g){return false}this.stop(g);return this.prevent(g)},stop:function(g){if(g.stopPropagation){g.stopPropagation()}else{g.cancelBubble=true}return false},prevent:function(g){if(g.preventDefault){g.preventDefault()}else{g.returnValue=false}return false},destroy:function(){var g=this;f(g.events,function(j,h){g._remove(j.obj,j.name,j.cfunc);j.obj=j.cfunc=null});g.events=[];g=null},_add:function(h,i,g){if(h.attachEvent){h.attachEvent("on"+i,g)}else{if(h.addEventListener){h.addEventListener(i,g,false)}else{h["on"+i]=g}}},_remove:function(i,j,h){if(i){try{if(i.detachEvent){i.detachEvent("on"+j,h)}else{if(i.removeEventListener){i.removeEventListener(j,h,false)}else{i["on"+j]=null}}}catch(g){}}},_pageInit:function(h){var g=this;if(g.domLoaded){return}g.domLoaded=true;f(g.inits,function(i){i()});g.inits=[]},_wait:function(i){var g=this,h=i.document;if(i.tinyMCE_GZ&&tinyMCE_GZ.loaded){g.domLoaded=1;return}if(h.attachEvent){h.attachEvent("onreadystatechange",function(){if(h.readyState==="complete"){h.detachEvent("onreadystatechange",arguments.callee);g._pageInit(i)}});if(h.documentElement.doScroll&&i==i.top){(function(){if(g.domLoaded){return}try{h.documentElement.doScroll("left")}catch(j){setTimeout(arguments.callee,0);return}g._pageInit(i)})()}}else{if(h.addEventListener){g._add(i,"DOMContentLoaded",function(){g._pageInit(i)})}}g._add(i,"load",function(){g._pageInit(i)})},_stoppers:{preventDefault:function(){this.returnValue=false},stopPropagation:function(){this.cancelBubble=true}}});a=d.dom.Event=new d.dom.EventUtils();a._wait(window);d.addUnload(function(){a.destroy()})})(tinymce);(function(a){a.dom.Element=function(f,d){var b=this,e,c;b.settings=d=d||{};b.id=f;b.dom=e=d.dom||a.DOM;if(!a.isIE){c=e.get(b.id)}a.each(("getPos,getRect,getParent,add,setStyle,getStyle,setStyles,setAttrib,setAttribs,getAttrib,addClass,removeClass,hasClass,getOuterHTML,setOuterHTML,remove,show,hide,isHidden,setHTML,get").split(/,/),function(g){b[g]=function(){var h=[f],j;for(j=0;j_';if(j.startContainer==k&&j.endContainer==k){k.body.innerHTML=i}else{j.deleteContents();if(k.body.childNodes.length==0){k.body.innerHTML=i}else{j.insertNode(j.createContextualFragment(i))}}l=f.dom.get("__caret");j=k.createRange();j.setStartBefore(l);j.setEndBefore(l);f.setRng(j);f.dom.remove("__caret")}else{if(j.item){k.execCommand("Delete",false,null);j=f.getRng()}j.pasteHTML(i)}f.onSetContent.dispatch(f,g)},getStart:function(){var g=this.getRng(),h,f,j,i;if(g.duplicate||g.item){if(g.item){return g.item(0)}j=g.duplicate();j.collapse(1);h=j.parentElement();f=i=g.parentElement();while(i=i.parentNode){if(i==h){h=f;break}}if(h&&h.nodeName=="BODY"){return h.firstChild||h}return h}else{h=g.startContainer;if(h.nodeType==1&&h.hasChildNodes()){h=h.childNodes[Math.min(h.childNodes.length-1,g.startOffset)]}if(h&&h.nodeType==3){return h.parentNode}return h}},getEnd:function(){var g=this,h=g.getRng(),i,f;if(h.duplicate||h.item){if(h.item){return h.item(0)}h=h.duplicate();h.collapse(0);i=h.parentElement();if(i&&i.nodeName=="BODY"){return i.lastChild||i}return i}else{i=h.endContainer;f=h.endOffset;if(i.nodeType==1&&i.hasChildNodes()){i=i.childNodes[f>0?f-1:f]}if(i&&i.nodeType==3){return i.parentNode}return i}},getBookmark:function(q,r){var u=this,m=u.dom,g,j,i,n,h,o,p,l="\uFEFF",s;function f(v,x){var t=0;d(m.select(v),function(z,y){if(z==x){t=y}});return t}if(q==2){function k(){var v=u.getRng(true),t=m.getRoot(),x={};function y(B,G){var A=B[G?"startContainer":"endContainer"],F=B[G?"startOffset":"endOffset"],z=[],C,E,D=0;if(A.nodeType==3){if(r){for(C=A.previousSibling;C&&C.nodeType==3;C=C.previousSibling){F+=C.nodeValue.length}}z.push(F)}else{E=A.childNodes;if(F>=E.length&&E.length){D=1;F=Math.max(0,E.length-1)}z.push(u.dom.nodeIndex(E[F],r)+D)}for(;A&&A!=t;A=A.parentNode){z.push(u.dom.nodeIndex(A,r))}return z}x.start=y(v,true);if(!u.isCollapsed()){x.end=y(v)}return x}return k()}if(q){return{rng:u.getRng()}}g=u.getRng();i=m.uniqueId();n=tinyMCE.activeEditor.selection.isCollapsed();s="overflow:hidden;line-height:0px";if(g.duplicate||g.item){if(!g.item){j=g.duplicate();g.collapse();g.pasteHTML(''+l+"");if(!n){j.collapse(false);j.pasteHTML(''+l+"")}}else{o=g.item(0);h=o.nodeName;return{name:h,index:f(h,o)}}}else{o=u.getNode();h=o.nodeName;if(h=="IMG"){return{name:h,index:f(h,o)}}j=g.cloneRange();if(!n){j.collapse(false);j.insertNode(m.create("span",{_mce_type:"bookmark",id:i+"_end",style:s},l))}g.collapse(true);g.insertNode(m.create("span",{_mce_type:"bookmark",id:i+"_start",style:s},l))}u.moveToBookmark({id:i,keep:1});return{id:i}},moveToBookmark:function(n){var r=this,l=r.dom,i,h,f,q,j,s,o,p;if(r.tridentSel){r.tridentSel.destroy()}if(n){if(n.start){f=l.createRng();q=l.getRoot();function g(z){var t=n[z?"start":"end"],v,x,y,u;if(t){for(x=q,v=t.length-1;v>=1;v--){u=x.childNodes;if(u.length){x=u[t[v]]}}if(z){f.setStart(x,t[0])}else{f.setEnd(x,t[0])}}}g(true);g();r.setRng(f)}else{if(n.id){function k(A){var u=l.get(n.id+"_"+A),z,t,x,y,v=n.keep;if(u){z=u.parentNode;if(A=="start"){if(!v){t=l.nodeIndex(u)}else{z=u.firstChild;t=1}j=s=z;o=p=t}else{if(!v){t=l.nodeIndex(u)}else{z=u.firstChild;t=1}s=z;p=t}if(!v){y=u.previousSibling;x=u.nextSibling;d(c.grep(u.childNodes),function(B){if(B.nodeType==3){B.nodeValue=B.nodeValue.replace(/\uFEFF/g,"")}});while(u=l.get(n.id+"_"+A)){l.remove(u,1)}if(y&&x&&y.nodeType==x.nodeType&&y.nodeType==3){t=y.nodeValue.length;y.appendData(x.nodeValue);l.remove(x);if(A=="start"){j=s=y;o=p=t}else{s=y;p=t}}}}}function m(t){if(!a&&l.isBlock(t)&&!t.innerHTML){t.innerHTML='
    '}return t}k("start");k("end");f=l.createRng();f.setStart(m(j),o);f.setEnd(m(s),p);r.setRng(f)}else{if(n.name){r.select(l.select(n.name)[n.index])}else{if(n.rng){r.setRng(n.rng)}}}}}},select:function(k,j){var i=this,l=i.dom,g=l.createRng(),f;f=l.nodeIndex(k);g.setStart(k.parentNode,f);g.setEnd(k.parentNode,f+1);if(j){function h(m,o){var n=new c.dom.TreeWalker(m,m);do{if(m.nodeType==3&&c.trim(m.nodeValue).length!=0){if(o){g.setStart(m,0)}else{g.setEnd(m,m.nodeValue.length)}return}if(m.nodeName=="BR"){if(o){g.setStartBefore(m)}else{g.setEndBefore(m)}return}}while(m=(o?n.next():n.prev()))}h(k,1);h(k)}i.setRng(g);return k},isCollapsed:function(){var f=this,h=f.getRng(),g=f.getSel();if(!h||h.item){return false}if(h.compareEndPoints){return h.compareEndPoints("StartToEnd",h)===0}return !g||h.collapsed},collapse:function(f){var g=this,h=g.getRng(),i;if(h.item){i=h.item(0);h=this.win.document.body.createTextRange();h.moveToElementText(i)}h.collapse(!!f);g.setRng(h)},getSel:function(){var g=this,f=this.win;return f.getSelection?f.getSelection():f.document.selection},getRng:function(j){var g=this,h,i;if(j&&g.tridentSel){return g.tridentSel.getRangeAt(0)}try{if(h=g.getSel()){i=h.rangeCount>0?h.getRangeAt(0):(h.createRange?h.createRange():g.win.document.createRange())}}catch(f){}if(!i){i=g.win.document.createRange?g.win.document.createRange():g.win.document.body.createTextRange()}if(g.selectedRange&&g.explicitRange){if(i.compareBoundaryPoints(i.START_TO_START,g.selectedRange)===0&&i.compareBoundaryPoints(i.END_TO_END,g.selectedRange)===0){i=g.explicitRange}else{g.selectedRange=null;g.explicitRange=null}}return i},setRng:function(i){var h,g=this;if(!g.tridentSel){h=g.getSel();if(h){g.explicitRange=i;h.removeAllRanges();h.addRange(i);g.selectedRange=h.getRangeAt(0)}}else{if(i.cloneRange){g.tridentSel.addRange(i);return}try{i.select()}catch(f){}}},setNode:function(g){var f=this;f.setContent(f.dom.getOuterHTML(g));return g},getNode:function(){var g=this,f=g.getRng(),h=g.getSel(),i;if(f.setStart){if(!f){return g.dom.getRoot()}i=f.commonAncestorContainer;if(!f.collapsed){if(f.startContainer==f.endContainer){if(f.startOffset-f.endOffset<2){if(f.startContainer.hasChildNodes()){i=f.startContainer.childNodes[f.startOffset]}}}if(c.isWebKit&&h.anchorNode&&h.anchorNode.nodeType==1){return h.anchorNode.childNodes[h.anchorOffset]}}if(i&&i.nodeType==3){return i.parentNode}return i}return f.item?f.item(0):f.parentElement()},getSelectedBlocks:function(g,f){var i=this,j=i.dom,m,h,l,k=[];m=j.getParent(g||i.getStart(),j.isBlock);h=j.getParent(f||i.getEnd(),j.isBlock);if(m){k.push(m)}if(m&&h&&m!=h){l=m;while((l=l.nextSibling)&&l!=h){if(j.isBlock(l)){k.push(l)}}}if(h&&m!=h){k.push(h)}return k},destroy:function(g){var f=this;f.win=null;if(f.tridentSel){f.tridentSel.destroy()}if(!g){c.removeUnload(f.destroy)}}})})(tinymce);(function(a){a.create("tinymce.dom.XMLWriter",{node:null,XMLWriter:function(c){function b(){var e=document.implementation;if(!e||!e.createDocument){try{return new ActiveXObject("MSXML2.DOMDocument")}catch(d){}try{return new ActiveXObject("Microsoft.XmlDom")}catch(d){}}else{return e.createDocument("","",null)}}this.doc=b();this.valid=a.isOpera||a.isWebKit;this.reset()},reset:function(){var b=this,c=b.doc;if(c.firstChild){c.removeChild(c.firstChild)}b.node=c.appendChild(c.createElement("html"))},writeStartElement:function(c){var b=this;b.node=b.node.appendChild(b.doc.createElement(c))},writeAttribute:function(c,b){if(this.valid){b=b.replace(/>/g,"%MCGT%")}this.node.setAttribute(c,b)},writeEndElement:function(){this.node=this.node.parentNode},writeFullEndElement:function(){var b=this,c=b.node;c.appendChild(b.doc.createTextNode(""));b.node=c.parentNode},writeText:function(b){if(this.valid){b=b.replace(/>/g,"%MCGT%")}this.node.appendChild(this.doc.createTextNode(b))},writeCDATA:function(b){this.node.appendChild(this.doc.createCDATASection(b))},writeComment:function(b){if(a.isIE){b=b.replace(/^\-|\-$/g," ")}this.node.appendChild(this.doc.createComment(b.replace(/\-\-/g," ")))},getContent:function(){var b;b=this.doc.xml||new XMLSerializer().serializeToString(this.doc);b=b.replace(/<\?[^?]+\?>||<\/html>||]+>/g,"");b=b.replace(/ ?\/>/g," />");if(this.valid){b=b.replace(/\%MCGT%/g,">")}return b}})})(tinymce);(function(a){a.create("tinymce.dom.StringWriter",{str:null,tags:null,count:0,settings:null,indent:null,StringWriter:function(b){this.settings=a.extend({indent_char:" ",indentation:0},b);this.reset()},reset:function(){this.indent="";this.str="";this.tags=[];this.count=0},writeStartElement:function(b){this._writeAttributesEnd();this.writeRaw("<"+b);this.tags.push(b);this.inAttr=true;this.count++;this.elementCount=this.count},writeAttribute:function(d,b){var c=this;c.writeRaw(" "+c.encode(d)+'="'+c.encode(b)+'"')},writeEndElement:function(){var b;if(this.tags.length>0){b=this.tags.pop();if(this._writeAttributesEnd(1)){this.writeRaw("")}if(this.settings.indentation>0){this.writeRaw("\n")}}},writeFullEndElement:function(){if(this.tags.length>0){this._writeAttributesEnd();this.writeRaw("");if(this.settings.indentation>0){this.writeRaw("\n")}}},writeText:function(b){this._writeAttributesEnd();this.writeRaw(this.encode(b));this.count++},writeCDATA:function(b){this._writeAttributesEnd();this.writeRaw("");this.count++},writeComment:function(b){this._writeAttributesEnd();this.writeRaw("");this.count++},writeRaw:function(b){this.str+=b},encode:function(b){return b.replace(/[<>&"]/g,function(c){switch(c){case"<":return"<";case">":return">";case"&":return"&";case'"':return"""}return c})},getContent:function(){return this.str},_writeAttributesEnd:function(b){if(!this.inAttr){return}this.inAttr=false;if(b&&this.elementCount==this.count){this.writeRaw(" />");return false}this.writeRaw(">");return true}})})(tinymce);(function(e){var g=e.extend,f=e.each,b=e.util.Dispatcher,d=e.isIE,a=e.isGecko;function c(h){return h.replace(/([?+*])/g,".$1")}e.create("tinymce.dom.Serializer",{Serializer:function(j){var i=this;i.key=0;i.onPreProcess=new b(i);i.onPostProcess=new b(i);try{i.writer=new e.dom.XMLWriter()}catch(h){i.writer=new e.dom.StringWriter()}i.settings=j=g({dom:e.DOM,valid_nodes:0,node_filter:0,attr_filter:0,invalid_attrs:/^(_mce_|_moz_|sizset|sizcache)/,closed:/^(br|hr|input|meta|img|link|param|area)$/,entity_encoding:"named",entities:"160,nbsp,161,iexcl,162,cent,163,pound,164,curren,165,yen,166,brvbar,167,sect,168,uml,169,copy,170,ordf,171,laquo,172,not,173,shy,174,reg,175,macr,176,deg,177,plusmn,178,sup2,179,sup3,180,acute,181,micro,182,para,183,middot,184,cedil,185,sup1,186,ordm,187,raquo,188,frac14,189,frac12,190,frac34,191,iquest,192,Agrave,193,Aacute,194,Acirc,195,Atilde,196,Auml,197,Aring,198,AElig,199,Ccedil,200,Egrave,201,Eacute,202,Ecirc,203,Euml,204,Igrave,205,Iacute,206,Icirc,207,Iuml,208,ETH,209,Ntilde,210,Ograve,211,Oacute,212,Ocirc,213,Otilde,214,Ouml,215,times,216,Oslash,217,Ugrave,218,Uacute,219,Ucirc,220,Uuml,221,Yacute,222,THORN,223,szlig,224,agrave,225,aacute,226,acirc,227,atilde,228,auml,229,aring,230,aelig,231,ccedil,232,egrave,233,eacute,234,ecirc,235,euml,236,igrave,237,iacute,238,icirc,239,iuml,240,eth,241,ntilde,242,ograve,243,oacute,244,ocirc,245,otilde,246,ouml,247,divide,248,oslash,249,ugrave,250,uacute,251,ucirc,252,uuml,253,yacute,254,thorn,255,yuml,402,fnof,913,Alpha,914,Beta,915,Gamma,916,Delta,917,Epsilon,918,Zeta,919,Eta,920,Theta,921,Iota,922,Kappa,923,Lambda,924,Mu,925,Nu,926,Xi,927,Omicron,928,Pi,929,Rho,931,Sigma,932,Tau,933,Upsilon,934,Phi,935,Chi,936,Psi,937,Omega,945,alpha,946,beta,947,gamma,948,delta,949,epsilon,950,zeta,951,eta,952,theta,953,iota,954,kappa,955,lambda,956,mu,957,nu,958,xi,959,omicron,960,pi,961,rho,962,sigmaf,963,sigma,964,tau,965,upsilon,966,phi,967,chi,968,psi,969,omega,977,thetasym,978,upsih,982,piv,8226,bull,8230,hellip,8242,prime,8243,Prime,8254,oline,8260,frasl,8472,weierp,8465,image,8476,real,8482,trade,8501,alefsym,8592,larr,8593,uarr,8594,rarr,8595,darr,8596,harr,8629,crarr,8656,lArr,8657,uArr,8658,rArr,8659,dArr,8660,hArr,8704,forall,8706,part,8707,exist,8709,empty,8711,nabla,8712,isin,8713,notin,8715,ni,8719,prod,8721,sum,8722,minus,8727,lowast,8730,radic,8733,prop,8734,infin,8736,ang,8743,and,8744,or,8745,cap,8746,cup,8747,int,8756,there4,8764,sim,8773,cong,8776,asymp,8800,ne,8801,equiv,8804,le,8805,ge,8834,sub,8835,sup,8836,nsub,8838,sube,8839,supe,8853,oplus,8855,otimes,8869,perp,8901,sdot,8968,lceil,8969,rceil,8970,lfloor,8971,rfloor,9001,lang,9002,rang,9674,loz,9824,spades,9827,clubs,9829,hearts,9830,diams,338,OElig,339,oelig,352,Scaron,353,scaron,376,Yuml,710,circ,732,tilde,8194,ensp,8195,emsp,8201,thinsp,8204,zwnj,8205,zwj,8206,lrm,8207,rlm,8211,ndash,8212,mdash,8216,lsquo,8217,rsquo,8218,sbquo,8220,ldquo,8221,rdquo,8222,bdquo,8224,dagger,8225,Dagger,8240,permil,8249,lsaquo,8250,rsaquo,8364,euro",valid_elements:"*[*]",extended_valid_elements:0,invalid_elements:0,fix_table_elements:1,fix_list_elements:true,fix_content_duplication:true,convert_fonts_to_spans:false,font_size_classes:0,apply_source_formatting:0,indent_mode:"simple",indent_char:"\t",indent_levels:1,remove_linebreaks:1,remove_redundant_brs:1,element_format:"xhtml"},j);i.dom=j.dom;i.schema=j.schema;if(j.entity_encoding=="named"&&!j.entities){j.entity_encoding="raw"}if(j.remove_redundant_brs){i.onPostProcess.add(function(k,l){l.content=l.content.replace(/(
    \s*)+<\/(p|h[1-6]|div|li)>/gi,function(n,m,o){if(/^
    \s*<\//.test(n)){return""}return n})})}if(j.element_format=="html"){i.onPostProcess.add(function(k,l){l.content=l.content.replace(/<([^>]+) \/>/g,"<$1>")})}if(j.fix_list_elements){i.onPreProcess.add(function(v,s){var l,z,y=["ol","ul"],u,t,q,k=/^(OL|UL)$/,A;function m(r,x){var o=x.split(","),p;while((r=r.previousSibling)!=null){for(p=0;p=1767){f(i.dom.select("p table",l.node).reverse(),function(p){var o=i.dom.getParent(p.parentNode,"table,p");if(o.nodeName!="TABLE"){try{i.dom.split(o,p)}catch(m){}}})}})}},setEntities:function(o){var n=this,j,m,h={},k;if(n.entityLookup){return}j=o.split(",");for(m=0;m1){f(q[1].split("|"),function(u){var p={},t;k=k||[];u=u.replace(/::/g,"~");u=/^([!\-])?([\w*.?~_\-]+|)([=:<])?(.+)?$/.exec(u);u[2]=u[2].replace(/~/g,":");if(u[1]=="!"){r=r||[];r.push(u[2])}if(u[1]=="-"){for(t=0;t=1767)){p=j.createHTMLDocument("");f(r.nodeName=="BODY"?r.childNodes:[r],function(h){p.body.appendChild(p.importNode(h,true))});if(r.nodeName!="BODY"){r=p.body.firstChild}else{r=p.body}i=k.dom.doc;k.dom.doc=p}k.key=""+(parseInt(k.key)+1);if(!q.no_events){q.node=r;k.onPreProcess.dispatch(k,q)}k.writer.reset();k._info=q;k._serializeNode(r,q.getInner);q.content=k.writer.getContent();if(i){k.dom.doc=i}if(!q.no_events){k.onPostProcess.dispatch(k,q)}k._postProcess(q);q.node=null;return e.trim(q.content)},_postProcess:function(n){var i=this,k=i.settings,j=n.content,m=[],l;if(n.format=="html"){l=i._protect({content:j,patterns:[{pattern:/(]*>)(.*?)(<\/script>)/g},{pattern:/(]*>)(.*?)(<\/noscript>)/g},{pattern:/(]*>)(.*?)(<\/style>)/g},{pattern:/(]*>)(.*?)(<\/pre>)/g,encode:1},{pattern:/()/g}]});j=l.content;if(k.entity_encoding!=="raw"){j=i._encode(j)}if(!n.set){j=j.replace(/

    \s+<\/p>|]+)>\s+<\/p>/g,k.entity_encoding=="numeric"?" 

    ":" 

    ");if(k.remove_linebreaks){j=j.replace(/\r?\n|\r/g," ");j=j.replace(/(<[^>]+>)\s+/g,"$1 ");j=j.replace(/\s+(<\/[^>]+>)/g," $1");j=j.replace(/<(p|h[1-6]|blockquote|hr|div|table|tbody|tr|td|body|head|html|title|meta|style|pre|script|link|object) ([^>]+)>\s+/g,"<$1 $2>");j=j.replace(/<(p|h[1-6]|blockquote|hr|div|table|tbody|tr|td|body|head|html|title|meta|style|pre|script|link|object)>\s+/g,"<$1>");j=j.replace(/\s+<\/(p|h[1-6]|blockquote|hr|div|table|tbody|tr|td|body|head|html|title|meta|style|pre|script|link|object)>/g,"")}if(k.apply_source_formatting&&k.indent_mode=="simple"){j=j.replace(/<(\/?)(ul|hr|table|meta|link|tbody|tr|object|body|head|html|map)(|[^>]+)>\s*/g,"\n<$1$2$3>\n");j=j.replace(/\s*<(p|h[1-6]|blockquote|div|title|style|pre|script|td|li|area)(|[^>]+)>/g,"\n<$1$2>");j=j.replace(/<\/(p|h[1-6]|blockquote|div|title|style|pre|script|td|li)>\s*/g,"\n");j=j.replace(/\n\n/g,"\n")}}j=i._unprotect(j,l);j=j.replace(//g,"");if(k.entity_encoding=="raw"){j=j.replace(/

     <\/p>|]+)> <\/p>/g,"\u00a0

    ")}j=j.replace(/]+|)>([\s\S]*?)<\/noscript>/g,function(h,p,o){return""+i.dom.decode(o.replace(//g,""))+""})}n.content=j},_serializeNode:function(D,I){var z=this,A=z.settings,x=z.writer,q,j,u,F,E,H,B,h,y,k,r,C,p,m,G,o;if(!A.node_filter||A.node_filter(D)){switch(D.nodeType){case 1:if(D.hasAttribute?D.hasAttribute("_mce_bogus"):D.getAttribute("_mce_bogus")){return}p=G=false;q=D.hasChildNodes();k=D.getAttribute("_mce_name")||D.nodeName.toLowerCase();o=D.getAttribute("_mce_type");if(o){if(!z._info.cleanup){p=true;return}else{G=1}}if(d){if(D.scopeName!=="HTML"&&D.scopeName!=="html"){k=D.scopeName+":"+k}}if(k.indexOf("mce:")===0){k=k.substring(4)}if(!G){if(!z.validElementsRE||!z.validElementsRE.test(k)||(z.invalidElementsRE&&z.invalidElementsRE.test(k))||I){p=true;break}}if(d){if(A.fix_content_duplication){if(D._mce_serialized==z.key){return}D._mce_serialized=z.key}if(k.charAt(0)=="/"){k=k.substring(1)}}else{if(a){if(D.nodeName==="BR"&&D.getAttribute("type")=="_moz"){return}}}if(A.validate_children){if(z.elementName&&!z.schema.isValid(z.elementName,k)){p=true;break}z.elementName=k}r=z.findRule(k);if(!r){p=true;break}k=r.name||k;m=A.closed.test(k);if((!q&&r.noEmpty)||(d&&!k)){p=true;break}if(r.requiredAttribs){H=r.requiredAttribs;for(F=H.length-1;F>=0;F--){if(this.dom.getAttrib(D,H[F])!==""){break}}if(F==-1){p=true;break}}x.writeStartElement(k);if(r.attribs){for(F=0,B=r.attribs,E=B.length;F-1;F--){h=B[F];if(h.specified){H=h.nodeName.toLowerCase();if(A.invalid_attrs.test(H)||!r.validAttribsRE.test(H)){continue}C=z.findAttribRule(r,H);y=z._getAttrib(D,C,H);if(y!==null){x.writeAttribute(H,y)}}}}if(o&&G){x.writeAttribute("_mce_type",o)}if(k==="script"&&e.trim(D.innerHTML)){x.writeText("// ");x.writeCDATA(D.innerHTML.replace(/|<\[CDATA\[|\]\]>/g,""));q=false;break}if(r.padd){if(q&&(u=D.firstChild)&&u.nodeType===1&&D.childNodes.length===1){if(u.hasAttribute?u.hasAttribute("_mce_bogus"):u.getAttribute("_mce_bogus")){x.writeText("\u00a0")}}else{if(!q){x.writeText("\u00a0")}}}break;case 3:if(A.validate_children&&z.elementName&&!z.schema.isValid(z.elementName,"#text")){return}return x.writeText(D.nodeValue);case 4:return x.writeCDATA(D.nodeValue);case 8:return x.writeComment(D.nodeValue)}}else{if(D.nodeType==1){q=D.hasChildNodes()}}if(q&&!m){u=D.firstChild;while(u){z._serializeNode(u);z.elementName=k;u=u.nextSibling}}if(!p){if(!m){x.writeFullEndElement()}else{x.writeEndElement()}}},_protect:function(j){var i=this;j.items=j.items||[];function h(l){return l.replace(/[\r\n\\]/g,function(m){if(m==="\n"){return"\\n"}else{if(m==="\\"){return"\\\\"}}return"\\r"})}function k(l){return l.replace(/\\[\\rn]/g,function(m){if(m==="\\n"){return"\n"}else{if(m==="\\\\"){return"\\"}}return"\r"})}f(j.patterns,function(l){j.content=k(h(j.content).replace(l.pattern,function(n,o,m,p){m=k(m);if(l.encode){m=i._encode(m)}j.items.push(m);return o+""+p}))});return j},_unprotect:function(i,j){i=i.replace(/\"))}if(a&&j.ListBox){if(a.Button||a.SplitButton){e+=b.createHTML("td",{"class":"mceToolbarEnd"},b.createHTML("span",null,""))}}if(b.stdMode){e+='
    '+j.renderHTML()+""+j.renderHTML()+"
    '+j.renderHTML()+""+j.renderHTML()+"
    ..
    ab|c + if (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) > 0) { + checkRng = rng.duplicate(); + checkRng.collapse(start); + + offset = -1; + while (parent == checkRng.parentElement()) { + if (checkRng.move('character', -1) == 0) + break; + + offset++; } - if ( type === "first" ) { - return true; + } + + offset = offset || checkRng.text.replace('\r\n', ' ').length; + } else { + // Child position is after the selection endpoint + checkRng.collapse(true); + checkRng.setEndPoint(start ? 'StartToStart' : 'StartToEnd', rng); + + // Get the length of the text to find where the endpoint is relative to it's container + offset = checkRng.text.replace('\r\n', ' ').length; + } + + return {node : child, position : position, offset : offset, inside : inside}; + }; + + // Returns a W3C DOM compatible range object by using the IE Range API + function getRange() { + var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark, fail; + + // If selection is outside the current document just return an empty range + element = ieRange.item ? ieRange.item(0) : ieRange.parentElement(); + if (element.ownerDocument != dom.doc) + return domRange; + + collapsed = selection.isCollapsed(); + + // Handle control selection + if (ieRange.item) { + domRange.setStart(element.parentNode, dom.nodeIndex(element)); + domRange.setEnd(domRange.startContainer, domRange.startOffset + 1); + + return domRange; + } + + function findEndPoint(start) { + var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue; + + container = endPoint.node; + offset = endPoint.offset; + + if (endPoint.inside && !container.hasChildNodes()) { + domRange[start ? 'setStart' : 'setEnd'](container, 0); + return; + } + + if (offset === undef) { + domRange[start ? 'setStartBefore' : 'setEndAfter'](container); + return; + } + + if (endPoint.position < 0) { + sibling = endPoint.inside ? container.firstChild : container.nextSibling; + + if (!sibling) { + domRange[start ? 'setStartAfter' : 'setEndAfter'](container); + return; } - node = elem; - case 'last': - while ( (node = node.nextSibling) ) { - if ( node.nodeType === 1 ) { - return false; + + if (!offset) { + if (sibling.nodeType == 3) + domRange[start ? 'setStart' : 'setEnd'](sibling, 0); + else + domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling); + + return; + } + + // Find the text node and offset + while (sibling) { + nodeValue = sibling.nodeValue; + textNodeOffset += nodeValue.length; + + // We are at or passed the position we where looking for + if (textNodeOffset >= offset) { + container = sibling; + textNodeOffset -= offset; + textNodeOffset = nodeValue.length - textNodeOffset; + break; } + + sibling = sibling.nextSibling; } - return true; - case 'nth': - var first = match[2], last = match[3]; + } else { + // Find the text node and offset + sibling = container.previousSibling; - if ( first === 1 && last === 0 ) { - return true; + if (!sibling) + return domRange[start ? 'setStartBefore' : 'setEndBefore'](container); + + // If there isn't any text to loop then use the first position + if (!offset) { + if (container.nodeType == 3) + domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length); + else + domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling); + + return; } - - var doneName = match[0], - parent = elem.parentNode; - - if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) { - var count = 0; - for ( node = parent.firstChild; node; node = node.nextSibling ) { - if ( node.nodeType === 1 ) { - node.nodeIndex = ++count; - } - } - parent.sizcache = doneName; + + while (sibling) { + textNodeOffset += sibling.nodeValue.length; + + // We are at or passed the position we where looking for + if (textNodeOffset >= offset) { + container = sibling; + textNodeOffset -= offset; + break; + } + + sibling = sibling.previousSibling; } - - var diff = elem.nodeIndex - last; - if ( first === 0 ) { - return diff === 0; - } else { - return ( diff % first === 0 && diff / first >= 0 ); + } + + domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset); + }; + + try { + // Find start point + findEndPoint(true); + + // Find end point if needed + if (!collapsed) + findEndPoint(); + } catch (ex) { + // IE has a nasty bug where text nodes might throw "invalid argument" when you + // access the nodeValue or other properties of text nodes. This seems to happend when + // text nodes are split into two nodes by a delete/backspace call. So lets detect it and try to fix it. + if (ex.number == -2147024809) { + // Get the current selection + bookmark = self.getBookmark(2); + + // Get start element + tmpRange = ieRange.duplicate(); + tmpRange.collapse(true); + element = tmpRange.parentElement(); + + // Get end element + if (!collapsed) { + tmpRange = ieRange.duplicate(); + tmpRange.collapse(false); + element2 = tmpRange.parentElement(); + element2.innerHTML = element2.innerHTML; } - } - }, - ID: function(elem, match){ - return elem.nodeType === 1 && elem.getAttribute("id") === match; - }, - TAG: function(elem, match){ - return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match; - }, - CLASS: function(elem, match){ - return (" " + (elem.className || elem.getAttribute("class")) + " ") - .indexOf( match ) > -1; - }, - ATTR: function(elem, match){ - var name = match[1], - result = Expr.attrHandle[ name ] ? - Expr.attrHandle[ name ]( elem ) : - elem[ name ] != null ? - elem[ name ] : - elem.getAttribute( name ), - value = result + "", - type = match[2], - check = match[4]; - return result == null ? - type === "!=" : - type === "=" ? - value === check : - type === "*=" ? - value.indexOf(check) >= 0 : - type === "~=" ? - (" " + value + " ").indexOf(check) >= 0 : - !check ? - value && result !== false : - type === "!=" ? - value !== check : - type === "^=" ? - value.indexOf(check) === 0 : - type === "$=" ? - value.substr(value.length - check.length) === check : - type === "|=" ? - value === check || value.substr(0, check.length + 1) === check + "-" : - false; - }, - POS: function(elem, match, i, array){ - var name = match[2], filter = Expr.setFilters[ name ]; + // Remove the broken elements + element.innerHTML = element.innerHTML; - if ( filter ) { - return filter( elem, i, match, array ); + // Restore the selection + self.moveToBookmark(bookmark); + + // Since the range has moved we need to re-get it + ieRange = selection.getRng(); + + // Find start point + findEndPoint(true); + + // Find end point if needed + if (!collapsed) + findEndPoint(); + } else + throw ex; // Throw other errors } - } - } -}; -var origPOS = Expr.match.POS, - fescape = function(all, num){ - return "\\" + (num - 0 + 1); - }; + return domRange; + }; -for ( var type in Expr.match ) { - Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) ); - Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) ); -} + this.getBookmark = function(type) { + var rng = selection.getRng(), start, end, bookmark = {}; -var makeArray = function(array, results) { - array = Array.prototype.slice.call( array, 0 ); + function getIndexes(node) { + var node, parent, root, children, i, indexes = []; - if ( results ) { - results.push.apply( results, array ); - return results; - } - - return array; -}; + parent = node.parentNode; + root = dom.getRoot().parentNode; -// Perform a simple check to determine if the browser is capable of -// converting a NodeList to an array using builtin methods. -// Also verifies that the returned array holds DOM nodes -// (which is not the case in the Blackberry browser) -try { - Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; + while (parent != root && parent.nodeType !== 9) { + children = parent.children; -// Provide a fallback method if it does not work -} catch(e){ - makeArray = function(array, results) { - var ret = results || [], i = 0; + i = children.length; + while (i--) { + if (node === children[i]) { + indexes.push(i); + break; + } + } - if ( toString.call(array) === "[object Array]" ) { - Array.prototype.push.apply( ret, array ); - } else { - if ( typeof array.length === "number" ) { - for ( var l = array.length; i < l; i++ ) { - ret.push( array[i] ); - } - } else { - for ( ; array[i]; i++ ) { - ret.push( array[i] ); + node = parent; + parent = parent.parentNode; } - } - } - return ret; - }; -} + return indexes; + }; -var sortOrder; + function getBookmarkEndPoint(start) { + var position; -if ( document.documentElement.compareDocumentPosition ) { - sortOrder = function( a, b ) { - if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { - if ( a == b ) { - hasDuplicate = true; - } - return a.compareDocumentPosition ? -1 : 1; - } + position = getPosition(rng, start); + if (position) { + return { + position : position.position, + offset : position.offset, + indexes : getIndexes(position.node), + inside : position.inside + }; + } + }; - var ret = a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1; - if ( ret === 0 ) { - hasDuplicate = true; - } - return ret; - }; -} else if ( "sourceIndex" in document.documentElement ) { - sortOrder = function( a, b ) { - if ( !a.sourceIndex || !b.sourceIndex ) { - if ( a == b ) { - hasDuplicate = true; - } - return a.sourceIndex ? -1 : 1; - } + // Non ubstructive bookmark + if (type === 2) { + // Handle text selection + if (!rng.item) { + bookmark.start = getBookmarkEndPoint(true); - var ret = a.sourceIndex - b.sourceIndex; - if ( ret === 0 ) { - hasDuplicate = true; - } - return ret; - }; -} else if ( document.createRange ) { - sortOrder = function( a, b ) { - if ( !a.ownerDocument || !b.ownerDocument ) { - if ( a == b ) { - hasDuplicate = true; + if (!selection.isCollapsed()) + bookmark.end = getBookmarkEndPoint(); + } else + bookmark.start = {ctrl : true, indexes : getIndexes(rng.item(0))}; } - return a.ownerDocument ? -1 : 1; - } - var aRange = a.ownerDocument.createRange(), bRange = b.ownerDocument.createRange(); - aRange.setStart(a, 0); - aRange.setEnd(a, 0); - bRange.setStart(b, 0); - bRange.setEnd(b, 0); - var ret = aRange.compareBoundaryPoints(Range.START_TO_END, bRange); - if ( ret === 0 ) { - hasDuplicate = true; - } - return ret; - }; -} + return bookmark; + }; -// Utility function for retreiving the text value of an array of DOM nodes -Sizzle.getText = function( elems ) { - var ret = "", elem; + this.moveToBookmark = function(bookmark) { + var rng, body = dom.doc.body; - for ( var i = 0; elems[i]; i++ ) { - elem = elems[i]; + function resolveIndexes(indexes) { + var node, i, idx, children; - // Get the text from text nodes and CDATA nodes - if ( elem.nodeType === 3 || elem.nodeType === 4 ) { - ret += elem.nodeValue; + node = dom.getRoot(); + for (i = indexes.length - 1; i >= 0; i--) { + children = node.children; + idx = indexes[i]; - // Traverse everything else, except comment nodes - } else if ( elem.nodeType !== 8 ) { - ret += Sizzle.getText( elem.childNodes ); - } - } + if (idx <= children.length - 1) { + node = children[idx]; + } + } - return ret; -}; + return node; + }; + + function setBookmarkEndPoint(start) { + var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef; -// Check to see if the browser returns elements by name when -// querying by getElementById (and provide a workaround) -(function(){ - // We're going to inject a fake input element with a specified name - var form = document.createElement("div"), - id = "script" + (new Date()).getTime(); - form.innerHTML = ""; + if (endPoint) { + moveLeft = endPoint.position > 0; - // Inject it into the root element, check its status, and remove it quickly - var root = document.documentElement; - root.insertBefore( form, root.firstChild ); + moveRng = body.createTextRange(); + moveRng.moveToElementText(resolveIndexes(endPoint.indexes)); - // The workaround has to do additional checks after a getElementById - // Which slows things down for other browsers (hence the branching) - if ( document.getElementById( id ) ) { - Expr.find.ID = function(match, context, isXML){ - if ( typeof context.getElementById !== "undefined" && !isXML ) { - var m = context.getElementById(match[1]); - return m ? m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : []; - } - }; + offset = endPoint.offset; + if (offset !== undef) { + moveRng.collapse(endPoint.inside || moveLeft); + moveRng.moveStart('character', moveLeft ? -offset : offset); + } else + moveRng.collapse(start); - Expr.filter.ID = function(elem, match){ - var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); - return elem.nodeType === 1 && node && node.nodeValue === match; - }; - } + rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng); - root.removeChild( form ); - root = form = null; // release memory in IE -})(); + if (start) + rng.collapse(true); + } + }; -(function(){ - // Check to see if the browser returns only elements - // when doing getElementsByTagName("*") + if (bookmark.start) { + if (bookmark.start.ctrl) { + rng = body.createControlRange(); + rng.addElement(resolveIndexes(bookmark.start.indexes)); + rng.select(); + } else { + rng = body.createTextRange(); + setBookmarkEndPoint(true); + setBookmarkEndPoint(); + rng.select(); + } + } + }; + + this.addRange = function(rng) { + var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, doc = selection.dom.doc, body = doc.body; - // Create a fake element - var div = document.createElement("div"); - div.appendChild( document.createComment("") ); + function setEndPoint(start) { + var container, offset, marker, tmpRng, nodes; - // Make sure no comments are found - if ( div.getElementsByTagName("*").length > 0 ) { - Expr.find.TAG = function(match, context){ - var results = context.getElementsByTagName(match[1]); + marker = dom.create('a'); + container = start ? startContainer : endContainer; + offset = start ? startOffset : endOffset; + tmpRng = ieRng.duplicate(); - // Filter out possible comments - if ( match[1] === "*" ) { - var tmp = []; + if (container == doc || container == doc.documentElement) { + container = body; + offset = 0; + } - for ( var i = 0; results[i]; i++ ) { - if ( results[i].nodeType === 1 ) { - tmp.push( results[i] ); + if (container.nodeType == 3) { + container.parentNode.insertBefore(marker, container); + tmpRng.moveToElementText(marker); + tmpRng.moveStart('character', offset); + dom.remove(marker); + ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); + } else { + nodes = container.childNodes; + + if (nodes.length) { + if (offset >= nodes.length) { + dom.insertAfter(marker, nodes[nodes.length - 1]); + } else { + container.insertBefore(marker, nodes[offset]); + } + + tmpRng.moveToElementText(marker); + } else { + // Empty node selection for example
    |
    + marker = doc.createTextNode('\uFEFF'); + container.appendChild(marker); + tmpRng.moveToElementText(marker.parentNode); + tmpRng.collapse(TRUE); } + + ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); + dom.remove(marker); } + } - results = tmp; + // Setup some shorter versions + startContainer = rng.startContainer; + startOffset = rng.startOffset; + endContainer = rng.endContainer; + endOffset = rng.endOffset; + ieRng = body.createTextRange(); + + // If single element selection then try making a control selection out of it + if (startContainer == endContainer && startContainer.nodeType == 1 && startOffset == endOffset - 1) { + if (startOffset == endOffset - 1) { + try { + ctrlRng = body.createControlRange(); + ctrlRng.addElement(startContainer.childNodes[startOffset]); + ctrlRng.select(); + return; + } catch (ex) { + // Ignore + } + } } - return results; - }; - } + // Set start/end point of selection + setEndPoint(true); + setEndPoint(); - // Check to see if an attribute returns normalized href attributes - div.innerHTML = "
    "; - if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && - div.firstChild.getAttribute("href") !== "#" ) { - Expr.attrHandle.href = function(elem){ - return elem.getAttribute("href", 2); + // Select the new range and scroll it into view + ieRng.select(); }; - } - div = null; // release memory in IE -})(); + // Expose range method + this.getRangeAt = getRange; + }; -if ( document.querySelectorAll ) { - (function(){ - var oldSizzle = Sizzle, div = document.createElement("div"); - div.innerHTML = "

    "; + // Expose the selection object + tinymce.dom.TridentSelection = Selection; +})(); - // Safari can't handle uppercase or unicode characters when - // in quirks mode. - if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { - return; - } - - Sizzle = function(query, context, extra, seed){ - context = context || document; - // Only use querySelectorAll on non-XML documents - // (ID selectors don't work in non-HTML documents) - if ( !seed && context.nodeType === 9 && !Sizzle.isXML(context) ) { - try { - return makeArray( context.querySelectorAll(query), extra ); - } catch(e){} - } - - return oldSizzle(query, context, extra, seed); - }; +/* + * Sizzle CSS Selector Engine - v1.0 + * Copyright 2009, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * More information: http://sizzlejs.com/ + */ +(function(){ - for ( var prop in oldSizzle ) { - Sizzle[ prop ] = oldSizzle[ prop ]; - } +var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, + done = 0, + toString = Object.prototype.toString, + hasDuplicate = false, + baseHasDuplicate = true; - div = null; // release memory in IE - })(); -} +// Here we check if the JavaScript engine is using some sort of +// optimization where it does not always call our comparision +// function. If that is the case, discard the hasDuplicate value. +// Thus far that includes Google Chrome. +[0, 0].sort(function(){ + baseHasDuplicate = false; + return 0; +}); -(function(){ - var div = document.createElement("div"); +var Sizzle = function(selector, context, results, seed) { + results = results || []; + context = context || document; - div.innerHTML = "
    "; + var origContext = context; - // Opera can't find a second classname (in 9.6) - // Also, make sure that getElementsByClassName actually exists - if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { - return; + if ( context.nodeType !== 1 && context.nodeType !== 9 ) { + return []; } - - // Safari caches class attributes, doesn't catch changes (in 3.2) - div.lastChild.className = "e"; - - if ( div.getElementsByClassName("e").length === 1 ) { - return; + + if ( !selector || typeof selector !== "string" ) { + return results; } + + var parts = [], m, set, checkSet, extra, prune = true, contextXML = Sizzle.isXML(context), + soFar = selector, ret, cur, pop, i; - Expr.order.splice(1, 0, "CLASS"); - Expr.find.CLASS = function(match, context, isXML) { - if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { - return context.getElementsByClassName(match[1]); + // Reset the position of the chunker regexp (start from head) + do { + chunker.exec(""); + m = chunker.exec(soFar); + + if ( m ) { + soFar = m[3]; + + parts.push( m[1] ); + + if ( m[2] ) { + extra = m[3]; + break; + } } - }; + } while ( m ); - div = null; // release memory in IE -})(); + if ( parts.length > 1 && origPOS.exec( selector ) ) { + if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { + set = posProcess( parts[0] + parts[1], context ); + } else { + set = Expr.relative[ parts[0] ] ? + [ context ] : + Sizzle( parts.shift(), context ); -function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { - for ( var i = 0, l = checkSet.length; i < l; i++ ) { - var elem = checkSet[i]; - if ( elem ) { - elem = elem[dir]; - var match = false; + while ( parts.length ) { + selector = parts.shift(); - while ( elem ) { - if ( elem.sizcache === doneName ) { - match = checkSet[elem.sizset]; - break; + if ( Expr.relative[ selector ] ) { + selector += parts.shift(); } + + set = posProcess( selector, set ); + } + } + } else { + // Take a shortcut and set the context if the root selector is an ID + // (but not if it'll be faster if the inner selector is an ID) + if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && + Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { + ret = Sizzle.find( parts.shift(), context, contextXML ); + context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0]; + } - if ( elem.nodeType === 1 && !isXML ){ - elem.sizcache = doneName; - elem.sizset = i; + if ( context ) { + ret = seed ? + { expr: parts.pop(), set: makeArray(seed) } : + Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); + set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set; + + if ( parts.length > 0 ) { + checkSet = makeArray(set); + } else { + prune = false; + } + + while ( parts.length ) { + cur = parts.pop(); + pop = cur; + + if ( !Expr.relative[ cur ] ) { + cur = ""; + } else { + pop = parts.pop(); } - if ( elem.nodeName.toLowerCase() === cur ) { - match = elem; - break; + if ( pop == null ) { + pop = context; } - elem = elem[dir]; + Expr.relative[ cur ]( checkSet, pop, contextXML ); } - - checkSet[i] = match; + } else { + checkSet = parts = []; } } -} -function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { - for ( var i = 0, l = checkSet.length; i < l; i++ ) { - var elem = checkSet[i]; - if ( elem ) { - elem = elem[dir]; - var match = false; + if ( !checkSet ) { + checkSet = set; + } - while ( elem ) { - if ( elem.sizcache === doneName ) { - match = checkSet[elem.sizset]; - break; + if ( !checkSet ) { + Sizzle.error( cur || selector ); + } + + if ( toString.call(checkSet) === "[object Array]" ) { + if ( !prune ) { + results.push.apply( results, checkSet ); + } else if ( context && context.nodeType === 1 ) { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { + results.push( set[i] ); + } + } + } else { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && checkSet[i].nodeType === 1 ) { + results.push( set[i] ); } + } + } + } else { + makeArray( checkSet, results ); + } + + if ( extra ) { + Sizzle( extra, origContext, results, seed ); + Sizzle.uniqueSort( results ); + } + + return results; +}; - if ( elem.nodeType === 1 ) { - if ( !isXML ) { - elem.sizcache = doneName; - elem.sizset = i; - } - if ( typeof cur !== "string" ) { - if ( elem === cur ) { - match = true; - break; - } +Sizzle.uniqueSort = function(results){ + if ( sortOrder ) { + hasDuplicate = baseHasDuplicate; + results.sort(sortOrder); - } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { - match = elem; - break; - } + if ( hasDuplicate ) { + for ( var i = 1; i < results.length; i++ ) { + if ( results[i] === results[i-1] ) { + results.splice(i--, 1); } - - elem = elem[dir]; } - - checkSet[i] = match; } } -} -Sizzle.contains = document.compareDocumentPosition ? function(a, b){ - return !!(a.compareDocumentPosition(b) & 16); -} : function(a, b){ - return a !== b && (a.contains ? a.contains(b) : true); + return results; }; -Sizzle.isXML = function(elem){ - // documentElement is verified for cases where it doesn't yet exist - // (such as loading iframes in IE - #4833) - var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; - return documentElement ? documentElement.nodeName !== "HTML" : false; +Sizzle.matches = function(expr, set){ + return Sizzle(expr, null, null, set); }; -var posProcess = function(selector, context){ - var tmpSet = [], later = "", match, - root = context.nodeType ? [context] : context; +Sizzle.find = function(expr, context, isXML){ + var set; - // Position selectors must be done after the filter - // And so must :not(positional) so we move all PSEUDOs to the end - while ( (match = Expr.match.PSEUDO.exec( selector )) ) { - later += match[0]; - selector = selector.replace( Expr.match.PSEUDO, "" ); + if ( !expr ) { + return []; } - selector = Expr.relative[selector] ? selector + "*" : selector; + for ( var i = 0, l = Expr.order.length; i < l; i++ ) { + var type = Expr.order[i], match; + + if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { + var left = match[1]; + match.splice(1,1); - for ( var i = 0, l = root.length; i < l; i++ ) { - Sizzle( selector, root[i], tmpSet ); + if ( left.substr( left.length - 1 ) !== "\\" ) { + match[1] = (match[1] || "").replace(/\\/g, ""); + set = Expr.find[ type ]( match, context, isXML ); + if ( set != null ) { + expr = expr.replace( Expr.match[ type ], "" ); + break; + } + } + } } - return Sizzle.filter( later, tmpSet ); + if ( !set ) { + set = context.getElementsByTagName("*"); + } + + return {set: set, expr: expr}; }; -// EXPOSE +Sizzle.filter = function(expr, set, inplace, not){ + var old = expr, result = [], curLoop = set, match, anyFound, + isXMLFilter = set && set[0] && Sizzle.isXML(set[0]); -window.tinymce.dom.Sizzle = Sizzle; + while ( expr && set.length ) { + for ( var type in Expr.filter ) { + if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { + var filter = Expr.filter[ type ], found, item, left = match[1]; + anyFound = false; -})(); + match.splice(1,1); + if ( left.substr( left.length - 1 ) === "\\" ) { + continue; + } -(function(tinymce) { - // Shorten names - var each = tinymce.each, DOM = tinymce.DOM, isIE = tinymce.isIE, isWebKit = tinymce.isWebKit, Event; + if ( curLoop === result ) { + result = []; + } - tinymce.create('tinymce.dom.EventUtils', { - EventUtils : function() { - this.inits = []; - this.events = []; - }, + if ( Expr.preFilter[ type ] ) { + match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); - add : function(o, n, f, s) { - var cb, t = this, el = t.events, r; + if ( !match ) { + anyFound = found = true; + } else if ( match === true ) { + continue; + } + } - if (n instanceof Array) { - r = []; + if ( match ) { + for ( var i = 0; (item = curLoop[i]) != null; i++ ) { + if ( item ) { + found = filter( item, match, i, curLoop ); + var pass = not ^ !!found; - each(n, function(n) { - r.push(t.add(o, n, f, s)); - }); + if ( inplace && found != null ) { + if ( pass ) { + anyFound = true; + } else { + curLoop[i] = false; + } + } else if ( pass ) { + result.push( item ); + anyFound = true; + } + } + } + } - return r; - } + if ( found !== undefined ) { + if ( !inplace ) { + curLoop = result; + } - // Handle array - if (o && o.hasOwnProperty && o instanceof Array) { - r = []; + expr = expr.replace( Expr.match[ type ], "" ); - each(o, function(o) { - o = DOM.get(o); - r.push(t.add(o, n, f, s)); - }); + if ( !anyFound ) { + return []; + } - return r; + break; + } } + } - o = DOM.get(o); + // Improper expression + if ( expr === old ) { + if ( anyFound == null ) { + Sizzle.error( expr ); + } else { + break; + } + } - if (!o) - return; + old = expr; + } - // Setup event callback - cb = function(e) { - // Is all events disabled - if (t.disabled) - return; + return curLoop; +}; - e = e || window.event; +Sizzle.error = function( msg ) { + throw "Syntax error, unrecognized expression: " + msg; +}; - // Patch in target, preventDefault and stopPropagation in IE it's W3C valid - if (e && isIE) { - if (!e.target) - e.target = e.srcElement; +var Expr = Sizzle.selectors = { + order: [ "ID", "NAME", "TAG" ], + match: { + ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/, + ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/, + TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/, + CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+\-]*)\))?/, + POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, + PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ + }, + leftMatch: {}, + attrMap: { + "class": "className", + "for": "htmlFor" + }, + attrHandle: { + href: function(elem){ + return elem.getAttribute("href"); + } + }, + relative: { + "+": function(checkSet, part){ + var isPartStr = typeof part === "string", + isTag = isPartStr && !/\W/.test(part), + isPartStrNotTag = isPartStr && !isTag; - // Patch in preventDefault, stopPropagation methods for W3C compatibility - tinymce.extend(e, t._stoppers); + if ( isTag ) { + part = part.toLowerCase(); + } + + for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { + if ( (elem = checkSet[i]) ) { + while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} + + checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? + elem || false : + elem === part; } + } - if (!s) - return f(e); + if ( isPartStrNotTag ) { + Sizzle.filter( part, checkSet, true ); + } + }, + ">": function(checkSet, part){ + var isPartStr = typeof part === "string", + elem, i = 0, l = checkSet.length; - return f.call(s, e); - }; + if ( isPartStr && !/\W/.test(part) ) { + part = part.toLowerCase(); + + for ( ; i < l; i++ ) { + elem = checkSet[i]; + if ( elem ) { + var parent = elem.parentNode; + checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; + } + } + } else { + for ( ; i < l; i++ ) { + elem = checkSet[i]; + if ( elem ) { + checkSet[i] = isPartStr ? + elem.parentNode : + elem.parentNode === part; + } + } - if (n == 'unload') { - tinymce.unloads.unshift({func : cb}); - return cb; + if ( isPartStr ) { + Sizzle.filter( part, checkSet, true ); + } } + }, + "": function(checkSet, part, isXML){ + var doneName = done++, checkFn = dirCheck, nodeCheck; - if (n == 'init') { - if (t.domLoaded) - cb(); - else - t.inits.push(cb); - - return cb; + if ( typeof part === "string" && !/\W/.test(part) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; } - // Store away listener reference - el.push({ - obj : o, - name : n, - func : f, - cfunc : cb, - scope : s - }); - - t._add(o, n, cb); - - return f; + checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML); }, + "~": function(checkSet, part, isXML){ + var doneName = done++, checkFn = dirCheck, nodeCheck; - remove : function(o, n, f) { - var t = this, a = t.events, s = false, r; - - // Handle array - if (o && o.hasOwnProperty && o instanceof Array) { - r = []; - - each(o, function(o) { - o = DOM.get(o); - r.push(t.remove(o, n, f)); - }); - - return r; + if ( typeof part === "string" && !/\W/.test(part) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; } - o = DOM.get(o); + checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML); + } + }, + find: { + ID: function(match, context, isXML){ + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + return m ? [m] : []; + } + }, + NAME: function(match, context){ + if ( typeof context.getElementsByName !== "undefined" ) { + var ret = [], results = context.getElementsByName(match[1]); - each(a, function(e, i) { - if (e.obj == o && e.name == n && (!f || (e.func == f || e.cfunc == f))) { - a.splice(i, 1); - t._remove(o, n, e.cfunc); - s = true; - return false; + for ( var i = 0, l = results.length; i < l; i++ ) { + if ( results[i].getAttribute("name") === match[1] ) { + ret.push( results[i] ); + } } - }); - return s; + return ret.length === 0 ? null : ret; + } }, + TAG: function(match, context){ + return context.getElementsByTagName(match[1]); + } + }, + preFilter: { + CLASS: function(match, curLoop, inplace, result, not, isXML){ + match = " " + match[1].replace(/\\/g, "") + " "; - clear : function(o) { - var t = this, a = t.events, i, e; - - if (o) { - o = DOM.get(o); - - for (i = a.length - 1; i >= 0; i--) { - e = a[i]; + if ( isXML ) { + return match; + } - if (e.obj === o) { - t._remove(e.obj, e.name, e.cfunc); - e.obj = e.cfunc = null; - a.splice(i, 1); + for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { + if ( elem ) { + if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n]/g, " ").indexOf(match) >= 0) ) { + if ( !inplace ) { + result.push( elem ); + } + } else if ( inplace ) { + curLoop[i] = false; } } } + + return false; }, + ID: function(match){ + return match[1].replace(/\\/g, ""); + }, + TAG: function(match, curLoop){ + return match[1].toLowerCase(); + }, + CHILD: function(match){ + if ( match[1] === "nth" ) { + // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' + var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec( + match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || + !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); - cancel : function(e) { - if (!e) - return false; + // calculate the numbers (first)n+(last) including if they are negative + match[2] = (test[1] + (test[2] || 1)) - 0; + match[3] = test[3] - 0; + } - this.stop(e); + // TODO: Move to normal caching system + match[0] = done++; - return this.prevent(e); + return match; }, + ATTR: function(match, curLoop, inplace, result, not, isXML){ + var name = match[1].replace(/\\/g, ""); + + if ( !isXML && Expr.attrMap[name] ) { + match[1] = Expr.attrMap[name]; + } - stop : function(e) { - if (e.stopPropagation) - e.stopPropagation(); - else - e.cancelBubble = true; + if ( match[2] === "~=" ) { + match[4] = " " + match[4] + " "; + } - return false; + return match; }, - - prevent : function(e) { - if (e.preventDefault) - e.preventDefault(); - else - e.returnValue = false; - - return false; + PSEUDO: function(match, curLoop, inplace, result, not){ + if ( match[1] === "not" ) { + // If we're dealing with a complex expression, or a simple one + if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { + match[3] = Sizzle(match[3], null, null, curLoop); + } else { + var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); + if ( !inplace ) { + result.push.apply( result, ret ); + } + return false; + } + } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { + return true; + } + + return match; }, - - destroy : function() { - var t = this; - - each(t.events, function(e, i) { - t._remove(e.obj, e.name, e.cfunc); - e.obj = e.cfunc = null; - }); - - t.events = []; - t = null; + POS: function(match){ + match.unshift( true ); + return match; + } + }, + filters: { + enabled: function(elem){ + return elem.disabled === false && elem.type !== "hidden"; + }, + disabled: function(elem){ + return elem.disabled === true; + }, + checked: function(elem){ + return elem.checked === true; + }, + selected: function(elem){ + // Accessing this property makes selected-by-default + // options in Safari work properly + elem.parentNode.selectedIndex; + return elem.selected === true; + }, + parent: function(elem){ + return !!elem.firstChild; + }, + empty: function(elem){ + return !elem.firstChild; + }, + has: function(elem, i, match){ + return !!Sizzle( match[3], elem ).length; + }, + header: function(elem){ + return (/h\d/i).test( elem.nodeName ); + }, + text: function(elem){ + return "text" === elem.type; + }, + radio: function(elem){ + return "radio" === elem.type; + }, + checkbox: function(elem){ + return "checkbox" === elem.type; + }, + file: function(elem){ + return "file" === elem.type; + }, + password: function(elem){ + return "password" === elem.type; + }, + submit: function(elem){ + return "submit" === elem.type; + }, + image: function(elem){ + return "image" === elem.type; + }, + reset: function(elem){ + return "reset" === elem.type; + }, + button: function(elem){ + return "button" === elem.type || elem.nodeName.toLowerCase() === "button"; + }, + input: function(elem){ + return (/input|select|textarea|button/i).test(elem.nodeName); + } + }, + setFilters: { + first: function(elem, i){ + return i === 0; + }, + last: function(elem, i, match, array){ + return i === array.length - 1; }, - - _add : function(o, n, f) { - if (o.attachEvent) - o.attachEvent('on' + n, f); - else if (o.addEventListener) - o.addEventListener(n, f, false); - else - o['on' + n] = f; + even: function(elem, i){ + return i % 2 === 0; }, - - _remove : function(o, n, f) { - if (o) { - try { - if (o.detachEvent) - o.detachEvent('on' + n, f); - else if (o.removeEventListener) - o.removeEventListener(n, f, false); - else - o['on' + n] = null; - } catch (ex) { - // Might fail with permission denined on IE so we just ignore that - } - } + odd: function(elem, i){ + return i % 2 === 1; }, - - _pageInit : function(win) { - var t = this; - - // Keep it from running more than once - if (t.domLoaded) - return; - - t.domLoaded = true; - - each(t.inits, function(c) { - c(); - }); - - t.inits = []; + lt: function(elem, i, match){ + return i < match[3] - 0; }, + gt: function(elem, i, match){ + return i > match[3] - 0; + }, + nth: function(elem, i, match){ + return match[3] - 0 === i; + }, + eq: function(elem, i, match){ + return match[3] - 0 === i; + } + }, + filter: { + PSEUDO: function(elem, match, i, array){ + var name = match[1], filter = Expr.filters[ name ]; - _wait : function(win) { - var t = this, doc = win.document; - - // No need since the document is already loaded - if (win.tinyMCE_GZ && tinyMCE_GZ.loaded) { - t.domLoaded = 1; - return; - } + if ( filter ) { + return filter( elem, i, match, array ); + } else if ( name === "contains" ) { + return (elem.textContent || elem.innerText || Sizzle.getText([ elem ]) || "").indexOf(match[3]) >= 0; + } else if ( name === "not" ) { + var not = match[3]; - // Use IE method - if (doc.attachEvent) { - doc.attachEvent("onreadystatechange", function() { - if (doc.readyState === "complete") { - doc.detachEvent("onreadystatechange", arguments.callee); - t._pageInit(win); + for ( var j = 0, l = not.length; j < l; j++ ) { + if ( not[j] === elem ) { + return false; } - }); - - if (doc.documentElement.doScroll && win == win.top) { - (function() { - if (t.domLoaded) - return; + } - try { - // If IE is used, use the trick by Diego Perini - // http://javascript.nwbox.com/IEContentLoaded/ - doc.documentElement.doScroll("left"); - } catch (ex) { - setTimeout(arguments.callee, 0); - return; + return true; + } else { + Sizzle.error( "Syntax error, unrecognized expression: " + name ); + } + }, + CHILD: function(elem, match){ + var type = match[1], node = elem; + switch (type) { + case 'only': + case 'first': + while ( (node = node.previousSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + if ( type === "first" ) { + return true; + } + node = elem; + case 'last': + while ( (node = node.nextSibling) ) { + if ( node.nodeType === 1 ) { + return false; } + } + return true; + case 'nth': + var first = match[2], last = match[3]; - t._pageInit(win); - })(); - } - } else if (doc.addEventListener) { - t._add(win, 'DOMContentLoaded', function() { - t._pageInit(win); - }); + if ( first === 1 && last === 0 ) { + return true; + } + + var doneName = match[0], + parent = elem.parentNode; + + if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) { + var count = 0; + for ( node = parent.firstChild; node; node = node.nextSibling ) { + if ( node.nodeType === 1 ) { + node.nodeIndex = ++count; + } + } + parent.sizcache = doneName; + } + + var diff = elem.nodeIndex - last; + if ( first === 0 ) { + return diff === 0; + } else { + return ( diff % first === 0 && diff / first >= 0 ); + } } - - t._add(win, 'load', function() { - t._pageInit(win); - }); }, + ID: function(elem, match){ + return elem.nodeType === 1 && elem.getAttribute("id") === match; + }, + TAG: function(elem, match){ + return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match; + }, + CLASS: function(elem, match){ + return (" " + (elem.className || elem.getAttribute("class")) + " ") + .indexOf( match ) > -1; + }, + ATTR: function(elem, match){ + var name = match[1], + result = Expr.attrHandle[ name ] ? + Expr.attrHandle[ name ]( elem ) : + elem[ name ] != null ? + elem[ name ] : + elem.getAttribute( name ), + value = result + "", + type = match[2], + check = match[4]; - _stoppers : { - preventDefault : function() { - this.returnValue = false; - }, + return result == null ? + type === "!=" : + type === "=" ? + value === check : + type === "*=" ? + value.indexOf(check) >= 0 : + type === "~=" ? + (" " + value + " ").indexOf(check) >= 0 : + !check ? + value && result !== false : + type === "!=" ? + value !== check : + type === "^=" ? + value.indexOf(check) === 0 : + type === "$=" ? + value.substr(value.length - check.length) === check : + type === "|=" ? + value === check || value.substr(0, check.length + 1) === check + "-" : + false; + }, + POS: function(elem, match, i, array){ + var name = match[2], filter = Expr.setFilters[ name ]; - stopPropagation : function() { - this.cancelBubble = true; + if ( filter ) { + return filter( elem, i, match, array ); } } - }); - - Event = tinymce.dom.Event = new tinymce.dom.EventUtils(); - - // Dispatch DOM content loaded event for IE and Safari - Event._wait(window); - - tinymce.addUnload(function() { - Event.destroy(); - }); -})(tinymce); + } +}; -(function(tinymce) { - tinymce.dom.Element = function(id, settings) { - var t = this, dom, el; +var origPOS = Expr.match.POS, + fescape = function(all, num){ + return "\\" + (num - 0 + 1); + }; - t.settings = settings = settings || {}; - t.id = id; - t.dom = dom = settings.dom || tinymce.DOM; +for ( var type in Expr.match ) { + Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) ); + Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) ); +} - // Only IE leaks DOM references, this is a lot faster - if (!tinymce.isIE) - el = dom.get(t.id); +var makeArray = function(array, results) { + array = Array.prototype.slice.call( array, 0 ); - tinymce.each( - ('getPos,getRect,getParent,add,setStyle,getStyle,setStyles,' + - 'setAttrib,setAttribs,getAttrib,addClass,removeClass,' + - 'hasClass,getOuterHTML,setOuterHTML,remove,show,hide,' + - 'isHidden,setHTML,get').split(/,/) - , function(k) { - t[k] = function() { - var a = [id], i; + if ( results ) { + results.push.apply( results, array ); + return results; + } + + return array; +}; - for (i = 0; i < arguments.length; i++) - a.push(arguments[i]); +// Perform a simple check to determine if the browser is capable of +// converting a NodeList to an array using builtin methods. +// Also verifies that the returned array holds DOM nodes +// (which is not the case in the Blackberry browser) +try { + Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; - a = dom[k].apply(dom, a); - t.update(k); +// Provide a fallback method if it does not work +} catch(e){ + makeArray = function(array, results) { + var ret = results || [], i = 0; - return a; - }; - }); + if ( toString.call(array) === "[object Array]" ) { + Array.prototype.push.apply( ret, array ); + } else { + if ( typeof array.length === "number" ) { + for ( var l = array.length; i < l; i++ ) { + ret.push( array[i] ); + } + } else { + for ( ; array[i]; i++ ) { + ret.push( array[i] ); + } + } + } - tinymce.extend(t, { - on : function(n, f, s) { - return tinymce.dom.Event.add(t.id, n, f, s); - }, + return ret; + }; +} - getXY : function() { - return { - x : parseInt(t.getStyle('left')), - y : parseInt(t.getStyle('top')) - }; - }, +var sortOrder; - getSize : function() { - var n = dom.get(t.id); +if ( document.documentElement.compareDocumentPosition ) { + sortOrder = function( a, b ) { + if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { + if ( a == b ) { + hasDuplicate = true; + } + return a.compareDocumentPosition ? -1 : 1; + } - return { - w : parseInt(t.getStyle('width') || n.clientWidth), - h : parseInt(t.getStyle('height') || n.clientHeight) - }; - }, + var ret = a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1; + if ( ret === 0 ) { + hasDuplicate = true; + } + return ret; + }; +} else if ( "sourceIndex" in document.documentElement ) { + sortOrder = function( a, b ) { + if ( !a.sourceIndex || !b.sourceIndex ) { + if ( a == b ) { + hasDuplicate = true; + } + return a.sourceIndex ? -1 : 1; + } - moveTo : function(x, y) { - t.setStyles({left : x, top : y}); - }, + var ret = a.sourceIndex - b.sourceIndex; + if ( ret === 0 ) { + hasDuplicate = true; + } + return ret; + }; +} else if ( document.createRange ) { + sortOrder = function( a, b ) { + if ( !a.ownerDocument || !b.ownerDocument ) { + if ( a == b ) { + hasDuplicate = true; + } + return a.ownerDocument ? -1 : 1; + } - moveBy : function(x, y) { - var p = t.getXY(); + var aRange = a.ownerDocument.createRange(), bRange = b.ownerDocument.createRange(); + aRange.setStart(a, 0); + aRange.setEnd(a, 0); + bRange.setStart(b, 0); + bRange.setEnd(b, 0); + var ret = aRange.compareBoundaryPoints(Range.START_TO_END, bRange); + if ( ret === 0 ) { + hasDuplicate = true; + } + return ret; + }; +} - t.moveTo(p.x + x, p.y + y); - }, +// Utility function for retreiving the text value of an array of DOM nodes +Sizzle.getText = function( elems ) { + var ret = "", elem; - resizeTo : function(w, h) { - t.setStyles({width : w, height : h}); - }, + for ( var i = 0; elems[i]; i++ ) { + elem = elems[i]; - resizeBy : function(w, h) { - var s = t.getSize(); + // Get the text from text nodes and CDATA nodes + if ( elem.nodeType === 3 || elem.nodeType === 4 ) { + ret += elem.nodeValue; - t.resizeTo(s.w + w, s.h + h); - }, + // Traverse everything else, except comment nodes + } else if ( elem.nodeType !== 8 ) { + ret += Sizzle.getText( elem.childNodes ); + } + } - update : function(k) { - var b; + return ret; +}; - if (tinymce.isIE6 && settings.blocker) { - k = k || ''; +// Check to see if the browser returns elements by name when +// querying by getElementById (and provide a workaround) +(function(){ + // We're going to inject a fake input element with a specified name + var form = document.createElement("div"), + id = "script" + (new Date()).getTime(); + form.innerHTML = ""; - // Ignore getters - if (k.indexOf('get') === 0 || k.indexOf('has') === 0 || k.indexOf('is') === 0) - return; + // Inject it into the root element, check its status, and remove it quickly + var root = document.documentElement; + root.insertBefore( form, root.firstChild ); - // Remove blocker on remove - if (k == 'remove') { - dom.remove(t.blocker); - return; - } + // The workaround has to do additional checks after a getElementById + // Which slows things down for other browsers (hence the branching) + if ( document.getElementById( id ) ) { + Expr.find.ID = function(match, context, isXML){ + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + return m ? m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : []; + } + }; - if (!t.blocker) { - t.blocker = dom.uniqueId(); - b = dom.add(settings.container || dom.getRoot(), 'iframe', {id : t.blocker, style : 'position:absolute;', frameBorder : 0, src : 'javascript:""'}); - dom.setStyle(b, 'opacity', 0); - } else - b = dom.get(t.blocker); + Expr.filter.ID = function(elem, match){ + var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); + return elem.nodeType === 1 && node && node.nodeValue === match; + }; + } - dom.setStyles(b, { - left : t.getStyle('left', 1), - top : t.getStyle('top', 1), - width : t.getStyle('width', 1), - height : t.getStyle('height', 1), - display : t.getStyle('display', 1), - zIndex : parseInt(t.getStyle('zIndex', 1) || 0) - 1 - }); - } - } - }); - }; -})(tinymce); + root.removeChild( form ); + root = form = null; // release memory in IE +})(); -(function(tinymce) { - function trimNl(s) { - return s.replace(/[\n\r]+/g, ''); - }; +(function(){ + // Check to see if the browser returns only elements + // when doing getElementsByTagName("*") - // Shorten names - var is = tinymce.is, isIE = tinymce.isIE, each = tinymce.each; + // Create a fake element + var div = document.createElement("div"); + div.appendChild( document.createComment("") ); - tinymce.create('tinymce.dom.Selection', { - Selection : function(dom, win, serializer) { - var t = this; + // Make sure no comments are found + if ( div.getElementsByTagName("*").length > 0 ) { + Expr.find.TAG = function(match, context){ + var results = context.getElementsByTagName(match[1]); - t.dom = dom; - t.win = win; - t.serializer = serializer; + // Filter out possible comments + if ( match[1] === "*" ) { + var tmp = []; - // Add events - each([ - 'onBeforeSetContent', - 'onBeforeGetContent', - 'onSetContent', - 'onGetContent' - ], function(e) { - t[e] = new tinymce.util.Dispatcher(t); - }); + for ( var i = 0; results[i]; i++ ) { + if ( results[i].nodeType === 1 ) { + tmp.push( results[i] ); + } + } - // No W3C Range support - if (!t.win.getSelection) - t.tridentSel = new tinymce.dom.TridentSelection(t); + results = tmp; + } - // Prevent leaks - tinymce.addUnload(t.destroy, t); - }, + return results; + }; + } - getContent : function(s) { - var t = this, r = t.getRng(), e = t.dom.create("body"), se = t.getSel(), wb, wa, n; + // Check to see if an attribute returns normalized href attributes + div.innerHTML = ""; + if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && + div.firstChild.getAttribute("href") !== "#" ) { + Expr.attrHandle.href = function(elem){ + return elem.getAttribute("href", 2); + }; + } - s = s || {}; - wb = wa = ''; - s.get = true; - s.format = s.format || 'html'; - t.onBeforeGetContent.dispatch(t, s); + div = null; // release memory in IE +})(); - if (s.format == 'text') - return t.isCollapsed() ? '' : (r.text || (se.toString ? se.toString() : '')); +if ( document.querySelectorAll ) { + (function(){ + var oldSizzle = Sizzle, div = document.createElement("div"); + div.innerHTML = "

    "; - if (r.cloneContents) { - n = r.cloneContents(); + // Safari can't handle uppercase or unicode characters when + // in quirks mode. + if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { + return; + } + + Sizzle = function(query, context, extra, seed){ + context = context || document; - if (n) - e.appendChild(n); - } else if (is(r.item) || is(r.htmlText)) - e.innerHTML = r.item ? r.item(0).outerHTML : r.htmlText; - else - e.innerHTML = r.toString(); + // Only use querySelectorAll on non-XML documents + // (ID selectors don't work in non-HTML documents) + if ( !seed && context.nodeType === 9 && !Sizzle.isXML(context) ) { + try { + return makeArray( context.querySelectorAll(query), extra ); + } catch(e){} + } + + return oldSizzle(query, context, extra, seed); + }; - // Keep whitespace before and after - if (/^\s/.test(e.innerHTML)) - wb = ' '; + for ( var prop in oldSizzle ) { + Sizzle[ prop ] = oldSizzle[ prop ]; + } - if (/\s+$/.test(e.innerHTML)) - wa = ' '; + div = null; // release memory in IE + })(); +} - s.getInner = true; +(function(){ + var div = document.createElement("div"); - s.content = t.isCollapsed() ? '' : wb + t.serializer.serialize(e, s) + wa; - t.onGetContent.dispatch(t, s); + div.innerHTML = "
    "; - return s.content; - }, + // Opera can't find a second classname (in 9.6) + // Also, make sure that getElementsByClassName actually exists + if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { + return; + } - setContent : function(h, s) { - var t = this, r = t.getRng(), c, d = t.win.document; + // Safari caches class attributes, doesn't catch changes (in 3.2) + div.lastChild.className = "e"; - s = s || {format : 'html'}; - s.set = true; - h = s.content = t.dom.processHTML(h); + if ( div.getElementsByClassName("e").length === 1 ) { + return; + } + + Expr.order.splice(1, 0, "CLASS"); + Expr.find.CLASS = function(match, context, isXML) { + if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { + return context.getElementsByClassName(match[1]); + } + }; - // Dispatch before set content event - t.onBeforeSetContent.dispatch(t, s); - h = s.content; + div = null; // release memory in IE +})(); - if (r.insertNode) { - // Make caret marker since insertNode places the caret in the beginning of text after insert - h += '_'; +function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + if ( elem ) { + elem = elem[dir]; + var match = false; - // Delete and insert new node - - if (r.startContainer == d && r.endContainer == d) { - // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents - d.body.innerHTML = h; - } else { - r.deleteContents(); - if (d.body.childNodes.length == 0) { - d.body.innerHTML = h; - } else { - r.insertNode(r.createContextualFragment(h)); - } + while ( elem ) { + if ( elem.sizcache === doneName ) { + match = checkSet[elem.sizset]; + break; } - // Move to caret marker - c = t.dom.get('__caret'); - // Make sure we wrap it compleatly, Opera fails with a simple select call - r = d.createRange(); - r.setStartBefore(c); - r.setEndBefore(c); - t.setRng(r); + if ( elem.nodeType === 1 && !isXML ){ + elem.sizcache = doneName; + elem.sizset = i; + } - // Remove the caret position - t.dom.remove('__caret'); - } else { - if (r.item) { - // Delete content and get caret text selection - d.execCommand('Delete', false, null); - r = t.getRng(); + if ( elem.nodeName.toLowerCase() === cur ) { + match = elem; + break; } - r.pasteHTML(h); + elem = elem[dir]; } - // Dispatch set content event - t.onSetContent.dispatch(t, s); - }, + checkSet[i] = match; + } + } +} - getStart : function() { - var rng = this.getRng(), startElement, parentElement, checkRng, node; +function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + if ( elem ) { + elem = elem[dir]; + var match = false; - if (rng.duplicate || rng.item) { - // Control selection, return first item - if (rng.item) - return rng.item(0); + while ( elem ) { + if ( elem.sizcache === doneName ) { + match = checkSet[elem.sizset]; + break; + } - // Get start element - checkRng = rng.duplicate(); - checkRng.collapse(1); - startElement = checkRng.parentElement(); + if ( elem.nodeType === 1 ) { + if ( !isXML ) { + elem.sizcache = doneName; + elem.sizset = i; + } + if ( typeof cur !== "string" ) { + if ( elem === cur ) { + match = true; + break; + } - // Check if range parent is inside the start element, then return the inner parent element - // This will fix issues when a single element is selected, IE would otherwise return the wrong start element - parentElement = node = rng.parentElement(); - while (node = node.parentNode) { - if (node == startElement) { - startElement = parentElement; + } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { + match = elem; break; } } - // If start element is body element try to move to the first child if it exists - if (startElement && startElement.nodeName == 'BODY') - return startElement.firstChild || startElement; - - return startElement; - } else { - startElement = rng.startContainer; - - if (startElement.nodeType == 1 && startElement.hasChildNodes()) - startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)]; - - if (startElement && startElement.nodeType == 3) - return startElement.parentNode; - - return startElement; + elem = elem[dir]; } - }, - - getEnd : function() { - var t = this, r = t.getRng(), e, eo; - - if (r.duplicate || r.item) { - if (r.item) - return r.item(0); - - r = r.duplicate(); - r.collapse(0); - e = r.parentElement(); - if (e && e.nodeName == 'BODY') - return e.lastChild || e; + checkSet[i] = match; + } + } +} - return e; - } else { - e = r.endContainer; - eo = r.endOffset; +Sizzle.contains = document.compareDocumentPosition ? function(a, b){ + return !!(a.compareDocumentPosition(b) & 16); +} : function(a, b){ + return a !== b && (a.contains ? a.contains(b) : true); +}; - if (e.nodeType == 1 && e.hasChildNodes()) - e = e.childNodes[eo > 0 ? eo - 1 : eo]; +Sizzle.isXML = function(elem){ + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; - if (e && e.nodeType == 3) - return e.parentNode; +var posProcess = function(selector, context){ + var tmpSet = [], later = "", match, + root = context.nodeType ? [context] : context; - return e; - } - }, + // Position selectors must be done after the filter + // And so must :not(positional) so we move all PSEUDOs to the end + while ( (match = Expr.match.PSEUDO.exec( selector )) ) { + later += match[0]; + selector = selector.replace( Expr.match.PSEUDO, "" ); + } - getBookmark : function(type, normalized) { - var t = this, dom = t.dom, rng, rng2, id, collapsed, name, element, index, chr = '\uFEFF', styles; + selector = Expr.relative[selector] ? selector + "*" : selector; - function findIndex(name, element) { - var index = 0; + for ( var i = 0, l = root.length; i < l; i++ ) { + Sizzle( selector, root[i], tmpSet ); + } - each(dom.select(name), function(node, i) { - if (node == element) - index = i; - }); + return Sizzle.filter( later, tmpSet ); +}; - return index; - }; +// EXPOSE - if (type == 2) { - function getLocation() { - var rng = t.getRng(true), root = dom.getRoot(), bookmark = {}; +window.tinymce.dom.Sizzle = Sizzle; - function getPoint(rng, start) { - var container = rng[start ? 'startContainer' : 'endContainer'], - offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0; +})(); - if (container.nodeType == 3) { - if (normalized) { - for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling) - offset += node.nodeValue.length; - } - point.push(offset); - } else { - childNodes = container.childNodes; +(function(tinymce) { + // Shorten names + var each = tinymce.each, DOM = tinymce.DOM, isIE = tinymce.isIE, isWebKit = tinymce.isWebKit, Event; - if (offset >= childNodes.length && childNodes.length) { - after = 1; - offset = Math.max(0, childNodes.length - 1); - } + tinymce.create('tinymce.dom.EventUtils', { + EventUtils : function() { + this.inits = []; + this.events = []; + }, - point.push(t.dom.nodeIndex(childNodes[offset], normalized) + after); - } + add : function(o, n, f, s) { + var cb, t = this, el = t.events, r; - for (; container && container != root; container = container.parentNode) - point.push(t.dom.nodeIndex(container, normalized)); + if (n instanceof Array) { + r = []; - return point; - }; + each(n, function(n) { + r.push(t.add(o, n, f, s)); + }); - bookmark.start = getPoint(rng, true); + return r; + } - if (!t.isCollapsed()) - bookmark.end = getPoint(rng); + // Handle array + if (o && o.hasOwnProperty && o instanceof Array) { + r = []; - return bookmark; - }; + each(o, function(o) { + o = DOM.get(o); + r.push(t.add(o, n, f, s)); + }); - return getLocation(); + return r; } - // Handle simple range - if (type) - return {rng : t.getRng()}; + o = DOM.get(o); - rng = t.getRng(); - id = dom.uniqueId(); - collapsed = tinyMCE.activeEditor.selection.isCollapsed(); - styles = 'overflow:hidden;line-height:0px'; + if (!o) + return; - // Explorer method - if (rng.duplicate || rng.item) { - // Text selection - if (!rng.item) { - rng2 = rng.duplicate(); + // Setup event callback + cb = function(e) { + // Is all events disabled + if (t.disabled) + return; - // Insert start marker - rng.collapse(); - rng.pasteHTML('' + chr + ''); + e = e || window.event; - // Insert end marker - if (!collapsed) { - rng2.collapse(false); - rng2.pasteHTML('' + chr + ''); - } - } else { - // Control selection - element = rng.item(0); - name = element.nodeName; + // Patch in target, preventDefault and stopPropagation in IE it's W3C valid + if (e && isIE) { + if (!e.target) + e.target = e.srcElement; - return {name : name, index : findIndex(name, element)}; + // Patch in preventDefault, stopPropagation methods for W3C compatibility + tinymce.extend(e, t._stoppers); } - } else { - element = t.getNode(); - name = element.nodeName; - if (name == 'IMG') - return {name : name, index : findIndex(name, element)}; - // W3C method - rng2 = rng.cloneRange(); + if (!s) + return f(e); - // Insert end marker - if (!collapsed) { - rng2.collapse(false); - rng2.insertNode(dom.create('span', {_mce_type : "bookmark", id : id + '_end', style : styles}, chr)); - } + return f.call(s, e); + }; - rng.collapse(true); - rng.insertNode(dom.create('span', {_mce_type : "bookmark", id : id + '_start', style : styles}, chr)); + if (n == 'unload') { + tinymce.unloads.unshift({func : cb}); + return cb; } - t.moveToBookmark({id : id, keep : 1}); + if (n == 'init') { + if (t.domLoaded) + cb(); + else + t.inits.push(cb); - return {id : id}; + return cb; + } + + // Store away listener reference + el.push({ + obj : o, + name : n, + func : f, + cfunc : cb, + scope : s + }); + + t._add(o, n, cb); + + return f; }, - moveToBookmark : function(bookmark) { - var t = this, dom = t.dom, marker1, marker2, rng, root, startContainer, endContainer, startOffset, endOffset; + remove : function(o, n, f) { + var t = this, a = t.events, s = false, r; - // Clear selection cache - if (t.tridentSel) - t.tridentSel.destroy(); + // Handle array + if (o && o.hasOwnProperty && o instanceof Array) { + r = []; - if (bookmark) { - if (bookmark.start) { - rng = dom.createRng(); - root = dom.getRoot(); + each(o, function(o) { + o = DOM.get(o); + r.push(t.remove(o, n, f)); + }); - function setEndPoint(start) { - var point = bookmark[start ? 'start' : 'end'], i, node, offset, children; + return r; + } - if (point) { - // Find container node - for (node = root, i = point.length - 1; i >= 1; i--) { - children = node.childNodes; + o = DOM.get(o); - if (children.length) - node = children[point[i]]; - } + each(a, function(e, i) { + if (e.obj == o && e.name == n && (!f || (e.func == f || e.cfunc == f))) { + a.splice(i, 1); + t._remove(o, n, e.cfunc); + s = true; + return false; + } + }); - // Set offset within container node - if (start) - rng.setStart(node, point[0]); - else - rng.setEnd(node, point[0]); - } - }; + return s; + }, - setEndPoint(true); - setEndPoint(); + clear : function(o) { + var t = this, a = t.events, i, e; - t.setRng(rng); - } else if (bookmark.id) { - function restoreEndPoint(suffix) { - var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep; + if (o) { + o = DOM.get(o); - if (marker) { - node = marker.parentNode; + for (i = a.length - 1; i >= 0; i--) { + e = a[i]; - if (suffix == 'start') { - if (!keep) { - idx = dom.nodeIndex(marker); - } else { - node = marker.firstChild; - idx = 1; - } + if (e.obj === o) { + t._remove(e.obj, e.name, e.cfunc); + e.obj = e.cfunc = null; + a.splice(i, 1); + } + } + } + }, - startContainer = endContainer = node; - startOffset = endOffset = idx; - } else { - if (!keep) { - idx = dom.nodeIndex(marker); - } else { - node = marker.firstChild; - idx = 1; - } + cancel : function(e) { + if (!e) + return false; - endContainer = node; - endOffset = idx; - } + this.stop(e); - if (!keep) { - prev = marker.previousSibling; - next = marker.nextSibling; + return this.prevent(e); + }, - // Remove all marker text nodes - each(tinymce.grep(marker.childNodes), function(node) { - if (node.nodeType == 3) - node.nodeValue = node.nodeValue.replace(/\uFEFF/g, ''); - }); + stop : function(e) { + if (e.stopPropagation) + e.stopPropagation(); + else + e.cancelBubble = true; - // Remove marker but keep children if for example contents where inserted into the marker - // Also remove duplicated instances of the marker for example by a split operation or by WebKit auto split on paste feature - while (marker = dom.get(bookmark.id + '_' + suffix)) - dom.remove(marker, 1); + return false; + }, - // If siblings are text nodes then merge them - if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3) { - idx = prev.nodeValue.length; - prev.appendData(next.nodeValue); - dom.remove(next); + prevent : function(e) { + if (e.preventDefault) + e.preventDefault(); + else + e.returnValue = false; - if (suffix == 'start') { - startContainer = endContainer = prev; - startOffset = endOffset = idx; - } else { - endContainer = prev; - endOffset = idx; - } - } - } - } - }; + return false; + }, - function addBogus(node) { - // Adds a bogus BR element for empty block elements - // on non IE browsers just to have a place to put the caret - if (!isIE && dom.isBlock(node) && !node.innerHTML) - node.innerHTML = '
    '; + destroy : function() { + var t = this; - return node; - }; + each(t.events, function(e, i) { + t._remove(e.obj, e.name, e.cfunc); + e.obj = e.cfunc = null; + }); - // Restore start/end points - restoreEndPoint('start'); - restoreEndPoint('end'); + t.events = []; + t = null; + }, - rng = dom.createRng(); - rng.setStart(addBogus(startContainer), startOffset); - rng.setEnd(addBogus(endContainer), endOffset); - t.setRng(rng); - } else if (bookmark.name) { - t.select(dom.select(bookmark.name)[bookmark.index]); - } else if (bookmark.rng) - t.setRng(bookmark.rng); + _add : function(o, n, f) { + if (o.attachEvent) + o.attachEvent('on' + n, f); + else if (o.addEventListener) + o.addEventListener(n, f, false); + else + o['on' + n] = f; + }, + + _remove : function(o, n, f) { + if (o) { + try { + if (o.detachEvent) + o.detachEvent('on' + n, f); + else if (o.removeEventListener) + o.removeEventListener(n, f, false); + else + o['on' + n] = null; + } catch (ex) { + // Might fail with permission denined on IE so we just ignore that + } } }, - select : function(node, content) { - var t = this, dom = t.dom, rng = dom.createRng(), idx; - - idx = dom.nodeIndex(node); - rng.setStart(node.parentNode, idx); - rng.setEnd(node.parentNode, idx + 1); + _pageInit : function(win) { + var t = this; - // Find first/last text node or BR element - if (content) { - function setPoint(node, start) { - var walker = new tinymce.dom.TreeWalker(node, node); + // Keep it from running more than once + if (t.domLoaded) + return; - do { - // Text node - if (node.nodeType == 3 && tinymce.trim(node.nodeValue).length != 0) { - if (start) - rng.setStart(node, 0); - else - rng.setEnd(node, node.nodeValue.length); + t.domLoaded = true; - return; - } + each(t.inits, function(c) { + c(); + }); - // BR element - if (node.nodeName == 'BR') { - if (start) - rng.setStartBefore(node); - else - rng.setEndBefore(node); + t.inits = []; + }, - return; - } - } while (node = (start ? walker.next() : walker.prev())); - }; + _wait : function(win) { + var t = this, doc = win.document; - setPoint(node, 1); - setPoint(node); + // No need since the document is already loaded + if (win.tinyMCE_GZ && tinyMCE_GZ.loaded) { + t.domLoaded = 1; + return; } - t.setRng(rng); - - return node; - }, + // Use IE method + if (doc.attachEvent) { + doc.attachEvent("onreadystatechange", function() { + if (doc.readyState === "complete") { + doc.detachEvent("onreadystatechange", arguments.callee); + t._pageInit(win); + } + }); - isCollapsed : function() { - var t = this, r = t.getRng(), s = t.getSel(); + if (doc.documentElement.doScroll && win == win.top) { + (function() { + if (t.domLoaded) + return; - if (!r || r.item) - return false; + try { + // If IE is used, use the trick by Diego Perini licensed under MIT by request to the author. + // http://javascript.nwbox.com/IEContentLoaded/ + doc.documentElement.doScroll("left"); + } catch (ex) { + setTimeout(arguments.callee, 0); + return; + } - if (r.compareEndPoints) - return r.compareEndPoints('StartToEnd', r) === 0; + t._pageInit(win); + })(); + } + } else if (doc.addEventListener) { + t._add(win, 'DOMContentLoaded', function() { + t._pageInit(win); + }); + } - return !s || r.collapsed; + t._add(win, 'load', function() { + t._pageInit(win); + }); }, - collapse : function(b) { - var t = this, r = t.getRng(), n; + _stoppers : { + preventDefault : function() { + this.returnValue = false; + }, - // Control range on IE - if (r.item) { - n = r.item(0); - r = this.win.document.body.createTextRange(); - r.moveToElementText(n); + stopPropagation : function() { + this.cancelBubble = true; } + } + }); - r.collapse(!!b); - t.setRng(r); - }, - - getSel : function() { - var t = this, w = this.win; + Event = tinymce.dom.Event = new tinymce.dom.EventUtils(); - return w.getSelection ? w.getSelection() : w.document.selection; - }, + // Dispatch DOM content loaded event for IE and Safari + Event._wait(window); - getRng : function(w3c) { - var t = this, s, r; + tinymce.addUnload(function() { + Event.destroy(); + }); +})(tinymce); - // Found tridentSel object then we need to use that one - if (w3c && t.tridentSel) - return t.tridentSel.getRangeAt(0); +(function(tinymce) { + tinymce.dom.Element = function(id, settings) { + var t = this, dom, el; - try { - if (s = t.getSel()) - r = s.rangeCount > 0 ? s.getRangeAt(0) : (s.createRange ? s.createRange() : t.win.document.createRange()); - } catch (ex) { - // IE throws unspecified error here if TinyMCE is placed in a frame/iframe - } + t.settings = settings = settings || {}; + t.id = id; + t.dom = dom = settings.dom || tinymce.DOM; - // No range found then create an empty one - // This can occur when the editor is placed in a hidden container element on Gecko - // Or on IE when there was an exception - if (!r) - r = t.win.document.createRange ? t.win.document.createRange() : t.win.document.body.createTextRange(); + // Only IE leaks DOM references, this is a lot faster + if (!tinymce.isIE) + el = dom.get(t.id); - if (t.selectedRange && t.explicitRange) { - if (r.compareBoundaryPoints(r.START_TO_START, t.selectedRange) === 0 && r.compareBoundaryPoints(r.END_TO_END, t.selectedRange) === 0) { - // Safari, Opera and Chrome only ever select text which causes the range to change. - // This lets us use the originally set range if the selection hasn't been changed by the user. - r = t.explicitRange; - } else { - t.selectedRange = null; - t.explicitRange = null; - } - } - return r; - }, + tinymce.each( + ('getPos,getRect,getParent,add,setStyle,getStyle,setStyles,' + + 'setAttrib,setAttribs,getAttrib,addClass,removeClass,' + + 'hasClass,getOuterHTML,setOuterHTML,remove,show,hide,' + + 'isHidden,setHTML,get').split(/,/) + , function(k) { + t[k] = function() { + var a = [id], i; - setRng : function(r) { - var s, t = this; - - if (!t.tridentSel) { - s = t.getSel(); + for (i = 0; i < arguments.length; i++) + a.push(arguments[i]); - if (s) { - t.explicitRange = r; - s.removeAllRanges(); - s.addRange(r); - t.selectedRange = s.getRangeAt(0); - } - } else { - // Is W3C Range - if (r.cloneRange) { - t.tridentSel.addRange(r); - return; - } + a = dom[k].apply(dom, a); + t.update(k); - // Is IE specific range - try { - r.select(); - } catch (ex) { - // Needed for some odd IE bug #1843306 - } - } - }, + return a; + }; + }); - setNode : function(n) { - var t = this; + tinymce.extend(t, { + on : function(n, f, s) { + return tinymce.dom.Event.add(t.id, n, f, s); + }, - t.setContent(t.dom.getOuterHTML(n)); + getXY : function() { + return { + x : parseInt(t.getStyle('left')), + y : parseInt(t.getStyle('top')) + }; + }, - return n; - }, + getSize : function() { + var n = dom.get(t.id); - getNode : function() { - var t = this, rng = t.getRng(), sel = t.getSel(), elm; + return { + w : parseInt(t.getStyle('width') || n.clientWidth), + h : parseInt(t.getStyle('height') || n.clientHeight) + }; + }, - if (rng.setStart) { - // Range maybe lost after the editor is made visible again - if (!rng) - return t.dom.getRoot(); + moveTo : function(x, y) { + t.setStyles({left : x, top : y}); + }, - elm = rng.commonAncestorContainer; + moveBy : function(x, y) { + var p = t.getXY(); - // Handle selection a image or other control like element such as anchors - if (!rng.collapsed) { - if (rng.startContainer == rng.endContainer) { - if (rng.startOffset - rng.endOffset < 2) { - if (rng.startContainer.hasChildNodes()) - elm = rng.startContainer.childNodes[rng.startOffset]; - } - } + t.moveTo(p.x + x, p.y + y); + }, - // If the anchor node is a element instead of a text node then return this element - if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1) - return sel.anchorNode.childNodes[sel.anchorOffset]; - } + resizeTo : function(w, h) { + t.setStyles({width : w, height : h}); + }, - if (elm && elm.nodeType == 3) - return elm.parentNode; + resizeBy : function(w, h) { + var s = t.getSize(); - return elm; - } + t.resizeTo(s.w + w, s.h + h); + }, - return rng.item ? rng.item(0) : rng.parentElement(); - }, + update : function(k) { + var b; - getSelectedBlocks : function(st, en) { - var t = this, dom = t.dom, sb, eb, n, bl = []; + if (tinymce.isIE6 && settings.blocker) { + k = k || ''; - sb = dom.getParent(st || t.getStart(), dom.isBlock); - eb = dom.getParent(en || t.getEnd(), dom.isBlock); + // Ignore getters + if (k.indexOf('get') === 0 || k.indexOf('has') === 0 || k.indexOf('is') === 0) + return; - if (sb) - bl.push(sb); + // Remove blocker on remove + if (k == 'remove') { + dom.remove(t.blocker); + return; + } - if (sb && eb && sb != eb) { - n = sb; + if (!t.blocker) { + t.blocker = dom.uniqueId(); + b = dom.add(settings.container || dom.getRoot(), 'iframe', {id : t.blocker, style : 'position:absolute;', frameBorder : 0, src : 'javascript:""'}); + dom.setStyle(b, 'opacity', 0); + } else + b = dom.get(t.blocker); - while ((n = n.nextSibling) && n != eb) { - if (dom.isBlock(n)) - bl.push(n); + dom.setStyles(b, { + left : t.getStyle('left', 1), + top : t.getStyle('top', 1), + width : t.getStyle('width', 1), + height : t.getStyle('height', 1), + display : t.getStyle('display', 1), + zIndex : parseInt(t.getStyle('zIndex', 1) || 0) - 1 + }); } } + }); + }; +})(tinymce); - if (eb && sb != eb) - bl.push(eb); +(function(tinymce) { + function trimNl(s) { + return s.replace(/[\n\r]+/g, ''); + }; - return bl; - }, + // Shorten names + var is = tinymce.is, isIE = tinymce.isIE, each = tinymce.each; - destroy : function(s) { + tinymce.create('tinymce.dom.Selection', { + Selection : function(dom, win, serializer) { var t = this; - t.win = null; + t.dom = dom; + t.win = win; + t.serializer = serializer; - if (t.tridentSel) - t.tridentSel.destroy(); + // Add events + each([ + 'onBeforeSetContent', - // Manual destroy then remove unload handler - if (!s) - tinymce.removeUnload(t.destroy); - } - }); -})(tinymce); + 'onBeforeGetContent', -(function(tinymce) { - tinymce.create('tinymce.dom.XMLWriter', { - node : null, - - XMLWriter : function(s) { - // Get XML document - function getXML() { - var i = document.implementation; - - if (!i || !i.createDocument) { - // Try IE objects - try {return new ActiveXObject('MSXML2.DOMDocument');} catch (ex) {} - try {return new ActiveXObject('Microsoft.XmlDom');} catch (ex) {} - } else - return i.createDocument('', '', null); - }; + 'onSetContent', - this.doc = getXML(); - - // Since Opera and WebKit doesn't escape > into > we need to do it our self to normalize the output for all browsers - this.valid = tinymce.isOpera || tinymce.isWebKit; + 'onGetContent' + ], function(e) { + t[e] = new tinymce.util.Dispatcher(t); + }); - this.reset(); - }, + // No W3C Range support + if (!t.win.getSelection) + t.tridentSel = new tinymce.dom.TridentSelection(t); - reset : function() { - var t = this, d = t.doc; + if (tinymce.isIE && dom.boxModel) + this._fixIESelection(); - if (d.firstChild) - d.removeChild(d.firstChild); + // Prevent leaks + tinymce.addUnload(t.destroy, t); + }, - t.node = d.appendChild(d.createElement("html")); + setCursorLocation: function(node, offset) { + var t = this; var r = t.dom.createRng(); + r.setStart(node, offset); + r.setEnd(node, offset); + t.setRng(r); + t.collapse(false); }, + getContent : function(s) { + var t = this, r = t.getRng(), e = t.dom.create("body"), se = t.getSel(), wb, wa, n; - writeStartElement : function(n) { - var t = this; + s = s || {}; + wb = wa = ''; + s.get = true; + s.format = s.format || 'html'; + s.forced_root_block = ''; + t.onBeforeGetContent.dispatch(t, s); - t.node = t.node.appendChild(t.doc.createElement(n)); - }, + if (s.format == 'text') + return t.isCollapsed() ? '' : (r.text || (se.toString ? se.toString() : '')); - writeAttribute : function(n, v) { - if (this.valid) - v = v.replace(/>/g, '%MCGT%'); + if (r.cloneContents) { + n = r.cloneContents(); - this.node.setAttribute(n, v); - }, + if (n) + e.appendChild(n); + } else if (is(r.item) || is(r.htmlText)) { + // IE will produce invalid markup if elements are present that + // it doesn't understand like custom elements or HTML5 elements. + // Adding a BR in front of the contents and then remoiving it seems to fix it though. + e.innerHTML = '
    ' + (r.item ? r.item(0).outerHTML : r.htmlText); + e.removeChild(e.firstChild); + } else + e.innerHTML = r.toString(); - writeEndElement : function() { - this.node = this.node.parentNode; - }, + // Keep whitespace before and after + if (/^\s/.test(e.innerHTML)) + wb = ' '; - writeFullEndElement : function() { - var t = this, n = t.node; + if (/\s+$/.test(e.innerHTML)) + wa = ' '; - n.appendChild(t.doc.createTextNode("")); - t.node = n.parentNode; - }, + s.getInner = true; - writeText : function(v) { - if (this.valid) - v = v.replace(/>/g, '%MCGT%'); + s.content = t.isCollapsed() ? '' : wb + t.serializer.serialize(e, s) + wa; + t.onGetContent.dispatch(t, s); - this.node.appendChild(this.doc.createTextNode(v)); + return s.content; }, - writeCDATA : function(v) { - this.node.appendChild(this.doc.createCDATASection(v)); - }, + setContent : function(content, args) { + var self = this, rng = self.getRng(), caretNode, doc = self.win.document, frag, temp; - writeComment : function(v) { - // Fix for bug #2035694 - if (tinymce.isIE) - v = v.replace(/^\-|\-$/g, ' '); + args = args || {format : 'html'}; + args.set = true; + content = args.content = content; - this.node.appendChild(this.doc.createComment(v.replace(/\-\-/g, ' '))); - }, + // Dispatch before set content event + if (!args.no_events) + self.onBeforeSetContent.dispatch(self, args); - getContent : function() { - var h; + content = args.content; - h = this.doc.xml || new XMLSerializer().serializeToString(this.doc); - h = h.replace(/<\?[^?]+\?>||<\/html>||]+>/g, ''); - h = h.replace(/ ?\/>/g, ' />'); + if (rng.insertNode) { + // Make caret marker since insertNode places the caret in the beginning of text after insert + content += '_'; - if (this.valid) - h = h.replace(/\%MCGT%/g, '>'); + // Delete and insert new node + if (rng.startContainer == doc && rng.endContainer == doc) { + // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents + doc.body.innerHTML = content; + } else { + rng.deleteContents(); - return h; - } - }); -})(tinymce); + if (doc.body.childNodes.length == 0) { + doc.body.innerHTML = content; + } else { + // createContextualFragment doesn't exists in IE 9 DOMRanges + if (rng.createContextualFragment) { + rng.insertNode(rng.createContextualFragment(content)); + } else { + // Fake createContextualFragment call in IE 9 + frag = doc.createDocumentFragment(); + temp = doc.createElement('div'); -(function(tinymce) { - tinymce.create('tinymce.dom.StringWriter', { - str : null, - tags : null, - count : 0, - settings : null, - indent : null, - - StringWriter : function(s) { - this.settings = tinymce.extend({ - indent_char : ' ', - indentation : 0 - }, s); + frag.appendChild(temp); + temp.outerHTML = content; - this.reset(); - }, + rng.insertNode(frag); + } + } + } - reset : function() { - this.indent = ''; - this.str = ""; - this.tags = []; - this.count = 0; - }, + // Move to caret marker + caretNode = self.dom.get('__caret'); - writeStartElement : function(n) { - this._writeAttributesEnd(); - this.writeRaw('<' + n); - this.tags.push(n); - this.inAttr = true; - this.count++; - this.elementCount = this.count; - }, + // Make sure we wrap it compleatly, Opera fails with a simple select call + rng = doc.createRange(); + rng.setStartBefore(caretNode); + rng.setEndBefore(caretNode); + self.setRng(rng); - writeAttribute : function(n, v) { - var t = this; + // Remove the caret position + self.dom.remove('__caret'); + + try { + self.setRng(rng); + } catch (ex) { + // Might fail on Opera for some odd reason + } + } else { + if (rng.item) { + // Delete content and get caret text selection + doc.execCommand('Delete', false, null); + rng = self.getRng(); + } + + // Explorer removes spaces from the beginning of pasted contents + if (/^\s+/.test(content)) { + rng.pasteHTML('_' + content); + self.dom.remove('__mce_tmp'); + } else + rng.pasteHTML(content); + } - t.writeRaw(" " + t.encode(n) + '="' + t.encode(v) + '"'); + // Dispatch set content event + if (!args.no_events) + self.onSetContent.dispatch(self, args); }, - writeEndElement : function() { - var n; + getStart : function() { + var rng = this.getRng(), startElement, parentElement, checkRng, node; - if (this.tags.length > 0) { - n = this.tags.pop(); + if (rng.duplicate || rng.item) { + // Control selection, return first item + if (rng.item) + return rng.item(0); - if (this._writeAttributesEnd(1)) - this.writeRaw(''); + // Get start element + checkRng = rng.duplicate(); + checkRng.collapse(1); + startElement = checkRng.parentElement(); - if (this.settings.indentation > 0) - this.writeRaw('\n'); - } - }, + // Check if range parent is inside the start element, then return the inner parent element + // This will fix issues when a single element is selected, IE would otherwise return the wrong start element + parentElement = node = rng.parentElement(); + while (node = node.parentNode) { + if (node == startElement) { + startElement = parentElement; + break; + } + } - writeFullEndElement : function() { - if (this.tags.length > 0) { - this._writeAttributesEnd(); - this.writeRaw(''); + return startElement; + } else { + startElement = rng.startContainer; - if (this.settings.indentation > 0) - this.writeRaw('\n'); - } - }, + if (startElement.nodeType == 1 && startElement.hasChildNodes()) + startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)]; - writeText : function(v) { - this._writeAttributesEnd(); - this.writeRaw(this.encode(v)); - this.count++; - }, + if (startElement && startElement.nodeType == 3) + return startElement.parentNode; - writeCDATA : function(v) { - this._writeAttributesEnd(); - this.writeRaw(''); - this.count++; + return startElement; + } }, - writeComment : function(v) { - this._writeAttributesEnd(); - this.writeRaw(''); - this.count++; - }, + getEnd : function() { + var t = this, r = t.getRng(), e, eo; - writeRaw : function(v) { - this.str += v; - }, + if (r.duplicate || r.item) { + if (r.item) + return r.item(0); - encode : function(s) { - return s.replace(/[<>&"]/g, function(v) { - switch (v) { - case '<': - return '<'; + r = r.duplicate(); + r.collapse(0); + e = r.parentElement(); - case '>': - return '>'; + if (e && e.nodeName == 'BODY') + return e.lastChild || e; - case '&': - return '&'; + return e; + } else { + e = r.endContainer; + eo = r.endOffset; - case '"': - return '"'; - } + if (e.nodeType == 1 && e.hasChildNodes()) + e = e.childNodes[eo > 0 ? eo - 1 : eo]; - return v; - }); - }, + if (e && e.nodeType == 3) + return e.parentNode; - getContent : function() { - return this.str; + return e; + } }, - _writeAttributesEnd : function(s) { - if (!this.inAttr) - return; + getBookmark : function(type, normalized) { + var t = this, dom = t.dom, rng, rng2, id, collapsed, name, element, index, chr = '\uFEFF', styles; - this.inAttr = false; + function findIndex(name, element) { + var index = 0; - if (s && this.elementCount == this.count) { - this.writeRaw(' />'); - return false; - } + each(dom.select(name), function(node, i) { + if (node == element) + index = i; + }); - this.writeRaw('>'); + return index; + }; - return true; - } - }); -})(tinymce); + if (type == 2) { + function getLocation() { + var rng = t.getRng(true), root = dom.getRoot(), bookmark = {}; -(function(tinymce) { - // Shorten names - var extend = tinymce.extend, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher, isIE = tinymce.isIE, isGecko = tinymce.isGecko; + function getPoint(rng, start) { + var container = rng[start ? 'startContainer' : 'endContainer'], + offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0; - function wildcardToRE(s) { - return s.replace(/([?+*])/g, '.$1'); - }; + if (container.nodeType == 3) { + if (normalized) { + for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling) + offset += node.nodeValue.length; + } - tinymce.create('tinymce.dom.Serializer', { - Serializer : function(s) { - var t = this; + point.push(offset); + } else { + childNodes = container.childNodes; - t.key = 0; - t.onPreProcess = new Dispatcher(t); - t.onPostProcess = new Dispatcher(t); + if (offset >= childNodes.length && childNodes.length) { + after = 1; + offset = Math.max(0, childNodes.length - 1); + } - try { - t.writer = new tinymce.dom.XMLWriter(); - } catch (ex) { - // IE might throw exception if ActiveX is disabled so we then switch to the slightly slower StringWriter - t.writer = new tinymce.dom.StringWriter(); - } + point.push(t.dom.nodeIndex(childNodes[offset], normalized) + after); + } - // Default settings - t.settings = s = extend({ - dom : tinymce.DOM, - valid_nodes : 0, - node_filter : 0, - attr_filter : 0, - invalid_attrs : /^(_mce_|_moz_|sizset|sizcache)/, - closed : /^(br|hr|input|meta|img|link|param|area)$/, - entity_encoding : 'named', - entities : '160,nbsp,161,iexcl,162,cent,163,pound,164,curren,165,yen,166,brvbar,167,sect,168,uml,169,copy,170,ordf,171,laquo,172,not,173,shy,174,reg,175,macr,176,deg,177,plusmn,178,sup2,179,sup3,180,acute,181,micro,182,para,183,middot,184,cedil,185,sup1,186,ordm,187,raquo,188,frac14,189,frac12,190,frac34,191,iquest,192,Agrave,193,Aacute,194,Acirc,195,Atilde,196,Auml,197,Aring,198,AElig,199,Ccedil,200,Egrave,201,Eacute,202,Ecirc,203,Euml,204,Igrave,205,Iacute,206,Icirc,207,Iuml,208,ETH,209,Ntilde,210,Ograve,211,Oacute,212,Ocirc,213,Otilde,214,Ouml,215,times,216,Oslash,217,Ugrave,218,Uacute,219,Ucirc,220,Uuml,221,Yacute,222,THORN,223,szlig,224,agrave,225,aacute,226,acirc,227,atilde,228,auml,229,aring,230,aelig,231,ccedil,232,egrave,233,eacute,234,ecirc,235,euml,236,igrave,237,iacute,238,icirc,239,iuml,240,eth,241,ntilde,242,ograve,243,oacute,244,ocirc,245,otilde,246,ouml,247,divide,248,oslash,249,ugrave,250,uacute,251,ucirc,252,uuml,253,yacute,254,thorn,255,yuml,402,fnof,913,Alpha,914,Beta,915,Gamma,916,Delta,917,Epsilon,918,Zeta,919,Eta,920,Theta,921,Iota,922,Kappa,923,Lambda,924,Mu,925,Nu,926,Xi,927,Omicron,928,Pi,929,Rho,931,Sigma,932,Tau,933,Upsilon,934,Phi,935,Chi,936,Psi,937,Omega,945,alpha,946,beta,947,gamma,948,delta,949,epsilon,950,zeta,951,eta,952,theta,953,iota,954,kappa,955,lambda,956,mu,957,nu,958,xi,959,omicron,960,pi,961,rho,962,sigmaf,963,sigma,964,tau,965,upsilon,966,phi,967,chi,968,psi,969,omega,977,thetasym,978,upsih,982,piv,8226,bull,8230,hellip,8242,prime,8243,Prime,8254,oline,8260,frasl,8472,weierp,8465,image,8476,real,8482,trade,8501,alefsym,8592,larr,8593,uarr,8594,rarr,8595,darr,8596,harr,8629,crarr,8656,lArr,8657,uArr,8658,rArr,8659,dArr,8660,hArr,8704,forall,8706,part,8707,exist,8709,empty,8711,nabla,8712,isin,8713,notin,8715,ni,8719,prod,8721,sum,8722,minus,8727,lowast,8730,radic,8733,prop,8734,infin,8736,ang,8743,and,8744,or,8745,cap,8746,cup,8747,int,8756,there4,8764,sim,8773,cong,8776,asymp,8800,ne,8801,equiv,8804,le,8805,ge,8834,sub,8835,sup,8836,nsub,8838,sube,8839,supe,8853,oplus,8855,otimes,8869,perp,8901,sdot,8968,lceil,8969,rceil,8970,lfloor,8971,rfloor,9001,lang,9002,rang,9674,loz,9824,spades,9827,clubs,9829,hearts,9830,diams,338,OElig,339,oelig,352,Scaron,353,scaron,376,Yuml,710,circ,732,tilde,8194,ensp,8195,emsp,8201,thinsp,8204,zwnj,8205,zwj,8206,lrm,8207,rlm,8211,ndash,8212,mdash,8216,lsquo,8217,rsquo,8218,sbquo,8220,ldquo,8221,rdquo,8222,bdquo,8224,dagger,8225,Dagger,8240,permil,8249,lsaquo,8250,rsaquo,8364,euro', - valid_elements : '*[*]', - extended_valid_elements : 0, - invalid_elements : 0, - fix_table_elements : 1, - fix_list_elements : true, - fix_content_duplication : true, - convert_fonts_to_spans : false, - font_size_classes : 0, - apply_source_formatting : 0, - indent_mode : 'simple', - indent_char : '\t', - indent_levels : 1, - remove_linebreaks : 1, - remove_redundant_brs : 1, - element_format : 'xhtml' - }, s); + for (; container && container != root; container = container.parentNode) + point.push(t.dom.nodeIndex(container, normalized)); - t.dom = s.dom; - t.schema = s.schema; + return point; + }; - // Use raw entities if no entities are defined - if (s.entity_encoding == 'named' && !s.entities) - s.entity_encoding = 'raw'; + bookmark.start = getPoint(rng, true); - if (s.remove_redundant_brs) { - t.onPostProcess.add(function(se, o) { - // Remove single BR at end of block elements since they get rendered - o.content = o.content.replace(/(
    \s*)+<\/(p|h[1-6]|div|li)>/gi, function(a, b, c) { - // Check if it's a single element - if (/^
    \s*<\//.test(a)) - return ''; + if (!t.isCollapsed()) + bookmark.end = getPoint(rng); - return a; - }); - }); - } + return bookmark; + }; - // Remove XHTML element endings i.e. produce crap :) XHTML is better - if (s.element_format == 'html') { - t.onPostProcess.add(function(se, o) { - o.content = o.content.replace(/<([^>]+) \/>/g, '<$1>'); - }); + if (t.tridentSel) + return t.tridentSel.getBookmark(type); + + return getLocation(); } - if (s.fix_list_elements) { - t.onPreProcess.add(function(se, o) { - var nl, x, a = ['ol', 'ul'], i, n, p, r = /^(OL|UL)$/, np; + // Handle simple range + if (type) + return {rng : t.getRng()}; + + rng = t.getRng(); + id = dom.uniqueId(); + collapsed = tinyMCE.activeEditor.selection.isCollapsed(); + styles = 'overflow:hidden;line-height:0px'; + + // Explorer method + if (rng.duplicate || rng.item) { + // Text selection + if (!rng.item) { + rng2 = rng.duplicate(); + + try { + // Insert start marker + rng.collapse(); + rng.pasteHTML('' + chr + ''); + + // Insert end marker + if (!collapsed) { + rng2.collapse(false); - function prevNode(e, n) { - var a = n.split(','), i; + // Detect the empty space after block elements in IE and move the end back one character

    ] becomes

    ]

    + rng.moveToElementText(rng2.parentElement()); + if (rng.compareEndPoints('StartToEnd', rng2) == 0) + rng2.move('character', -1); - while ((e = e.previousSibling) != null) { - for (i=0; i' + chr + ''); } - + } catch (ex) { + // IE might throw unspecified error so lets ignore it return null; - }; + } + } else { + // Control selection + element = rng.item(0); + name = element.nodeName; - for (x=0; x= 1767) { - each(t.dom.select('p table', o.node).reverse(), function(n) { - var parent = t.dom.getParent(n.parentNode, 'table,p'); + t.moveToBookmark({id : id, keep : 1}); - if (parent.nodeName != 'TABLE') { - try { - t.dom.split(parent, n); - } catch (ex) { - // IE can sometimes fire an unknown runtime error so we just ignore it - } - } - }); - } - }); - } + return {id : id}; }, - setEntities : function(s) { - var t = this, a, i, l = {}, v; + moveToBookmark : function(bookmark) { + var t = this, dom = t.dom, marker1, marker2, rng, root, startContainer, endContainer, startOffset, endOffset; - // No need to setup more than once - if (t.entityLookup) - return; + if (bookmark) { + if (bookmark.start) { + rng = dom.createRng(); + root = dom.getRoot(); - // Build regex and lookup array - a = s.split(','); - for (i = 0; i < a.length; i += 2) { - v = a[i]; + function setEndPoint(start) { + var point = bookmark[start ? 'start' : 'end'], i, node, offset, children; - // Don't add default & " etc. - if (v == 34 || v == 38 || v == 60 || v == 62) - continue; + if (point) { + offset = point[0]; - l[String.fromCharCode(a[i])] = a[i + 1]; + // Find container node + for (node = root, i = point.length - 1; i >= 1; i--) { + children = node.childNodes; - v = parseInt(a[i]).toString(16); - } + if (point[i] > children.length - 1) + return; - t.entityLookup = l; - }, + node = children[point[i]]; + } - setRules : function(s) { - var t = this; + // Move text offset to best suitable location + if (node.nodeType === 3) + offset = Math.min(point[0], node.nodeValue.length); - t._setup(); - t.rules = {}; - t.wildRules = []; - t.validElements = {}; + // Move element offset to best suitable location + if (node.nodeType === 1) + offset = Math.min(point[0], node.childNodes.length); - return t.addRules(s); - }, + // Set offset within container node + if (start) + rng.setStart(node, offset); + else + rng.setEnd(node, offset); + } - addRules : function(s) { - var t = this, dr; + return true; + }; - if (!s) - return; + if (t.tridentSel) + return t.tridentSel.moveToBookmark(bookmark); - t._setup(); + if (setEndPoint(true) && setEndPoint()) { + t.setRng(rng); + } + } else if (bookmark.id) { + function restoreEndPoint(suffix) { + var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep; - each(s.split(','), function(s) { - var p = s.split(/\[|\]/), tn = p[0].split('/'), ra, at, wat, va = []; + if (marker) { + node = marker.parentNode; - // Extend with default rules - if (dr) - at = tinymce.extend([], dr.attribs); + if (suffix == 'start') { + if (!keep) { + idx = dom.nodeIndex(marker); + } else { + node = marker.firstChild; + idx = 1; + } - // Parse attributes - if (p.length > 1) { - each(p[1].split('|'), function(s) { - var ar = {}, i; + startContainer = endContainer = node; + startOffset = endOffset = idx; + } else { + if (!keep) { + idx = dom.nodeIndex(marker); + } else { + node = marker.firstChild; + idx = 1; + } - at = at || []; + endContainer = node; + endOffset = idx; + } - // Parse attribute rule - s = s.replace(/::/g, '~'); - s = /^([!\-])?([\w*.?~_\-]+|)([=:<])?(.+)?$/.exec(s); - s[2] = s[2].replace(/~/g, ':'); + if (!keep) { + prev = marker.previousSibling; + next = marker.nextSibling; - // Add required attributes - if (s[1] == '!') { - ra = ra || []; - ra.push(s[2]); - } + // Remove all marker text nodes + each(tinymce.grep(marker.childNodes), function(node) { + if (node.nodeType == 3) + node.nodeValue = node.nodeValue.replace(/\uFEFF/g, ''); + }); - // Remove inherited attributes - if (s[1] == '-') { - for (i = 0; i ' : ' '; - // Add validation values - case '<': - ar.validVals = s[4].split('?'); - break; - } + return node; + }; - if (/[*.?]/.test(s[2])) { - wat = wat || []; - ar.nameRE = new RegExp('^' + wildcardToRE(s[2]) + '$'); - wat.push(ar); - } else { - ar.name = s[2]; - at.push(ar); - } + // Restore start/end points + restoreEndPoint('start'); + restoreEndPoint('end'); - va.push(s[2]); - }); - } + if (startContainer) { + rng = dom.createRng(); + rng.setStart(addBogus(startContainer), startOffset); + rng.setEnd(addBogus(endContainer), endOffset); + t.setRng(rng); + } + } else if (bookmark.name) { + t.select(dom.select(bookmark.name)[bookmark.index]); + } else if (bookmark.rng) + t.setRng(bookmark.rng); + } + }, - // Handle element names - each(tn, function(s, i) { - var pr = s.charAt(0), x = 1, ru = {}; + select : function(node, content) { + var t = this, dom = t.dom, rng = dom.createRng(), idx; - // Extend with default rule data - if (dr) { - if (dr.noEmpty) - ru.noEmpty = dr.noEmpty; + if (node) { + idx = dom.nodeIndex(node); + rng.setStart(node.parentNode, idx); + rng.setEnd(node.parentNode, idx + 1); + + // Find first/last text node or BR element + if (content) { + function setPoint(node, start) { + var walker = new tinymce.dom.TreeWalker(node, node); + + do { + // Text node + if (node.nodeType == 3 && tinymce.trim(node.nodeValue).length != 0) { + if (start) + rng.setStart(node, 0); + else + rng.setEnd(node, node.nodeValue.length); - if (dr.fullEnd) - ru.fullEnd = dr.fullEnd; + return; + } - if (dr.padd) - ru.padd = dr.padd; - } + // BR element + if (node.nodeName == 'BR') { + if (start) + rng.setStartBefore(node); + else + rng.setEndBefore(node); - // Handle prefixes - switch (pr) { - case '-': - ru.noEmpty = true; - break; + return; + } + } while (node = (start ? walker.next() : walker.prev())); + }; - case '+': - ru.fullEnd = true; - break; + setPoint(node, 1); + setPoint(node); + } - case '#': - ru.padd = true; - break; + t.setRng(rng); + } - default: - x = 0; - } + return node; + }, - tn[i] = s = s.substring(x); - t.validElements[s] = 1; + isCollapsed : function() { + var t = this, r = t.getRng(), s = t.getSel(); - // Add element name or element regex - if (/[*.?]/.test(tn[0])) { - ru.nameRE = new RegExp('^' + wildcardToRE(tn[0]) + '$'); - t.wildRules = t.wildRules || {}; - t.wildRules.push(ru); - } else { - ru.name = tn[0]; + if (!r || r.item) + return false; - // Store away default rule - if (tn[0] == '@') - dr = ru; + if (r.compareEndPoints) + return r.compareEndPoints('StartToEnd', r) === 0; - t.rules[s] = ru; - } + return !s || r.collapsed; + }, - ru.attribs = at; + collapse : function(to_start) { + var self = this, rng = self.getRng(), node; - if (ra) - ru.requiredAttribs = ra; + // Control range on IE + if (rng.item) { + node = rng.item(0); + rng = self.win.document.body.createTextRange(); + rng.moveToElementText(node); + } - if (wat) { - // Build valid attributes regexp - s = ''; - each(va, function(v) { - if (s) - s += '|'; + rng.collapse(!!to_start); + self.setRng(rng); + }, - s += '(' + wildcardToRE(v) + ')'; - }); - ru.validAttribsRE = new RegExp('^' + s.toLowerCase() + '$'); - ru.wildAttribs = wat; - } - }); - }); + getSel : function() { + var t = this, w = this.win; - // Build valid elements regexp - s = ''; - each(t.validElements, function(v, k) { - if (s) - s += '|'; + return w.getSelection ? w.getSelection() : w.document.selection; + }, - if (k != '@') - s += k; - }); - t.validElementsRE = new RegExp('^(' + wildcardToRE(s.toLowerCase()) + ')$'); + getRng : function(w3c) { + var t = this, s, r, elm, doc = t.win.document; - //console.debug(t.validElementsRE.toString()); - //console.dir(t.rules); - //console.dir(t.wildRules); - }, + // Found tridentSel object then we need to use that one + if (w3c && t.tridentSel) + return t.tridentSel.getRangeAt(0); - findRule : function(n) { - var t = this, rl = t.rules, i, r; + try { + if (s = t.getSel()) + r = s.rangeCount > 0 ? s.getRangeAt(0) : (s.createRange ? s.createRange() : doc.createRange()); + } catch (ex) { + // IE throws unspecified error here if TinyMCE is placed in a frame/iframe + } - t._setup(); + // We have W3C ranges and it's IE then fake control selection since IE9 doesn't handle that correctly yet + if (tinymce.isIE && r && r.setStart && doc.selection.createRange().item) { + elm = doc.selection.createRange().item(0); + r = doc.createRange(); + r.setStartBefore(elm); + r.setEndAfter(elm); + } - // Exact match - r = rl[n]; - if (r) - return r; + // No range found then create an empty one + // This can occur when the editor is placed in a hidden container element on Gecko + // Or on IE when there was an exception + if (!r) + r = doc.createRange ? doc.createRange() : doc.body.createTextRange(); - // Try wildcards - rl = t.wildRules; - for (i = 0; i < rl.length; i++) { - if (rl[i].nameRE.test(n)) - return rl[i]; + if (t.selectedRange && t.explicitRange) { + if (r.compareBoundaryPoints(r.START_TO_START, t.selectedRange) === 0 && r.compareBoundaryPoints(r.END_TO_END, t.selectedRange) === 0) { + // Safari, Opera and Chrome only ever select text which causes the range to change. + // This lets us use the originally set range if the selection hasn't been changed by the user. + r = t.explicitRange; + } else { + t.selectedRange = null; + t.explicitRange = null; + } } - return null; + return r; }, - findAttribRule : function(ru, n) { - var i, wa = ru.wildAttribs; + setRng : function(r) { + var s, t = this; + + if (!t.tridentSel) { + s = t.getSel(); - for (i = 0; i < wa.length; i++) { - if (wa[i].nameRE.test(n)) - return wa[i]; - } + if (s) { + t.explicitRange = r; - return null; + try { + s.removeAllRanges(); + } catch (ex) { + // IE9 might throw errors here don't know why + } + + s.addRange(r); + t.selectedRange = s.getRangeAt(0); + } + } else { + // Is W3C Range + if (r.cloneRange) { + t.tridentSel.addRange(r); + return; + } + + // Is IE specific range + try { + r.select(); + } catch (ex) { + // Needed for some odd IE bug #1843306 + } + } }, - serialize : function(n, o) { - var h, t = this, doc, oldDoc, impl, selected; + setNode : function(n) { + var t = this; + + t.setContent(t.dom.getOuterHTML(n)); - t._setup(); - o = o || {}; - o.format = o.format || 'html'; - t.processObj = o; + return n; + }, - // IE looses the selected attribute on option elements so we need to store it - // See: http://support.microsoft.com/kb/829907 - if (isIE) { - selected = []; - each(n.getElementsByTagName('option'), function(n) { - var v = t.dom.getAttrib(n, 'selected'); + getNode : function() { + var t = this, rng = t.getRng(), sel = t.getSel(), elm, start = rng.startContainer, end = rng.endContainer; - selected.push(v ? v : null); - }); - } + // Range maybe lost after the editor is made visible again + if (!rng) + return t.dom.getRoot(); - n = n.cloneNode(true); + if (rng.setStart) { + elm = rng.commonAncestorContainer; - // IE looses the selected attribute on option elements so we need to restore it - if (isIE) { - each(n.getElementsByTagName('option'), function(n, i) { - t.dom.setAttrib(n, 'selected', selected[i]); - }); - } + // Handle selection a image or other control like element such as anchors + if (!rng.collapsed) { + if (rng.startContainer == rng.endContainer) { + if (rng.endOffset - rng.startOffset < 2) { + if (rng.startContainer.hasChildNodes()) + elm = rng.startContainer.childNodes[rng.startOffset]; + } + } - // Nodes needs to be attached to something in WebKit/Opera - // Older builds of Opera crashes if you attach the node to an document created dynamically - // and since we can't feature detect a crash we need to sniff the acutal build number - // This fix will make DOM ranges and make Sizzle happy! - impl = n.ownerDocument.implementation; - if (impl.createHTMLDocument && (tinymce.isOpera && opera.buildNumber() >= 1767)) { - // Create an empty HTML document - doc = impl.createHTMLDocument(""); + // If the anchor node is a element instead of a text node then return this element + //if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1) + // return sel.anchorNode.childNodes[sel.anchorOffset]; + + // Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent. + // This happens when you double click an underlined word in FireFox. + if (start.nodeType === 3 && end.nodeType === 3) { + function skipEmptyTextNodes(n, forwards) { + var orig = n; + while (n && n.nodeType === 3 && n.length === 0) { + n = forwards ? n.nextSibling : n.previousSibling; + } + return n || orig; + } + if (start.length === rng.startOffset) { + start = skipEmptyTextNodes(start.nextSibling, true); + } else { + start = start.parentNode; + } + if (rng.endOffset === 0) { + end = skipEmptyTextNodes(end.previousSibling, false); + } else { + end = end.parentNode; + } - // Add the element or it's children if it's a body element to the new document - each(n.nodeName == 'BODY' ? n.childNodes : [n], function(node) { - doc.body.appendChild(doc.importNode(node, true)); - }); + if (start && start === end) + return start; + } + } - // Grab first child or body element for serialization - if (n.nodeName != 'BODY') - n = doc.body.firstChild; - else - n = doc.body; + if (elm && elm.nodeType == 3) + return elm.parentNode; - // set the new document in DOMUtils so createElement etc works - oldDoc = t.dom.doc; - t.dom.doc = doc; + return elm; } - t.key = '' + (parseInt(t.key) + 1); + return rng.item ? rng.item(0) : rng.parentElement(); + }, - // Pre process - if (!o.no_events) { - o.node = n; - t.onPreProcess.dispatch(t, o); - } + getSelectedBlocks : function(st, en) { + var t = this, dom = t.dom, sb, eb, n, bl = []; - // Serialize HTML DOM into a string - t.writer.reset(); - t._info = o; - t._serializeNode(n, o.getInner); + sb = dom.getParent(st || t.getStart(), dom.isBlock); + eb = dom.getParent(en || t.getEnd(), dom.isBlock); - // Post process - o.content = t.writer.getContent(); + if (sb) + bl.push(sb); - // Restore the old document if it was changed - if (oldDoc) - t.dom.doc = oldDoc; + if (sb && eb && sb != eb) { + n = sb; - if (!o.no_events) - t.onPostProcess.dispatch(t, o); + var walker = new tinymce.dom.TreeWalker(sb, dom.getRoot()); + while ((n = walker.next()) && n != eb) { + if (dom.isBlock(n)) + bl.push(n); + } + } - t._postProcess(o); - o.node = null; + if (eb && sb != eb) + bl.push(eb); - return tinymce.trim(o.content); + return bl; }, - // Internal functions + normalize : function() { + var self = this, rng, normalized; - _postProcess : function(o) { - var t = this, s = t.settings, h = o.content, sc = [], p; - - if (o.format == 'html') { - // Protect some elements - p = t._protect({ - content : h, - patterns : [ - {pattern : /(]*>)(.*?)(<\/script>)/g}, - {pattern : /(]*>)(.*?)(<\/noscript>)/g}, - {pattern : /(]*>)(.*?)(<\/style>)/g}, - {pattern : /(]*>)(.*?)(<\/pre>)/g, encode : 1}, - {pattern : /()/g} - ] - }); + // Normalize only on non IE browsers for now + if (tinymce.isIE) + return; - h = p.content; + function normalizeEndPoint(start) { + var container, offset, walker, dom = self.dom, body = dom.getRoot(), node; - // Entity encode - if (s.entity_encoding !== 'raw') - h = t._encode(h); + container = rng[(start ? 'start' : 'end') + 'Container']; + offset = rng[(start ? 'start' : 'end') + 'Offset']; - // Use BR instead of   padded P elements inside editor and use

     

    outside editor -/* if (o.set) - h = h.replace(/

    \s+( | |\u00a0|
    )\s+<\/p>/g, '


    '); - else - h = h.replace(/

    \s+( | |\u00a0|
    )\s+<\/p>/g, '

    $1

    ');*/ + // If the container is a document move it to the body element + if (container.nodeType === 9) { + container = container.body; + offset = 0; + } - // Since Gecko and Safari keeps whitespace in the DOM we need to - // remove it inorder to match other browsers. But I think Gecko and Safari is right. - // This process is only done when getting contents out from the editor. - if (!o.set) { - // We need to replace paragraph whitespace with an nbsp before indentation to keep the \u00a0 char - h = h.replace(/

    \s+<\/p>|]+)>\s+<\/p>/g, s.entity_encoding == 'numeric' ? ' 

    ' : ' 

    '); + // If the container is body try move it into the closest text node or position + // TODO: Add more logic here to handle element selection cases + if (container === body) { + // Resolve the index + if (container.hasChildNodes()) { + container = container.childNodes[Math.min(!start && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1)]; + offset = 0; + + // Don't walk into elements that doesn't have any child nodes like a IMG + if (container.hasChildNodes()) { + // Walk the DOM to find a text node to place the caret at or a BR + node = container; + walker = new tinymce.dom.TreeWalker(container, body); + do { + // Found a text node use that position + if (node.nodeType === 3) { + offset = start ? 0 : node.nodeValue.length - 1; + container = node; + break; + } - if (s.remove_linebreaks) { - h = h.replace(/\r?\n|\r/g, ' '); - h = h.replace(/(<[^>]+>)\s+/g, '$1 '); - h = h.replace(/\s+(<\/[^>]+>)/g, ' $1'); - h = h.replace(/<(p|h[1-6]|blockquote|hr|div|table|tbody|tr|td|body|head|html|title|meta|style|pre|script|link|object) ([^>]+)>\s+/g, '<$1 $2>'); // Trim block start - h = h.replace(/<(p|h[1-6]|blockquote|hr|div|table|tbody|tr|td|body|head|html|title|meta|style|pre|script|link|object)>\s+/g, '<$1>'); // Trim block start - h = h.replace(/\s+<\/(p|h[1-6]|blockquote|hr|div|table|tbody|tr|td|body|head|html|title|meta|style|pre|script|link|object)>/g, ''); // Trim block end - } + // Found a BR element that we can place the caret before + if (node.nodeName === 'BR') { + offset = dom.nodeIndex(node); + container = node.parentNode; + break; + } + } while (node = (start ? walker.next() : walker.prev())); - // Simple indentation - if (s.apply_source_formatting && s.indent_mode == 'simple') { - // Add line breaks before and after block elements - h = h.replace(/<(\/?)(ul|hr|table|meta|link|tbody|tr|object|body|head|html|map)(|[^>]+)>\s*/g, '\n<$1$2$3>\n'); - h = h.replace(/\s*<(p|h[1-6]|blockquote|div|title|style|pre|script|td|li|area)(|[^>]+)>/g, '\n<$1$2>'); - h = h.replace(/<\/(p|h[1-6]|blockquote|div|title|style|pre|script|td|li)>\s*/g, '\n'); - h = h.replace(/\n\n/g, '\n'); + normalized = true; + } } } - h = t._unprotect(h, p); + // Set endpoint if it was normalized + if (normalized) + rng['set' + (start ? 'Start' : 'End')](container, offset); + }; - // Restore CDATA sections - h = h.replace(//g, ''); + rng = self.getRng(); - // Restore the \u00a0 character if raw mode is enabled - if (s.entity_encoding == 'raw') - h = h.replace(/

     <\/p>|]+)> <\/p>/g, '\u00a0

    '); + // Normalize the end points + normalizeEndPoint(true); + + if (rng.collapsed) + normalizeEndPoint(); - // Restore noscript elements - h = h.replace(/]+|)>([\s\S]*?)<\/noscript>/g, function(v, attribs, text) { - return '' + t.dom.decode(text.replace(//g, '')) + ''; - }); + // Set the selection if it was normalized + if (normalized) { + //console.log(self.dom.dumpRng(rng)); + self.setRng(rng); } - - o.content = h; }, - _serializeNode : function(n, inner) { - var t = this, s = t.settings, w = t.writer, hc, el, cn, i, l, a, at, no, v, nn, ru, ar, iv, closed, keep, type; - - if (!s.node_filter || s.node_filter(n)) { - switch (n.nodeType) { - case 1: // Element - if (n.hasAttribute ? n.hasAttribute('_mce_bogus') : n.getAttribute('_mce_bogus')) - return; - - iv = keep = false; - hc = n.hasChildNodes(); - nn = n.getAttribute('_mce_name') || n.nodeName.toLowerCase(); - - // Get internal type - type = n.getAttribute('_mce_type'); - if (type) { - if (!t._info.cleanup) { - iv = true; - return; - } else - keep = 1; - } - - // Add correct prefix on IE - if (isIE) { - if (n.scopeName !== 'HTML' && n.scopeName !== 'html') - nn = n.scopeName + ':' + nn; - } - - // Remove mce prefix on IE needed for the abbr element - if (nn.indexOf('mce:') === 0) - nn = nn.substring(4); + destroy : function(s) { + var t = this; - // Check if valid - if (!keep) { - if (!t.validElementsRE || !t.validElementsRE.test(nn) || (t.invalidElementsRE && t.invalidElementsRE.test(nn)) || inner) { - iv = true; - break; - } - } + t.win = null; - if (isIE) { - // Fix IE content duplication (DOM can have multiple copies of the same node) - if (s.fix_content_duplication) { - if (n._mce_serialized == t.key) - return; + // Manual destroy then remove unload handler + if (!s) + tinymce.removeUnload(t.destroy); + }, - n._mce_serialized = t.key; - } + // IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode + _fixIESelection : function() { + var dom = this.dom, doc = dom.doc, body = doc.body, started, startRng, htmlElm; - // IE sometimes adds a / infront of the node name - if (nn.charAt(0) == '/') - nn = nn.substring(1); - } else if (isGecko) { - // Ignore br elements - if (n.nodeName === 'BR' && n.getAttribute('type') == '_moz') - return; - } + // Make HTML element unselectable since we are going to handle selection by hand + doc.documentElement.unselectable = true; - // Check if valid child - if (s.validate_children) { - if (t.elementName && !t.schema.isValid(t.elementName, nn)) { - iv = true; - break; - } + // Return range from point or null if it failed + function rngFromPoint(x, y) { + var rng = body.createTextRange(); - t.elementName = nn; - } + try { + rng.moveToPoint(x, y); + } catch (ex) { + // IE sometimes throws and exception, so lets just ignore it + rng = null; + } - ru = t.findRule(nn); - - // No valid rule for this element could be found then skip it - if (!ru) { - iv = true; - break; - } + return rng; + }; - nn = ru.name || nn; - closed = s.closed.test(nn); + // Fires while the selection is changing + function selectionChange(e) { + var pointRng; - // Skip empty nodes or empty node name in IE - if ((!hc && ru.noEmpty) || (isIE && !nn)) { - iv = true; - break; - } + // Check if the button is down or not + if (e.button) { + // Create range from mouse position + pointRng = rngFromPoint(e.x, e.y); - // Check required - if (ru.requiredAttribs) { - a = ru.requiredAttribs; + if (pointRng) { + // Check if pointRange is before/after selection then change the endPoint + if (pointRng.compareEndPoints('StartToStart', startRng) > 0) + pointRng.setEndPoint('StartToStart', startRng); + else + pointRng.setEndPoint('EndToEnd', startRng); - for (i = a.length - 1; i >= 0; i--) { - if (this.dom.getAttrib(n, a[i]) !== '') - break; - } + pointRng.select(); + } + } else + endSelection(); + } - // None of the required was there - if (i == -1) { - iv = true; - break; - } - } + // Removes listeners + function endSelection() { + var rng = doc.selection.createRange(); - w.writeStartElement(nn); + // If the range is collapsed then use the last start range + if (startRng && !rng.item && rng.compareEndPoints('StartToEnd', rng) === 0) + startRng.select(); - // Add ordered attributes - if (ru.attribs) { - for (i=0, at = ru.attribs, l = at.length; i-1; i--) { - no = at[i]; + // Detect vertical scrollbar, since IE will fire a mousedown on the scrollbar and have target set as HTML + htmlElm = doc.documentElement; + if (htmlElm.scrollHeight > htmlElm.clientHeight) + return; - if (no.specified) { - a = no.nodeName.toLowerCase(); + started = 1; + // Setup start position + startRng = rngFromPoint(e.x, e.y); + if (startRng) { + // Listen for selection change events + dom.bind(doc, 'mouseup', endSelection); + dom.bind(doc, 'mousemove', selectionChange); - if (s.invalid_attrs.test(a) || !ru.validAttribsRE.test(a)) - continue; + dom.win.focus(); + startRng.select(); + } + } + }); + } + }); +})(tinymce); - ar = t.findAttribRule(ru, a); - v = t._getAttrib(n, ar, a); +(function(tinymce) { + tinymce.dom.Serializer = function(settings, dom, schema) { + var onPreProcess, onPostProcess, isIE = tinymce.isIE, each = tinymce.each, htmlParser; - if (v !== null) - w.writeAttribute(a, v); - } - } - } + // Support the old apply_source_formatting option + if (!settings.apply_source_formatting) + settings.indent = false; - // Keep type attribute - if (type && keep) - w.writeAttribute('_mce_type', type); + settings.remove_trailing_brs = true; - // Write text from script - if (nn === 'script' && tinymce.trim(n.innerHTML)) { - w.writeText('// '); // Padd it with a comment so it will parse on older browsers - w.writeCDATA(n.innerHTML.replace(/|<\[CDATA\[|\]\]>/g, '')); // Remove comments and cdata stuctures - hc = false; - break; - } + // Default DOM and Schema if they are undefined + dom = dom || tinymce.DOM; + schema = schema || new tinymce.html.Schema(settings); + settings.entity_encoding = settings.entity_encoding || 'named'; - // Padd empty nodes with a   - if (ru.padd) { - // If it has only one bogus child, padd it anyway workaround for

    '; - h += ''; - h += ''; - h += '
    ' + DOM.createHTML('a', {id : t.id + '_text', href : 'javascript:;', 'class' : 'mceText', onclick : "return false;", onmousedown : 'return false;'}, DOM.encode(t.settings.title)) + '' + DOM.createHTML('a', {id : t.id + '_open', tabindex : -1, href : 'javascript:;', 'class' : 'mceOpen', onclick : "return false;", onmousedown : 'return false;'}, '') + '
    '; + h = ''; + h += ''; + h += ''; + h += ''; return h; }, showMenu : function() { - var t = this, p1, p2, e = DOM.get(this.id), m; + var t = this, p2, e = DOM.get(this.id), m; if (t.isDisabled() || t.items.length == 0) return; @@ -8128,7 +9923,6 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { t.isMenuRendered = true; } - p1 = DOM.getPos(this.settings.menu_container); p2 = DOM.getPos(e); m = t.menu; @@ -8159,6 +9953,8 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { var t = this; if (t.menu && t.menu.isMenuVisible) { + DOM.removeClass(t.id, t.classPrefix + 'Selected'); + // Prevent double toogles by canceling the mouse click event to the button if (e && e.type == "mousedown" && (e.target.id == t.id + '_text' || e.target.id == t.id + '_open')) return; @@ -8181,7 +9977,10 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { max_height : 150 }); - m.onHideMenu.add(t.hideMenu, t); + m.onHideMenu.add(function() { + t.hideMenu(); + t.focus(); + }); m.add({ title : t.settings.title, @@ -8197,6 +9996,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { if (o.value === undefined) { m.add({ title : o.title, + role : "option", 'class' : 'mceMenuItemTitle', onclick : function() { if (t.settings.onselect('') !== false) @@ -8205,6 +10005,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { }); } else { o.id = DOM.uniqueId(); + o.role= "option"; o.onclick = function() { if (t.settings.onselect(o.value) !== false) t.select(o.value); // Must be runned after @@ -8222,40 +10023,39 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { var t = this, cp = t.classPrefix; Event.add(t.id, 'click', t.showMenu, t); - Event.add(t.id + '_text', 'focus', function() { + Event.add(t.id, 'keydown', function(evt) { + if (evt.keyCode == 32) { // Space + t.showMenu(evt); + Event.cancel(evt); + } + }); + Event.add(t.id, 'focus', function() { if (!t._focused) { - t.keyDownHandler = Event.add(t.id + '_text', 'keydown', function(e) { - var idx = -1, v, kc = e.keyCode; - - // Find current index - each(t.items, function(v, i) { - if (t.selectedValue == v.value) - idx = i; - }); - - // Move up/down - if (kc == 38) - v = t.items[idx - 1]; - else if (kc == 40) - v = t.items[idx + 1]; - else if (kc == 13) { + t.keyDownHandler = Event.add(t.id, 'keydown', function(e) { + if (e.keyCode == 40) { + t.showMenu(); + Event.cancel(e); + } + }); + t.keyPressHandler = Event.add(t.id, 'keypress', function(e) { + var v; + if (e.keyCode == 13) { // Fake select on enter v = t.selectedValue; t.selectedValue = null; // Needs to be null to fake change + Event.cancel(e); t.settings.onselect(v); - return Event.cancel(e); - } - - if (v) { - t.hideMenu(); - t.select(v.value); } }); } t._focused = 1; }); - Event.add(t.id + '_text', 'blur', function() {Event.remove(t.id + '_text', 'keydown', t.keyDownHandler); t._focused = 0;}); + Event.add(t.id, 'blur', function() { + Event.remove(t.id, 'keydown', t.keyDownHandler); + Event.remove(t.id, 'keypress', t.keyPressHandler); + t._focused = 0; + }); // Old IE doesn't have hover on all elements if (tinymce.isIE6 || !DOM.boxModel) { @@ -8281,6 +10081,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { } }); })(tinymce); + (function(tinymce) { var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher; @@ -8292,6 +10093,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { setDisabled : function(s) { DOM.get(this.id).disabled = s; + this.setAriaProperty('disabled', s); }, isDisabled : function() { @@ -8366,13 +10168,13 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { h += DOM.createHTML('option', {value : it.value}, it.title); }); - h = DOM.createHTML('select', {id : t.id, 'class' : 'mceNativeListBox'}, h); - + h = DOM.createHTML('select', {id : t.id, 'class' : 'mceNativeListBox', 'aria-labelledby': t.id + '_aria'}, h); + h += DOM.createHTML('span', {id : t.id + '_aria', 'style': 'display: none'}, t.settings.title); return h; }, postRender : function() { - var t = this, ch; + var t = this, ch, changeListenerAdded = true; t.rendered = true; @@ -8394,12 +10196,20 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { var bf; Event.remove(t.id, 'change', ch); + changeListenerAdded = false; bf = Event.add(t.id, 'blur', function() { + if (changeListenerAdded) return; + changeListenerAdded = true; Event.add(t.id, 'change', onChange); Event.remove(t.id, 'blur', bf); }); + //prevent default left and right keys on chrome - so that the keyboard navigation is used. + if (tinymce.isWebKit && (e.keyCode==37 ||e.keyCode==39)) { + return Event.prevent(e); + } + if (e.keyCode == 13 || e.keyCode == 32) { onChange(e); return Event.cancel(e); @@ -8410,12 +10220,13 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { } }); })(tinymce); + (function(tinymce) { var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each; tinymce.create('tinymce.ui.MenuButton:tinymce.ui.Button', { - MenuButton : function(id, s) { - this.parent(id, s); + MenuButton : function(id, s, ed) { + this.parent(id, s, ed); this.onRenderMenu = new tinymce.util.Dispatcher(this); @@ -8462,7 +10273,10 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { icons : t.settings.icons }); - m.onHideMenu.add(t.hideMenu, t); + m.onHideMenu.add(function() { + t.hideMenu(); + t.focus(); + }); t.onRenderMenu.dispatch(t, m); t.menu = m; @@ -8504,8 +10318,8 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each; tinymce.create('tinymce.ui.SplitButton:tinymce.ui.MenuButton', { - SplitButton : function(id, s) { - this.parent(id, s); + SplitButton : function(id, s, ed) { + this.parent(id, s, ed); this.classPrefix = 'mceSplitButton'; }, @@ -8515,33 +10329,50 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { h = '
    ' + DOM.createHTML('a', {id : t.id + '_action', href : 'javascript:;', 'class' : 'mceAction ' + s['class'], onclick : "return false;", onmousedown : 'return false;', title : s.title}, h1) + '' + DOM.createHTML('a', {role: 'button', id : t.id + '_action', tabindex: '-1', href : 'javascript:;', 'class' : 'mceAction ' + s['class'], onclick : "return false;", onmousedown : 'return false;', title : s.title}, h1) + '' + DOM.createHTML('a', {id : t.id + '_open', href : 'javascript:;', 'class' : 'mceOpen ' + s['class'], onclick : "return false;", onmousedown : 'return false;', title : s.title}, h1) + '' + DOM.createHTML('a', {role: 'button', id : t.id + '_open', tabindex: '-1', href : 'javascript:;', 'class' : 'mceOpen ' + s['class'], onclick : "return false;", onmousedown : 'return false;', title : s.title}, h1) + '