From b4772a41d1e81d4a95ab032eb38308ecf5c40acf Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Sat, 2 Apr 2016 16:10:53 +1100 Subject: [PATCH 1/5] MDL-51603 dataformat: Imported 3rd party lib spout --- lib/spout/LICENSE | 166 ++++++++ lib/spout/README.md | 338 ++++++++++++++++ lib/spout/readme_moodle.txt | 13 + .../src/Spout/Autoloader/Psr4Autoloader.php | 150 +++++++ lib/spout/src/Spout/Autoloader/autoload.php | 15 + lib/spout/src/Spout/Common/Escaper/CSV.php | 38 ++ .../Spout/Common/Escaper/EscaperInterface.php | 27 ++ lib/spout/src/Spout/Common/Escaper/ODS.php | 34 ++ lib/spout/src/Spout/Common/Escaper/XLSX.php | 143 +++++++ .../Exception/EncodingConversionException.php | 13 + .../Spout/Common/Exception/IOException.php | 13 + .../Exception/InvalidArgumentException.php | 13 + .../Spout/Common/Exception/SpoutException.php | 13 + .../Exception/UnsupportedTypeException.php | 13 + .../Spout/Common/Helper/EncodingHelper.php | 175 +++++++++ .../Spout/Common/Helper/FileSystemHelper.php | 132 +++++++ .../Common/Helper/GlobalFunctionsHelper.php | 305 ++++++++++++++ .../src/Spout/Common/Helper/StringHelper.php | 71 ++++ lib/spout/src/Spout/Common/Type.php | 16 + lib/spout/src/Spout/Reader/AbstractReader.php | 202 ++++++++++ lib/spout/src/Spout/Reader/CSV/Reader.php | 152 +++++++ .../src/Spout/Reader/CSV/RowIterator.php | 236 +++++++++++ lib/spout/src/Spout/Reader/CSV/Sheet.php | 37 ++ .../src/Spout/Reader/CSV/SheetIterator.php | 97 +++++ .../IteratorNotRewindableException.php | 13 + .../Exception/NoSheetsFoundException.php | 13 + .../Reader/Exception/ReaderException.php | 15 + .../Exception/ReaderNotOpenedException.php | 13 + .../SharedStringNotFoundException.php | 13 + .../Exception/XMLProcessingException.php | 12 + .../src/Spout/Reader/IteratorInterface.php | 18 + .../Reader/ODS/Helper/CellValueFormatter.php | 197 ++++++++++ lib/spout/src/Spout/Reader/ODS/Reader.php | 72 ++++ .../src/Spout/Reader/ODS/RowIterator.php | 229 +++++++++++ lib/spout/src/Spout/Reader/ODS/Sheet.php | 66 ++++ .../src/Spout/Reader/ODS/SheetIterator.php | 135 +++++++ lib/spout/src/Spout/Reader/ReaderFactory.php | 48 +++ .../src/Spout/Reader/ReaderInterface.php | 36 ++ lib/spout/src/Spout/Reader/SheetInterface.php | 18 + .../Spout/Reader/Wrapper/SimpleXMLElement.php | 177 +++++++++ .../Wrapper/XMLInternalErrorsHelper.php | 82 ++++ .../src/Spout/Reader/Wrapper/XMLReader.php | 184 +++++++++ .../Spout/Reader/XLSX/Helper/CellHelper.php | 103 +++++ .../Reader/XLSX/Helper/CellValueFormatter.php | 241 ++++++++++++ .../CachingStrategyFactory.php | 154 ++++++++ .../CachingStrategyInterface.php | 44 +++ .../FileBasedStrategy.php | 193 +++++++++ .../SharedStringsCaching/InMemoryStrategy.php | 83 ++++ .../XLSX/Helper/SharedStringsHelper.php | 248 ++++++++++++ .../Spout/Reader/XLSX/Helper/SheetHelper.php | 180 +++++++++ .../Spout/Reader/XLSX/Helper/StyleHelper.php | 226 +++++++++++ lib/spout/src/Spout/Reader/XLSX/Reader.php | 103 +++++ .../src/Spout/Reader/XLSX/RowIterator.php | 227 +++++++++++ lib/spout/src/Spout/Reader/XLSX/Sheet.php | 64 +++ .../src/Spout/Reader/XLSX/SheetIterator.php | 113 ++++++ .../Writer/AbstractMultiSheetsWriter.php | 119 ++++++ lib/spout/src/Spout/Writer/AbstractWriter.php | 330 ++++++++++++++++ lib/spout/src/Spout/Writer/CSV/Writer.php | 101 +++++ .../Common/Helper/AbstractStyleHelper.php | 138 +++++++ .../Spout/Writer/Common/Helper/CellHelper.php | 82 ++++ .../Spout/Writer/Common/Helper/ZipHelper.php | 217 ++++++++++ .../Common/Internal/AbstractWorkbook.php | 188 +++++++++ .../Common/Internal/WorkbookInterface.php | 74 ++++ .../Common/Internal/WorksheetInterface.php | 40 ++ lib/spout/src/Spout/Writer/Common/Sheet.php | 159 ++++++++ .../Exception/InvalidColorException.php | 13 + .../Exception/InvalidSheetNameException.php | 13 + .../Exception/SheetNotFoundException.php | 13 + .../WriterAlreadyOpenedException.php | 13 + .../Writer/Exception/WriterException.php | 15 + .../Exception/WriterNotOpenedException.php | 13 + .../Writer/ODS/Helper/FileSystemHelper.php | 279 +++++++++++++ .../Spout/Writer/ODS/Helper/StyleHelper.php | 264 +++++++++++++ .../Spout/Writer/ODS/Internal/Workbook.php | 119 ++++++ .../Spout/Writer/ODS/Internal/Worksheet.php | 223 +++++++++++ lib/spout/src/Spout/Writer/ODS/Writer.php | 93 +++++ lib/spout/src/Spout/Writer/Style/Color.php | 87 ++++ lib/spout/src/Spout/Writer/Style/Style.php | 308 +++++++++++++++ .../src/Spout/Writer/Style/StyleBuilder.php | 134 +++++++ lib/spout/src/Spout/Writer/WriterFactory.php | 48 +++ .../src/Spout/Writer/WriterInterface.php | 91 +++++ .../Writer/XLSX/Helper/FileSystemHelper.php | 371 ++++++++++++++++++ .../XLSX/Helper/SharedStringsHelper.php | 103 +++++ .../Spout/Writer/XLSX/Helper/StyleHelper.php | 175 +++++++++ .../Spout/Writer/XLSX/Internal/Workbook.php | 135 +++++++ .../Spout/Writer/XLSX/Internal/Worksheet.php | 185 +++++++++ lib/spout/src/Spout/Writer/XLSX/Writer.php | 132 +++++++ 87 files changed, 10007 insertions(+) create mode 100644 lib/spout/LICENSE create mode 100644 lib/spout/README.md create mode 100755 lib/spout/readme_moodle.txt create mode 100644 lib/spout/src/Spout/Autoloader/Psr4Autoloader.php create mode 100644 lib/spout/src/Spout/Autoloader/autoload.php create mode 100644 lib/spout/src/Spout/Common/Escaper/CSV.php create mode 100644 lib/spout/src/Spout/Common/Escaper/EscaperInterface.php create mode 100644 lib/spout/src/Spout/Common/Escaper/ODS.php create mode 100644 lib/spout/src/Spout/Common/Escaper/XLSX.php create mode 100644 lib/spout/src/Spout/Common/Exception/EncodingConversionException.php create mode 100644 lib/spout/src/Spout/Common/Exception/IOException.php create mode 100644 lib/spout/src/Spout/Common/Exception/InvalidArgumentException.php create mode 100644 lib/spout/src/Spout/Common/Exception/SpoutException.php create mode 100644 lib/spout/src/Spout/Common/Exception/UnsupportedTypeException.php create mode 100644 lib/spout/src/Spout/Common/Helper/EncodingHelper.php create mode 100644 lib/spout/src/Spout/Common/Helper/FileSystemHelper.php create mode 100644 lib/spout/src/Spout/Common/Helper/GlobalFunctionsHelper.php create mode 100644 lib/spout/src/Spout/Common/Helper/StringHelper.php create mode 100644 lib/spout/src/Spout/Common/Type.php create mode 100644 lib/spout/src/Spout/Reader/AbstractReader.php create mode 100644 lib/spout/src/Spout/Reader/CSV/Reader.php create mode 100644 lib/spout/src/Spout/Reader/CSV/RowIterator.php create mode 100644 lib/spout/src/Spout/Reader/CSV/Sheet.php create mode 100644 lib/spout/src/Spout/Reader/CSV/SheetIterator.php create mode 100644 lib/spout/src/Spout/Reader/Exception/IteratorNotRewindableException.php create mode 100644 lib/spout/src/Spout/Reader/Exception/NoSheetsFoundException.php create mode 100644 lib/spout/src/Spout/Reader/Exception/ReaderException.php create mode 100644 lib/spout/src/Spout/Reader/Exception/ReaderNotOpenedException.php create mode 100644 lib/spout/src/Spout/Reader/Exception/SharedStringNotFoundException.php create mode 100644 lib/spout/src/Spout/Reader/Exception/XMLProcessingException.php create mode 100644 lib/spout/src/Spout/Reader/IteratorInterface.php create mode 100644 lib/spout/src/Spout/Reader/ODS/Helper/CellValueFormatter.php create mode 100644 lib/spout/src/Spout/Reader/ODS/Reader.php create mode 100644 lib/spout/src/Spout/Reader/ODS/RowIterator.php create mode 100644 lib/spout/src/Spout/Reader/ODS/Sheet.php create mode 100644 lib/spout/src/Spout/Reader/ODS/SheetIterator.php create mode 100644 lib/spout/src/Spout/Reader/ReaderFactory.php create mode 100644 lib/spout/src/Spout/Reader/ReaderInterface.php create mode 100644 lib/spout/src/Spout/Reader/SheetInterface.php create mode 100644 lib/spout/src/Spout/Reader/Wrapper/SimpleXMLElement.php create mode 100644 lib/spout/src/Spout/Reader/Wrapper/XMLInternalErrorsHelper.php create mode 100644 lib/spout/src/Spout/Reader/Wrapper/XMLReader.php create mode 100644 lib/spout/src/Spout/Reader/XLSX/Helper/CellHelper.php create mode 100644 lib/spout/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php create mode 100644 lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyFactory.php create mode 100644 lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyInterface.php create mode 100644 lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/FileBasedStrategy.php create mode 100644 lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/InMemoryStrategy.php create mode 100644 lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php create mode 100644 lib/spout/src/Spout/Reader/XLSX/Helper/SheetHelper.php create mode 100644 lib/spout/src/Spout/Reader/XLSX/Helper/StyleHelper.php create mode 100644 lib/spout/src/Spout/Reader/XLSX/Reader.php create mode 100644 lib/spout/src/Spout/Reader/XLSX/RowIterator.php create mode 100644 lib/spout/src/Spout/Reader/XLSX/Sheet.php create mode 100644 lib/spout/src/Spout/Reader/XLSX/SheetIterator.php create mode 100644 lib/spout/src/Spout/Writer/AbstractMultiSheetsWriter.php create mode 100644 lib/spout/src/Spout/Writer/AbstractWriter.php create mode 100644 lib/spout/src/Spout/Writer/CSV/Writer.php create mode 100644 lib/spout/src/Spout/Writer/Common/Helper/AbstractStyleHelper.php create mode 100644 lib/spout/src/Spout/Writer/Common/Helper/CellHelper.php create mode 100644 lib/spout/src/Spout/Writer/Common/Helper/ZipHelper.php create mode 100644 lib/spout/src/Spout/Writer/Common/Internal/AbstractWorkbook.php create mode 100644 lib/spout/src/Spout/Writer/Common/Internal/WorkbookInterface.php create mode 100644 lib/spout/src/Spout/Writer/Common/Internal/WorksheetInterface.php create mode 100644 lib/spout/src/Spout/Writer/Common/Sheet.php create mode 100644 lib/spout/src/Spout/Writer/Exception/InvalidColorException.php create mode 100644 lib/spout/src/Spout/Writer/Exception/InvalidSheetNameException.php create mode 100644 lib/spout/src/Spout/Writer/Exception/SheetNotFoundException.php create mode 100644 lib/spout/src/Spout/Writer/Exception/WriterAlreadyOpenedException.php create mode 100644 lib/spout/src/Spout/Writer/Exception/WriterException.php create mode 100644 lib/spout/src/Spout/Writer/Exception/WriterNotOpenedException.php create mode 100644 lib/spout/src/Spout/Writer/ODS/Helper/FileSystemHelper.php create mode 100644 lib/spout/src/Spout/Writer/ODS/Helper/StyleHelper.php create mode 100644 lib/spout/src/Spout/Writer/ODS/Internal/Workbook.php create mode 100644 lib/spout/src/Spout/Writer/ODS/Internal/Worksheet.php create mode 100644 lib/spout/src/Spout/Writer/ODS/Writer.php create mode 100644 lib/spout/src/Spout/Writer/Style/Color.php create mode 100644 lib/spout/src/Spout/Writer/Style/Style.php create mode 100644 lib/spout/src/Spout/Writer/Style/StyleBuilder.php create mode 100644 lib/spout/src/Spout/Writer/WriterFactory.php create mode 100644 lib/spout/src/Spout/Writer/WriterInterface.php create mode 100644 lib/spout/src/Spout/Writer/XLSX/Helper/FileSystemHelper.php create mode 100644 lib/spout/src/Spout/Writer/XLSX/Helper/SharedStringsHelper.php create mode 100644 lib/spout/src/Spout/Writer/XLSX/Helper/StyleHelper.php create mode 100644 lib/spout/src/Spout/Writer/XLSX/Internal/Workbook.php create mode 100644 lib/spout/src/Spout/Writer/XLSX/Internal/Worksheet.php create mode 100644 lib/spout/src/Spout/Writer/XLSX/Writer.php diff --git a/lib/spout/LICENSE b/lib/spout/LICENSE new file mode 100644 index 00000000000..167ec4d66df --- /dev/null +++ b/lib/spout/LICENSE @@ -0,0 +1,166 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/lib/spout/README.md b/lib/spout/README.md new file mode 100644 index 00000000000..6bb73aab19c --- /dev/null +++ b/lib/spout/README.md @@ -0,0 +1,338 @@ +# Spout + +[![Latest Stable Version](https://poser.pugx.org/box/spout/v/stable)](https://packagist.org/packages/box/spout) +[![Project Status](http://opensource.box.com/badges/active.svg)](http://opensource.box.com/badges) +[![Build Status](https://travis-ci.org/box/spout.svg?branch=master)](https://travis-ci.org/box/spout) +[![Code Coverage](https://scrutinizer-ci.com/g/box/spout/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/box/spout/?branch=master) +[![Total Downloads](https://poser.pugx.org/box/spout/downloads)](https://packagist.org/packages/box/spout) +[![License](https://poser.pugx.org/box/spout/license)](https://packagist.org/packages/box/spout) + +Spout is a PHP library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way. +Contrary to other file readers or writers, it is capable of processing very large files while keeping the memory usage really low (less than 10MB). + +Join the community and come discuss about Spout: [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) + +## Installation + +### Composer (recommended) + +Spout can be installed directly from [Composer](https://getcomposer.org/). + +Run the following command: +``` +$ composer require box/spout +``` + +### Manual installation + +If you can't use Composer, no worries! You can still install Spout manually. + +> Before starting, make sure your system meets the [requirements](#requirements). + +1. Download the source code from the [Releases page](https://github.com/box/spout/releases) +2. Extract the downloaded content into your project. +3. Add this code to the top controller (index.php) or wherever it may be more appropriate: +```php +require_once '[PATH/TO]/src/Spout/Autoloader/autoload.php'; // don't forget to change the path! +``` + + +## Requirements + +* PHP version 5.4.0 or higher +* PHP extension `php_zip` enabled +* PHP extension `php_xmlreader` enabled +* PHP extension `php_simplexml` enabled + + +## Basic usage + +### Reader + +Regardless of the file type, the interface to read a file is always the same: + +```php +use Box\Spout\Reader\ReaderFactory; +use Box\Spout\Common\Type; + +$reader = ReaderFactory::create(Type::XLSX); // for XLSX files +//$reader = ReaderFactory::create(Type::CSV); // for CSV files +//$reader = ReaderFactory::create(Type::ODS); // for ODS files + +$reader->open($filePath); + +foreach ($reader->getSheetIterator() as $sheet) { + foreach ($sheet->getRowIterator() as $row) { + // do stuff with the row + } +} + +$reader->close(); +``` + +If there are multiple sheets in the file, the reader will read all of them sequentially. + +### Writer + +As with the reader, there is one common interface to write data to a file: + +```php +use Box\Spout\Writer\WriterFactory; +use Box\Spout\Common\Type; + +$writer = WriterFactory::create(Type::XLSX); // for XLSX files +//$writer = WriterFactory::create(Type::CSV); // for CSV files +//$writer = WriterFactory::create(Type::ODS); // for ODS files + +$writer->openToFile($filePath); // write data to a file or to a PHP stream +//$writer->openToBrowser($fileName); // stream data directly to the browser + +$writer->addRow($singleRow); // add a row at a time +$writer->addRows($multipleRows); // add multiple rows at a time + +$writer->close(); +``` + +For XLSX and ODS files, the number of rows per sheet is limited to 1,048,576. By default, once this limit is reached, the writer will automatically create a new sheet and continue writing data into it. + + +## Advanced usage + +If you are looking for how to perform some common, more advanced tasks with Spout, please take a look at the [Wiki](https://github.com/box/spout/wiki). It contains code snippets, ready to be used. + +### Configuring the CSV reader and writer + +It is possible to configure both the CSV reader and writer to specify the field separator as well as the field enclosure: +```php +use Box\Spout\Reader\ReaderFactory; +use Box\Spout\Common\Type; + +$reader = ReaderFactory::create(Type::CSV); +$reader->setFieldDelimiter('|'); +$reader->setFieldEnclosure('@'); +$reader->setEndOfLineCharacter("\r"); +``` + +Additionally, if you need to read non UTF-8 files, you can specify the encoding of your file this way: +```php +$reader->setEncoding('UTF-16LE'); +``` + +The writer always generate CSV files encoded in UTF-8, with a BOM. + + +### Configuring the XLSX and ODS writers + +#### Row styling + +It is possible to apply some formatting options to a row. Spout supports fonts as well as alignment styles. + +```php +use Box\Spout\Common\Type; +use Box\Spout\Writer\WriterFactory; +use Box\Spout\Writer\Style\StyleBuilder; +use Box\Spout\Writer\Style\Color; + +$style = (new StyleBuilder()) + ->setFontBold() + ->setFontSize(15) + ->setFontColor(Color::BLUE) + ->setShouldWrapText() + ->build(); + +$writer = WriterFactory::create(Type::XLSX); +$writer->openToFile($filePath); + +$writer->addRowWithStyle($singleRow, $style); // style will only be applied to this row +$writer->addRow($otherSingleRow); // no style will be applied +$writer->addRowsWithStyle($multipleRows, $style); // style will be applied to all given rows + +$writer->close(); +``` + +Unfortunately, Spout does not support all the possible formatting options yet. But you can find the most important ones: + +Category | Property | API +----------|---------------|--------------------------------------- +Font | Bold | `StyleBuilder::setFontBold()` + | Italic | `StyleBuilder::setFontItalic()` + | Underline | `StyleBuilder::setFontUnderline()` + | Strikethrough | `StyleBuilder::setFontStrikethrough()` + | Font name | `StyleBuilder::setFontName('Arial')` + | Font size | `StyleBuilder::setFontSize(14)` + | Font color | `StyleBuilder::setFontColor(Color::BLUE)`
`StyleBuilder::setFontColor(Color::rgb(0, 128, 255))` +Alignment | Wrap text | `StyleBuilder::setShouldWrapText()` + + +#### New sheet creation + +It is also possible to change the behavior of the writer when the maximum number of rows (1,048,576) have been written in the current sheet: +```php +use Box\Spout\Writer\WriterFactory; +use Box\Spout\Common\Type; + +$writer = WriterFactory::create(Type::ODS); +$writer->setShouldCreateNewSheetsAutomatically(true); // default value +$writer->setShouldCreateNewSheetsAutomatically(false); // will stop writing new data when limit is reached +``` + +#### Using custom temporary folder + +Processing XLSX and ODS files require temporary files to be created. By default, Spout will use the system default temporary folder (as returned by `sys_get_temp_dir()`). It is possible to override this by explicitly setting it on the reader or writer: +```php +use Box\Spout\Writer\WriterFactory; +use Box\Spout\Common\Type; + +$writer = WriterFactory::create(Type::XLSX); +$writer->setTempFolder($customTempFolderPath); +``` + +#### Strings storage (XLSX writer) + +XLSX files support different ways to store the string values: +* Shared strings are meant to optimize file size by separating strings from the sheet representation and ignoring strings duplicates (if a string is used three times, only one string will be stored) +* Inline strings are less optimized (as duplicate strings are all stored) but is faster to process + +In order to keep the memory usage really low, Spout does not optimize strings when using shared strings. It is nevertheless possible to use this mode. +```php +use Box\Spout\Writer\WriterFactory; +use Box\Spout\Common\Type; + +$writer = WriterFactory::create(Type::XLSX); +$writer->setShouldUseInlineStrings(true); // default (and recommended) value +$writer->setShouldUseInlineStrings(false); // will use shared strings +``` + +> ##### Note on Apple Numbers and iOS support +> +> Apple's products (Numbers and the iOS previewer) don't support inline strings and display empty cells instead. Therefore, if these platforms need to be supported, make sure to use shared strings! + + +### Playing with sheets + +When creating a XLSX or ODS file, it is possible to control which sheet the data will be written into. At any time, you can retrieve or set the current sheet: +```php +$firstSheet = $writer->getCurrentSheet(); +$writer->addRow($rowForSheet1); // writes the row to the first sheet + +$newSheet = $writer->addNewSheetAndMakeItCurrent(); +$writer->addRow($rowForSheet2); // writes the row to the new sheet + +$writer->setCurrentSheet($firstSheet); +$writer->addRow($anotherRowForSheet1); // append the row to the first sheet +``` + +It is also possible to retrieve all the sheets currently created: +```php +$sheets = $writer->getSheets(); +``` + +If you rely on the sheet's name in your application, you can access it and customize it this way: +```php +// Accessing the sheet name when reading +foreach ($reader->getSheetIterator() as $sheet) { + $sheetName = $sheet->getName(); +} + +// Accessing the sheet name when writing +$sheet = $writer->getCurrentSheet(); +$sheetName = $sheet->getName(); + +// Customizing the sheet name when writing +$sheet = $writer->getCurrentSheet(); +$sheet->setName('My custom name'); +``` + +> Please note that Excel has some restrictions on the sheet's name: +> * it must not be blank +> * it must not exceed 31 characters +> * it must not contain these characters: \ / ? * : [ or ] +> * it must not start or end with a single quote +> * it must be unique +> +> Handling these restrictions is the developer's responsibility. Spout does not try to automatically change the sheet's name, as one may rely on this name to be exactly what was passed in. + + +### Fluent interface + +Because fluent interfaces are great, you can use them with Spout: +```php +use Box\Spout\Writer\WriterFactory; +use Box\Spout\Common\Type; + +$writer = WriterFactory::create(Type::XLSX); +$writer->setTempFolder($customTempFolderPath) + ->setShouldUseInlineStrings(true) + ->openToFile($filePath) + ->addRow($headerRow) + ->addRows($dataRows) + ->close(); +``` + + +## Running tests + +On the `master` branch, only unit and functional tests are included. The performance tests require very large files and have been excluded. +If you just want to check that everything is working as expected, executing the tests of the `master` branch is enough. + +If you want to run performance tests, you will need to checkout the `perf-tests` branch. Multiple test suites can then be run, depending on the expected output: + +* `phpunit` - runs the whole test suite (unit + functional + performance tests) +* `phpunit --exclude-group perf-tests` - only runs the unit and functional tests +* `phpunit --group perf-tests` - only runs the performance tests + +For information, the performance tests take about 30 minutes to run (processing 1 million rows files is not a quick thing). + +> Performance tests status: [![Build Status](https://travis-ci.org/box/spout.svg?branch=perf-tests)](https://travis-ci.org/box/spout) + + +## Frequently Asked Questions + +#### How can Spout handle such large data sets and still use less than 10MB of memory? + +When writing data, Spout is streaming the data to files, one or few lines at a time. That means that it only keeps in memory the few rows that it needs to write. Once written, the memory is freed. + +Same goes with reading. Only one row at a time is stored in memory. A special technique is used to handle shared strings in XLSX, storing them - if needed - into several small temporary files that allows fast access. + +#### How long does it take to generate a file with X rows? + +Here are a few numbers regarding the performance of Spout: + +| Type | Action | 2,000 rows (6,000 cells) | 200,000 rows (600,000 cells) | 2,000,000 rows (6,000,000 cells) | +|------|-------------------------------|--------------------------|------------------------------|----------------------------------| +| CSV | Read | < 1 second | 4 seconds | 2-3 minutes | +| | Write | < 1 second | 2 seconds | 2-3 minutes | +| XLSX | Read
*inline strings* | < 1 second | 35-40 seconds | 18-20 minutes | +| | Read
*shared strings* | 1 second | 1-2 minutes | 35-40 minutes | +| | Write | 1 second | 20-25 seconds | 8-10 minutes | +| ODS | Read | 1 second | 1-2 minutes | 5-6 minutes | +| | Write | < 1 second | 35-40 seconds | 5-6 minutes | + +#### Does Spout support charts or formulas? + +No. This is a compromise to keep memory usage low. Charts and formulas requires data to be kept in memory in order to be used. +So the larger the file would be, the more memory would be consumed, preventing your code to scale well. + + +## Support + +Need to contact us directly? Email oss@box.com and be sure to include the name of this project in the subject. + +You can also ask questions, submit new features ideas or discuss about Spout in the chat room:
+[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) + +## Copyright and License + +Copyright 2015 Box, Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/lib/spout/readme_moodle.txt b/lib/spout/readme_moodle.txt new file mode 100755 index 00000000000..0df51eb7cab --- /dev/null +++ b/lib/spout/readme_moodle.txt @@ -0,0 +1,13 @@ +#!/bin/sh +# +# Description of import of Horde libraries +# + +wget https://codeload.github.com/box/spout/zip/v2.4.3 +unzip v2.4.3 +rm v2.4.3 +rm spout-2.4.3/composer.json +rm -rf src +mv -f spout-2.4.3/* . +rm -r spout-2.4.3/ + diff --git a/lib/spout/src/Spout/Autoloader/Psr4Autoloader.php b/lib/spout/src/Spout/Autoloader/Psr4Autoloader.php new file mode 100644 index 00000000000..67cccc376f0 --- /dev/null +++ b/lib/spout/src/Spout/Autoloader/Psr4Autoloader.php @@ -0,0 +1,150 @@ +prefixes[$prefix]) === false) { + $this->prefixes[$prefix] = array(); + } + + // retain the base directory for the namespace prefix + if ($prepend) { + array_unshift($this->prefixes[$prefix], $baseDir); + } else { + array_push($this->prefixes[$prefix], $baseDir); + } + } + + /** + * Loads the class file for a given class name. + * + * @param string $class The fully-qualified class name. + * @return mixed The mapped file name on success, or boolean false on + * failure. + */ + public function loadClass($class) + { + // the current namespace prefix + $prefix = $class; + + // work backwards through the namespace names of the fully-qualified + // class name to find a mapped file name + while (false !== $pos = strrpos($prefix, '\\')) { + + // retain the trailing namespace separator in the prefix + $prefix = substr($class, 0, $pos + 1); + + // the rest is the relative class name + $relativeClass = substr($class, $pos + 1); + + // try to load a mapped file for the prefix and relative class + $mappedFile = $this->loadMappedFile($prefix, $relativeClass); + if ($mappedFile !== false) { + return $mappedFile; + } + + // remove the trailing namespace separator for the next iteration + // of strrpos() + $prefix = rtrim($prefix, '\\'); + } + + // never found a mapped file + return false; + } + + /** + * Load the mapped file for a namespace prefix and relative class. + * + * @param string $prefix The namespace prefix. + * @param string $relativeClass The relative class name. + * @return mixed Boolean false if no mapped file can be loaded, or the + * name of the mapped file that was loaded. + */ + protected function loadMappedFile($prefix, $relativeClass) + { + // are there any base directories for this namespace prefix? + if (isset($this->prefixes[$prefix]) === false) { + return false; + } + + // look through base directories for this namespace prefix + foreach ($this->prefixes[$prefix] as $baseDir) { + + // replace the namespace prefix with the base directory, + // replace namespace separators with directory separators + // in the relative class name, append with .php + $file = $baseDir + . str_replace('\\', '/', $relativeClass) + . '.php'; + + // if the mapped file exists, require it + if ($this->requireFile($file)) { + // yes, we're done + return $file; + } + } + + // never found it + return false; + } + + /** + * If a file exists, require it from the file system. + * + * @param string $file The file to require. + * @return bool True if the file exists, false if not. + */ + protected function requireFile($file) + { + if (file_exists($file)) { + require $file; + return true; + } + return false; + } +} diff --git a/lib/spout/src/Spout/Autoloader/autoload.php b/lib/spout/src/Spout/Autoloader/autoload.php new file mode 100644 index 00000000000..73ee5190394 --- /dev/null +++ b/lib/spout/src/Spout/Autoloader/autoload.php @@ -0,0 +1,15 @@ +register(); +$loader->addNamespace('Box\Spout', $srcBaseDirectory); diff --git a/lib/spout/src/Spout/Common/Escaper/CSV.php b/lib/spout/src/Spout/Common/Escaper/CSV.php new file mode 100644 index 00000000000..4bc2d1af619 --- /dev/null +++ b/lib/spout/src/Spout/Common/Escaper/CSV.php @@ -0,0 +1,38 @@ +controlCharactersEscapingMap = $this->getControlCharactersEscapingMap(); + } + + /** + * Escapes the given string to make it compatible with XLSX + * + * @param string $string The string to escape + * @return string The escaped string + */ + public function escape($string) + { + $escapedString = $this->escapeControlCharacters($string); + $escapedString = htmlspecialchars($escapedString, ENT_QUOTES); + + return $escapedString; + } + + /** + * Unescapes the given string to make it compatible with XLSX + * + * @param string $string The string to unescape + * @return string The unescaped string + */ + public function unescape($string) + { + $unescapedString = htmlspecialchars_decode($string, ENT_QUOTES); + $unescapedString = $this->unescapeControlCharacters($unescapedString); + + return $unescapedString; + } + + /** + * Builds the map containing control characters to be escaped + * mapped to their escaped values. + * "\t", "\r" and "\n" don't need to be escaped. + * + * NOTE: the logic has been adapted from the XlsxWriter library (BSD License) + * @link https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89 + * + * @return string[] + */ + protected function getControlCharactersEscapingMap() + { + $controlCharactersEscapingMap = []; + $whitelistedControlCharacters = ["\t", "\r", "\n"]; + + // control characters values are from 0 to 1F (hex values) in the ASCII table + for ($charValue = 0x0; $charValue <= 0x1F; $charValue++) { + if (!in_array(chr($charValue), $whitelistedControlCharacters)) { + $charHexValue = dechex($charValue); + $escapedChar = '_x' . sprintf('%04s' , strtoupper($charHexValue)) . '_'; + $controlCharactersEscapingMap[$escapedChar] = chr($charValue); + } + } + + return $controlCharactersEscapingMap; + } + + /** + * Converts PHP control characters from the given string to OpenXML escaped control characters + * + * Excel escapes control characters with _xHHHH_ and also escapes any + * literal strings of that type by encoding the leading underscore. + * So "\0" -> _x0000_ and "_x0000_" -> _x005F_x0000_. + * + * NOTE: the logic has been adapted from the XlsxWriter library (BSD License) + * @link https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89 + * + * @param string $string String to escape + * @return string + */ + protected function escapeControlCharacters($string) + { + $escapedString = $this->escapeEscapeCharacter($string); + return str_replace(array_values($this->controlCharactersEscapingMap), array_keys($this->controlCharactersEscapingMap), $escapedString); + } + + /** + * Escapes the escape character: "_x0000_" -> "_x005F_x0000_" + * + * @param string $string String to escape + * @return string The escaped string + */ + protected function escapeEscapeCharacter($string) + { + return preg_replace('/_(x[\dA-F]{4})_/', '_x005F_$1_', $string); + } + + /** + * Converts OpenXML escaped control characters from the given string to PHP control characters + * + * Excel escapes control characters with _xHHHH_ and also escapes any + * literal strings of that type by encoding the leading underscore. + * So "_x0000_" -> "\0" and "_x005F_x0000_" -> "_x0000_" + * + * NOTE: the logic has been adapted from the XlsxWriter library (BSD License) + * @link https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89 + * + * @param string $string String to unescape + * @return string + */ + protected function unescapeControlCharacters($string) + { + $unescapedString = $string; + foreach ($this->controlCharactersEscapingMap as $escapedCharValue => $charValue) { + // only unescape characters that don't contain the escaped escape character for now + $unescapedString = preg_replace("/(?unescapeEscapeCharacter($unescapedString); + } + + /** + * Unecapes the escape character: "_x005F_x0000_" => "_x0000_" + * + * @param string $string String to unescape + * @return string The unescaped string + */ + protected function unescapeEscapeCharacter($string) + { + return preg_replace('/_x005F(_x[\dA-F]{4}_)/', '$1', $string); + } +} diff --git a/lib/spout/src/Spout/Common/Exception/EncodingConversionException.php b/lib/spout/src/Spout/Common/Exception/EncodingConversionException.php new file mode 100644 index 00000000000..6dcc0a8d6ae --- /dev/null +++ b/lib/spout/src/Spout/Common/Exception/EncodingConversionException.php @@ -0,0 +1,13 @@ +globalFunctionsHelper = $globalFunctionsHelper; + + $this->supportedEncodingsWithBom = [ + self::ENCODING_UTF8 => self::BOM_UTF8, + self::ENCODING_UTF16_LE => self::BOM_UTF16_LE, + self::ENCODING_UTF16_BE => self::BOM_UTF16_BE, + self::ENCODING_UTF32_LE => self::BOM_UTF32_LE, + self::ENCODING_UTF32_BE => self::BOM_UTF32_BE, + ]; + } + + /** + * Returns the number of bytes to use as offset in order to skip the BOM. + * + * @param resource $filePointer Pointer to the file to check + * @param string $encoding Encoding of the file to check + * @return int Bytes offset to apply to skip the BOM (0 means no BOM) + */ + public function getBytesOffsetToSkipBOM($filePointer, $encoding) + { + $byteOffsetToSkipBom = 0; + + if ($this->hasBom($filePointer, $encoding)) { + $bomUsed = $this->supportedEncodingsWithBom[$encoding]; + + // we skip the N first bytes + $byteOffsetToSkipBom = strlen($bomUsed); + } + + return $byteOffsetToSkipBom; + } + + /** + * Returns whether the file identified by the given pointer has a BOM. + * + * @param resource $filePointer Pointer to the file to check + * @param string $encoding Encoding of the file to check + * @return bool TRUE if the file has a BOM, FALSE otherwise + */ + protected function hasBOM($filePointer, $encoding) + { + $hasBOM = false; + + $this->globalFunctionsHelper->rewind($filePointer); + + if (array_key_exists($encoding, $this->supportedEncodingsWithBom)) { + $potentialBom = $this->supportedEncodingsWithBom[$encoding]; + $numBytesInBom = strlen($potentialBom); + + $hasBOM = ($this->globalFunctionsHelper->fgets($filePointer, $numBytesInBom + 1) === $potentialBom); + } + + return $hasBOM; + } + + /** + * Attempts to convert a non UTF-8 string into UTF-8. + * + * @param string $string Non UTF-8 string to be converted + * @param string $sourceEncoding The encoding used to encode the source string + * @return string The converted, UTF-8 string + * @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed + */ + public function attemptConversionToUTF8($string, $sourceEncoding) + { + return $this->attemptConversion($string, $sourceEncoding, self::ENCODING_UTF8); + } + + /** + * Attempts to convert a UTF-8 string into the given encoding. + * + * @param string $string UTF-8 string to be converted + * @param string $targetEncoding The encoding the string should be re-encoded into + * @return string The converted string, encoded with the given encoding + * @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed + */ + public function attemptConversionFromUTF8($string, $targetEncoding) + { + return $this->attemptConversion($string, self::ENCODING_UTF8, $targetEncoding); + } + + /** + * Attempts to convert the given string to the given encoding. + * Depending on what is installed on the server, we will try to iconv or mbstring. + * + * @param string $string string to be converted + * @param string $sourceEncoding The encoding used to encode the source string + * @param string $targetEncoding The encoding the string should be re-encoded into + * @return string The converted string, encoded with the given encoding + * @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed + */ + protected function attemptConversion($string, $sourceEncoding, $targetEncoding) + { + // if source and target encodings are the same, it's a no-op + if ($sourceEncoding === $targetEncoding) { + return $string; + } + + $convertedString = null; + + if ($this->canUseIconv()) { + $convertedString = $this->globalFunctionsHelper->iconv($string, $sourceEncoding, $targetEncoding); + } else if ($this->canUseMbString()) { + $convertedString = $this->globalFunctionsHelper->mb_convert_encoding($string, $sourceEncoding, $targetEncoding); + } else { + throw new EncodingConversionException("The conversion from $sourceEncoding to $targetEncoding is not supported. Please install \"iconv\" or \"PHP Intl\"."); + } + + if ($convertedString === false) { + throw new EncodingConversionException("The conversion from $sourceEncoding to $targetEncoding failed."); + } + + return $convertedString; + } + + /** + * Returns whether "iconv" can be used. + * + * @return bool TRUE if "iconv" is available and can be used, FALSE otherwise + */ + protected function canUseIconv() + { + return $this->globalFunctionsHelper->function_exists('iconv'); + } + + /** + * Returns whether "mb_string" functions can be used. + * These functions come with the PHP Intl package. + * + * @return bool TRUE if "mb_string" functions are available and can be used, FALSE otherwise + */ + protected function canUseMbString() + { + return $this->globalFunctionsHelper->function_exists('mb_convert_encoding'); + } +} diff --git a/lib/spout/src/Spout/Common/Helper/FileSystemHelper.php b/lib/spout/src/Spout/Common/Helper/FileSystemHelper.php new file mode 100644 index 00000000000..4d4f0be6880 --- /dev/null +++ b/lib/spout/src/Spout/Common/Helper/FileSystemHelper.php @@ -0,0 +1,132 @@ +baseFolderPath = $baseFolderPath; + } + + /** + * Creates an empty folder with the given name under the given parent folder. + * + * @param string $parentFolderPath The parent folder path under which the folder is going to be created + * @param string $folderName The name of the folder to create + * @return string Path of the created folder + * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or if the folder path is not inside of the base folder + */ + public function createFolder($parentFolderPath, $folderName) + { + $this->throwIfOperationNotInBaseFolder($parentFolderPath); + + $folderPath = $parentFolderPath . '/' . $folderName; + + $wasCreationSuccessful = mkdir($folderPath, 0777, true); + if (!$wasCreationSuccessful) { + throw new IOException("Unable to create folder: $folderPath"); + } + + return $folderPath; + } + + /** + * Creates a file with the given name and content in the given folder. + * The parent folder must exist. + * + * @param string $parentFolderPath The parent folder path where the file is going to be created + * @param string $fileName The name of the file to create + * @param string $fileContents The contents of the file to create + * @return string Path of the created file + * @throws \Box\Spout\Common\Exception\IOException If unable to create the file or if the file path is not inside of the base folder + */ + public function createFileWithContents($parentFolderPath, $fileName, $fileContents) + { + $this->throwIfOperationNotInBaseFolder($parentFolderPath); + + $filePath = $parentFolderPath . '/' . $fileName; + + $wasCreationSuccessful = file_put_contents($filePath, $fileContents); + if ($wasCreationSuccessful === false) { + throw new IOException("Unable to create file: $filePath"); + } + + return $filePath; + } + + /** + * Delete the file at the given path + * + * @param string $filePath Path of the file to delete + * @return void + * @throws \Box\Spout\Common\Exception\IOException If the file path is not inside of the base folder + */ + public function deleteFile($filePath) + { + $this->throwIfOperationNotInBaseFolder($filePath); + + if (file_exists($filePath) && is_file($filePath)) { + unlink($filePath); + } + } + + /** + * Delete the folder at the given path as well as all its contents + * + * @param string $folderPath Path of the folder to delete + * @return void + * @throws \Box\Spout\Common\Exception\IOException If the folder path is not inside of the base folder + */ + public function deleteFolderRecursively($folderPath) + { + $this->throwIfOperationNotInBaseFolder($folderPath); + + $itemIterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($itemIterator as $item) { + if ($item->isDir()) { + rmdir($item->getPathname()); + } else { + unlink($item->getPathname()); + } + } + + rmdir($folderPath); + } + + /** + * All I/O operations must occur inside the base folder, for security reasons. + * This function will throw an exception if the folder where the I/O operation + * should occur is not inside the base folder. + * + * @param string $operationFolderPath The path of the folder where the I/O operation should occur + * @return void + * @throws \Box\Spout\Common\Exception\IOException If the folder where the I/O operation should occur is not inside the base folder + */ + protected function throwIfOperationNotInBaseFolder($operationFolderPath) + { + $isInBaseFolder = (strpos($operationFolderPath, $this->baseFolderPath) === 0); + if (!$isInBaseFolder) { + throw new IOException("Cannot perform I/O operation outside of the base folder: {$this->baseFolderPath}"); + } + } +} diff --git a/lib/spout/src/Spout/Common/Helper/GlobalFunctionsHelper.php b/lib/spout/src/Spout/Common/Helper/GlobalFunctionsHelper.php new file mode 100644 index 00000000000..1d660c1000b --- /dev/null +++ b/lib/spout/src/Spout/Common/Helper/GlobalFunctionsHelper.php @@ -0,0 +1,305 @@ +convertToUseRealPath($filePath); + return file_get_contents($realFilePath); + } + + /** + * Updates the given file path to use a real path. + * This is to avoid issues on some Windows setup. + * + * @param string $filePath File path + * @return string The file path using a real path + */ + protected function convertToUseRealPath($filePath) + { + $realFilePath = $filePath; + + if ($this->isZipStream($filePath)) { + if (preg_match('/zip:\/\/(.*)#(.*)/', $filePath, $matches)) { + $documentPath = $matches[1]; + $documentInsideZipPath = $matches[2]; + $realFilePath = 'zip://' . realpath($documentPath) . '#' . $documentInsideZipPath; + } + } else { + $realFilePath = realpath($filePath); + } + + return $realFilePath; + } + + /** + * Returns whether the given path is a zip stream. + * + * @param string $path Path pointing to a document + * @return bool TRUE if path is a zip stream, FALSE otherwise + */ + protected function isZipStream($path) + { + return (strpos($path, 'zip://') === 0); + } + + /** + * Wrapper around global function feof() + * @see feof() + * + * @param resource + * @return bool + */ + public function feof($handle) + { + return feof($handle); + } + + /** + * Wrapper around global function is_readable() + * @see is_readable() + * + * @param string $fileName + * @return bool + */ + public function is_readable($fileName) + { + return is_readable($fileName); + } + + /** + * Wrapper around global function basename() + * @see basename() + * + * @param string $path + * @param string|void $suffix + * @return string + */ + public function basename($path, $suffix = null) + { + return basename($path, $suffix); + } + + /** + * Wrapper around global function header() + * @see header() + * + * @param string $string + * @return void + */ + public function header($string) + { + header($string); + } + + /** + * Wrapper around global function iconv() + * @see iconv() + * + * @param string $string The string to be converted + * @param string $sourceEncoding The encoding of the source string + * @param string $targetEncoding The encoding the source string should be converted to + * @return string|bool the converted string or FALSE on failure. + */ + public function iconv($string, $sourceEncoding, $targetEncoding) + { + return iconv($sourceEncoding, $targetEncoding, $string); + } + + /** + * Wrapper around global function mb_convert_encoding() + * @see mb_convert_encoding() + * + * @param string $string The string to be converted + * @param string $sourceEncoding The encoding of the source string + * @param string $targetEncoding The encoding the source string should be converted to + * @return string|bool the converted string or FALSE on failure. + */ + public function mb_convert_encoding($string, $sourceEncoding, $targetEncoding) + { + return mb_convert_encoding($string, $targetEncoding, $sourceEncoding); + } + + /** + * Wrapper around global function stream_get_wrappers() + * @see stream_get_wrappers() + * + * @return array + */ + public function stream_get_wrappers() + { + return stream_get_wrappers(); + } + + /** + * Wrapper around global function function_exists() + * @see function_exists() + * + * @param string $functionName + * @return bool + */ + public function function_exists($functionName) + { + return function_exists($functionName); + } +} diff --git a/lib/spout/src/Spout/Common/Helper/StringHelper.php b/lib/spout/src/Spout/Common/Helper/StringHelper.php new file mode 100644 index 00000000000..273104e90c4 --- /dev/null +++ b/lib/spout/src/Spout/Common/Helper/StringHelper.php @@ -0,0 +1,71 @@ +hasMbstringSupport = extension_loaded('mbstring'); + } + + /** + * Returns the length of the given string. + * It uses the multi-bytes function is available. + * @see strlen + * @see mb_strlen + * + * @param string $string + * @return int + */ + public function getStringLength($string) + { + return $this->hasMbstringSupport ? mb_strlen($string) : strlen($string); + } + + /** + * Returns the position of the first occurrence of the given character/substring within the given string. + * It uses the multi-bytes function is available. + * @see strpos + * @see mb_strpos + * + * @param string $char Needle + * @param string $string Haystack + * @return int Char/substring's first occurrence position within the string if found (starts at 0) or -1 if not found + */ + public function getCharFirstOccurrencePosition($char, $string) + { + $position = $this->hasMbstringSupport ? mb_strpos($string, $char) : strpos($string, $char); + return ($position !== false) ? $position : -1; + } + + /** + * Returns the position of the last occurrence of the given character/substring within the given string. + * It uses the multi-bytes function is available. + * @see strrpos + * @see mb_strrpos + * + * @param string $char Needle + * @param string $string Haystack + * @return int Char/substring's last occurrence position within the string if found (starts at 0) or -1 if not found + */ + public function getCharLastOccurrencePosition($char, $string) + { + $position = $this->hasMbstringSupport ? mb_strrpos($string, $char) : strrpos($string, $char); + return ($position !== false) ? $position : -1; + } +} diff --git a/lib/spout/src/Spout/Common/Type.php b/lib/spout/src/Spout/Common/Type.php new file mode 100644 index 00000000000..5e9b75e6146 --- /dev/null +++ b/lib/spout/src/Spout/Common/Type.php @@ -0,0 +1,16 @@ +globalFunctionsHelper = $globalFunctionsHelper; + return $this; + } + + /** + * Prepares the reader to read the given file. It also makes sure + * that the file exists and is readable. + * + * @api + * @param string $filePath Path of the file to be read + * @return void + * @throws \Box\Spout\Common\Exception\IOException If the file at the given path does not exist, is not readable or is corrupted + */ + public function open($filePath) + { + if ($this->isStreamWrapper($filePath) && (!$this->doesSupportStreamWrapper() || !$this->isSupportedStreamWrapper($filePath))) { + throw new IOException("Could not open $filePath for reading! Stream wrapper used is not supported for this type of file."); + } + + if (!$this->isPhpStream($filePath)) { + // we skip the checks if the provided file path points to a PHP stream + if (!$this->globalFunctionsHelper->file_exists($filePath)) { + throw new IOException("Could not open $filePath for reading! File does not exist."); + } else if (!$this->globalFunctionsHelper->is_readable($filePath)) { + throw new IOException("Could not open $filePath for reading! File is not readable."); + } + } + + try { + $fileRealPath = $this->getFileRealPath($filePath); + $this->openReader($fileRealPath); + $this->isStreamOpened = true; + } catch (\Exception $exception) { + throw new IOException("Could not open $filePath for reading! ({$exception->getMessage()})"); + } + } + + /** + * Returns the real path of the given path. + * If the given path is a valid stream wrapper, returns the path unchanged. + * + * @param string $filePath + * @return string + */ + protected function getFileRealPath($filePath) + { + if ($this->isSupportedStreamWrapper($filePath)) { + return $filePath; + } + + // Need to use realpath to fix "Can't open file" on some Windows setup + return realpath($filePath); + } + + /** + * Returns the scheme of the custom stream wrapper, if the path indicates a stream wrapper is used. + * For example, php://temp => php, s3://path/to/file => s3... + * + * @param string $filePath Path of the file to be read + * @return string|null The stream wrapper scheme or NULL if not a stream wrapper + */ + protected function getStreamWrapperScheme($filePath) + { + $streamScheme = null; + if (preg_match('/^(\w+):\/\//', $filePath, $matches)) { + $streamScheme = $matches[1]; + } + return $streamScheme; + } + + /** + * Checks if the given path is an unsupported stream wrapper + * (like local path, php://temp, mystream://foo/bar...). + * + * @param string $filePath Path of the file to be read + * @return bool Whether the given path is an unsupported stream wrapper + */ + protected function isStreamWrapper($filePath) + { + return ($this->getStreamWrapperScheme($filePath) !== null); + } + + /** + * Checks if the given path is an supported stream wrapper + * (like php://temp, mystream://foo/bar...). + * If the given path is a local path, returns true. + * + * @param string $filePath Path of the file to be read + * @return bool Whether the given path is an supported stream wrapper + */ + protected function isSupportedStreamWrapper($filePath) + { + $streamScheme = $this->getStreamWrapperScheme($filePath); + return ($streamScheme !== null) ? + in_array($streamScheme, $this->globalFunctionsHelper->stream_get_wrappers()) : + true; + } + + /** + * Checks if a path is a PHP stream (like php://output, php://memory, ...) + * + * @param string $filePath Path of the file to be read + * @return bool Whether the given path maps to a PHP stream + */ + protected function isPhpStream($filePath) + { + $streamScheme = $this->getStreamWrapperScheme($filePath); + return ($streamScheme === 'php'); + } + + /** + * Returns an iterator to iterate over sheets. + * + * @api + * @return \Iterator To iterate over sheets + * @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException If called before opening the reader + */ + public function getSheetIterator() + { + if (!$this->isStreamOpened) { + throw new ReaderNotOpenedException('Reader should be opened first.'); + } + + return $this->getConcreteSheetIterator(); + } + + /** + * Closes the reader, preventing any additional reading + * + * @api + * @return void + */ + public function close() + { + if ($this->isStreamOpened) { + $this->closeReader(); + + $sheetIterator = $this->getConcreteSheetIterator(); + if ($sheetIterator) { + $sheetIterator->end(); + } + + $this->isStreamOpened = false; + } + } +} diff --git a/lib/spout/src/Spout/Reader/CSV/Reader.php b/lib/spout/src/Spout/Reader/CSV/Reader.php new file mode 100644 index 00000000000..ab887ef73af --- /dev/null +++ b/lib/spout/src/Spout/Reader/CSV/Reader.php @@ -0,0 +1,152 @@ +fieldDelimiter = $fieldDelimiter; + return $this; + } + + /** + * Sets the field enclosure for the CSV. + * Needs to be called before opening the reader. + * + * @param string $fieldEnclosure Character that enclose fields + * @return Reader + */ + public function setFieldEnclosure($fieldEnclosure) + { + $this->fieldEnclosure = $fieldEnclosure; + return $this; + } + + /** + * Sets the encoding of the CSV file to be read. + * Needs to be called before opening the reader. + * + * @param string $encoding Encoding of the CSV file to be read + * @return Reader + */ + public function setEncoding($encoding) + { + $this->encoding = $encoding; + return $this; + } + + /** + * Sets the EOL for the CSV. + * Needs to be called before opening the reader. + * + * @param string $endOfLineCharacter used to properly get lines from the CSV file. + * @return Reader + */ + public function setEndOfLineCharacter($endOfLineCharacter) + { + $this->endOfLineCharacter = $endOfLineCharacter; + return $this; + } + + /** + * Returns whether stream wrappers are supported + * + * @return bool + */ + protected function doesSupportStreamWrapper() + { + return true; + } + + /** + * Opens the file at the given path to make it ready to be read. + * If setEncoding() was not called, it assumes that the file is encoded in UTF-8. + * + * @param string $filePath Path of the CSV file to be read + * @return void + * @throws \Box\Spout\Common\Exception\IOException + */ + protected function openReader($filePath) + { + $this->autoDetectLineEndings = ini_get('auto_detect_line_endings'); + ini_set('auto_detect_line_endings', '1'); + + $this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r'); + if (!$this->filePointer) { + throw new IOException("Could not open file $filePath for reading."); + } + + $this->sheetIterator = new SheetIterator( + $this->filePointer, + $this->fieldDelimiter, + $this->fieldEnclosure, + $this->encoding, + $this->endOfLineCharacter, + $this->globalFunctionsHelper + ); + } + + /** + * Returns an iterator to iterate over sheets. + * + * @return SheetIterator To iterate over sheets + */ + public function getConcreteSheetIterator() + { + return $this->sheetIterator; + } + + + /** + * Closes the reader. To be used after reading the file. + * + * @return void + */ + protected function closeReader() + { + if ($this->filePointer) { + $this->globalFunctionsHelper->fclose($this->filePointer); + } + + ini_set('auto_detect_line_endings', $this->autoDetectLineEndings); + } +} diff --git a/lib/spout/src/Spout/Reader/CSV/RowIterator.php b/lib/spout/src/Spout/Reader/CSV/RowIterator.php new file mode 100644 index 00000000000..95b2596e99b --- /dev/null +++ b/lib/spout/src/Spout/Reader/CSV/RowIterator.php @@ -0,0 +1,236 @@ +filePointer = $filePointer; + $this->fieldDelimiter = $fieldDelimiter; + $this->fieldEnclosure = $fieldEnclosure; + $this->encoding = $encoding; + $this->inputEOLDelimiter = $endOfLineDelimiter; + $this->globalFunctionsHelper = $globalFunctionsHelper; + + $this->encodingHelper = new EncodingHelper($globalFunctionsHelper); + } + + /** + * Rewind the Iterator to the first element + * @link http://php.net/manual/en/iterator.rewind.php + * + * @return void + */ + public function rewind() + { + $this->rewindAndSkipBom(); + + $this->numReadRows = 0; + $this->rowDataBuffer = null; + + $this->next(); + } + + /** + * This rewinds and skips the BOM if inserted at the beginning of the file + * by moving the file pointer after it, so that it is not read. + * + * @return void + */ + protected function rewindAndSkipBom() + { + $byteOffsetToSkipBom = $this->encodingHelper->getBytesOffsetToSkipBOM($this->filePointer, $this->encoding); + + // sets the cursor after the BOM (0 means no BOM, so rewind it) + $this->globalFunctionsHelper->fseek($this->filePointer, $byteOffsetToSkipBom); + } + + /** + * Checks if current position is valid + * @link http://php.net/manual/en/iterator.valid.php + * + * @return boolean + */ + public function valid() + { + return ($this->filePointer && !$this->hasReachedEndOfFile); + } + + /** + * Move forward to next element. Empty rows are skipped. + * @link http://php.net/manual/en/iterator.next.php + * + * @return void + * @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8 + */ + public function next() + { + $this->hasReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer); + + if ($this->hasReachedEndOfFile) { + return; + } + + do { + $rowData = $this->getNextUTF8EncodedRow(); + $hasNowReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer); + } while (($rowData === false && !$hasNowReachedEndOfFile) || $this->isEmptyLine($rowData)); + + if ($rowData !== false) { + $this->rowDataBuffer = $rowData; + $this->numReadRows++; + } else { + // If we reach this point, it means end of file was reached. + // This happens when the last lines are empty lines. + $this->hasReachedEndOfFile = $hasNowReachedEndOfFile; + } + } + + /** + * Returns the next row, converted if necessary to UTF-8. + * As fgetcsv() does not manage correctly encoding for non UTF-8 data, + * we remove manually whitespace with ltrim or rtrim (depending on the order of the bytes) + * + * @return array|false The row for the current file pointer, encoded in UTF-8 or FALSE if nothing to read + * @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8 + */ + protected function getNextUTF8EncodedRow() + { + $encodedRowData = fgetcsv($this->filePointer, self::MAX_READ_BYTES_PER_LINE, $this->fieldDelimiter, $this->fieldEnclosure); + if (false === $encodedRowData) { + return false; + } + + foreach ($encodedRowData as $cellIndex => $cellValue) { + switch($this->encoding) { + case EncodingHelper::ENCODING_UTF16_LE: + case EncodingHelper::ENCODING_UTF32_LE: + // remove whitespace from the beginning of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data + $cellValue = ltrim($cellValue); + break; + + case EncodingHelper::ENCODING_UTF16_BE: + case EncodingHelper::ENCODING_UTF32_BE: + // remove whitespace from the end of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data + $cellValue = rtrim($cellValue); + break; + } + + $encodedRowData[$cellIndex] = $this->encodingHelper->attemptConversionToUTF8($cellValue, $this->encoding); + } + + return $encodedRowData; + } + + /** + * Returns the end of line delimiter, encoded using the same encoding as the CSV. + * The return value is cached. + * + * @return string + */ + protected function getEncodedEOLDelimiter() + { + if (!isset($this->encodedEOLDelimiter)) { + $this->encodedEOLDelimiter = $this->encodingHelper->attemptConversionFromUTF8($this->inputEOLDelimiter, $this->encoding); + } + + return $this->encodedEOLDelimiter; + } + + /** + * @param array $lineData Array containing the cells value for the line + * @return bool Whether the given line is empty + */ + protected function isEmptyLine($lineData) + { + return (is_array($lineData) && count($lineData) === 1 && $lineData[0] === null); + } + + /** + * Return the current element from the buffer + * @link http://php.net/manual/en/iterator.current.php + * + * @return array|null + */ + public function current() + { + return $this->rowDataBuffer; + } + + /** + * Return the key of the current element + * @link http://php.net/manual/en/iterator.key.php + * + * @return int + */ + public function key() + { + return $this->numReadRows; + } + + /** + * Cleans up what was created to iterate over the object. + * + * @return void + */ + public function end() + { + // do nothing + } +} diff --git a/lib/spout/src/Spout/Reader/CSV/Sheet.php b/lib/spout/src/Spout/Reader/CSV/Sheet.php new file mode 100644 index 00000000000..b9c66c70f94 --- /dev/null +++ b/lib/spout/src/Spout/Reader/CSV/Sheet.php @@ -0,0 +1,37 @@ +rowIterator = new RowIterator($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $endOfLineCharacter, $globalFunctionsHelper); + } + + /** + * @api + * @return \Box\Spout\Reader\CSV\RowIterator + */ + public function getRowIterator() + { + return $this->rowIterator; + } +} diff --git a/lib/spout/src/Spout/Reader/CSV/SheetIterator.php b/lib/spout/src/Spout/Reader/CSV/SheetIterator.php new file mode 100644 index 00000000000..8ee2e99b728 --- /dev/null +++ b/lib/spout/src/Spout/Reader/CSV/SheetIterator.php @@ -0,0 +1,97 @@ +sheet = new Sheet($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $endOfLineCharacter, $globalFunctionsHelper); + } + + /** + * Rewind the Iterator to the first element + * @link http://php.net/manual/en/iterator.rewind.php + * + * @return void + */ + public function rewind() + { + $this->hasReadUniqueSheet = false; + } + + /** + * Checks if current position is valid + * @link http://php.net/manual/en/iterator.valid.php + * + * @return boolean + */ + public function valid() + { + return (!$this->hasReadUniqueSheet); + } + + /** + * Move forward to next element + * @link http://php.net/manual/en/iterator.next.php + * + * @return void + */ + public function next() + { + $this->hasReadUniqueSheet = true; + } + + /** + * Return the current element + * @link http://php.net/manual/en/iterator.current.php + * + * @return \Box\Spout\Reader\CSV\Sheet + */ + public function current() + { + return $this->sheet; + } + + /** + * Return the key of the current element + * @link http://php.net/manual/en/iterator.key.php + * + * @return int + */ + public function key() + { + return 1; + } + + /** + * Cleans up what was created to iterate over the object. + * + * @return void + */ + public function end() + { + // do nothing + } +} diff --git a/lib/spout/src/Spout/Reader/Exception/IteratorNotRewindableException.php b/lib/spout/src/Spout/Reader/Exception/IteratorNotRewindableException.php new file mode 100644 index 00000000000..a030c121e85 --- /dev/null +++ b/lib/spout/src/Spout/Reader/Exception/IteratorNotRewindableException.php @@ -0,0 +1,13 @@ +escaper = new \Box\Spout\Common\Escaper\ODS(); + } + + /** + * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node. + * @see http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#refTable13 + * + * @param \DOMNode $node + * @return string|int|float|bool|\DateTime|\DateInterval|null The value associated with the cell, empty string if cell's type is void/undefined, null on error + */ + public function extractAndFormatNodeValue($node) + { + $cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE); + + switch ($cellType) { + case self::CELL_TYPE_STRING: + return $this->formatStringCellValue($node); + case self::CELL_TYPE_FLOAT: + return $this->formatFloatCellValue($node); + case self::CELL_TYPE_BOOLEAN: + return $this->formatBooleanCellValue($node); + case self::CELL_TYPE_DATE: + return $this->formatDateCellValue($node); + case self::CELL_TYPE_TIME: + return $this->formatTimeCellValue($node); + case self::CELL_TYPE_CURRENCY: + return $this->formatCurrencyCellValue($node); + case self::CELL_TYPE_PERCENTAGE: + return $this->formatPercentageCellValue($node); + case self::CELL_TYPE_VOID: + default: + return ''; + } + } + + /** + * Returns the cell String value. + * + * @param \DOMNode $node + * @return string The value associated with the cell + */ + protected function formatStringCellValue($node) + { + $pNodeValues = []; + $pNodes = $node->getElementsByTagName(self::XML_NODE_P); + + foreach ($pNodes as $pNode) { + $currentPValue = ''; + + foreach ($pNode->childNodes as $childNode) { + if ($childNode instanceof \DOMText) { + $currentPValue .= $childNode->nodeValue; + } else if ($childNode->nodeName === self::XML_NODE_S) { + $spaceAttribute = $childNode->getAttribute(self::XML_ATTRIBUTE_C); + $numSpaces = (!empty($spaceAttribute)) ? intval($spaceAttribute) : 1; + $currentPValue .= str_repeat(' ', $numSpaces); + } + } + + $pNodeValues[] = $currentPValue; + } + + $escapedCellValue = implode("\n", $pNodeValues); + $cellValue = $this->escaper->unescape($escapedCellValue); + return $cellValue; + } + + /** + * Returns the cell Numeric value from the given node. + * + * @param \DOMNode $node + * @return int|float The value associated with the cell + */ + protected function formatFloatCellValue($node) + { + $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_VALUE); + $nodeIntValue = intval($nodeValue); + $cellValue = ($nodeIntValue == $nodeValue) ? $nodeIntValue : floatval($nodeValue); + return $cellValue; + } + + /** + * Returns the cell Boolean value from the given node. + * + * @param \DOMNode $node + * @return bool The value associated with the cell + */ + protected function formatBooleanCellValue($node) + { + $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_BOOLEAN_VALUE); + // !! is similar to boolval() + $cellValue = !!$nodeValue; + return $cellValue; + } + + /** + * Returns the cell Date value from the given node. + * + * @param \DOMNode $node + * @return \DateTime|null The value associated with the cell or NULL if invalid date value + */ + protected function formatDateCellValue($node) + { + try { + $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_DATE_VALUE); + return new \DateTime($nodeValue); + } catch (\Exception $e) { + return null; + } + } + + /** + * Returns the cell Time value from the given node. + * + * @param \DOMNode $node + * @return \DateInterval|null The value associated with the cell or NULL if invalid time value + */ + protected function formatTimeCellValue($node) + { + try { + $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_TIME_VALUE); + return new \DateInterval($nodeValue); + } catch (\Exception $e) { + return null; + } + } + + /** + * Returns the cell Currency value from the given node. + * + * @param \DOMNode $node + * @return string The value associated with the cell (e.g. "100 USD" or "9.99 EUR") + */ + protected function formatCurrencyCellValue($node) + { + $value = $node->getAttribute(self::XML_ATTRIBUTE_VALUE); + $currency = $node->getAttribute(self::XML_ATTRIBUTE_CURRENCY); + + return "$value $currency"; + } + + /** + * Returns the cell Percentage value from the given node. + * + * @param \DOMNode $node + * @return int|float The value associated with the cell + */ + protected function formatPercentageCellValue($node) + { + // percentages are formatted like floats + return $this->formatFloatCellValue($node); + } +} diff --git a/lib/spout/src/Spout/Reader/ODS/Reader.php b/lib/spout/src/Spout/Reader/ODS/Reader.php new file mode 100644 index 00000000000..b4093ae6752 --- /dev/null +++ b/lib/spout/src/Spout/Reader/ODS/Reader.php @@ -0,0 +1,72 @@ +zip = new \ZipArchive(); + + if ($this->zip->open($filePath) === true) { + $this->sheetIterator = new SheetIterator($filePath); + } else { + throw new IOException("Could not open $filePath for reading."); + } + } + + /** + * Returns an iterator to iterate over sheets. + * + * @return SheetIterator To iterate over sheets + */ + public function getConcreteSheetIterator() + { + return $this->sheetIterator; + } + + /** + * Closes the reader. To be used after reading the file. + * + * @return void + */ + protected function closeReader() + { + if ($this->zip) { + $this->zip->close(); + } + } +} diff --git a/lib/spout/src/Spout/Reader/ODS/RowIterator.php b/lib/spout/src/Spout/Reader/ODS/RowIterator.php new file mode 100644 index 00000000000..aa7a4964a8a --- /dev/null +++ b/lib/spout/src/Spout/Reader/ODS/RowIterator.php @@ -0,0 +1,229 @@ +" element + */ + public function __construct($xmlReader) + { + $this->xmlReader = $xmlReader; + $this->cellValueFormatter = new CellValueFormatter(); + } + + /** + * Rewind the Iterator to the first element. + * NOTE: It can only be done once, as it is not possible to read an XML file backwards. + * @link http://php.net/manual/en/iterator.rewind.php + * + * @return void + * @throws \Box\Spout\Reader\Exception\IteratorNotRewindableException If the iterator is rewound more than once + */ + public function rewind() + { + // Because sheet and row data is located in the file, we can't rewind both the + // sheet iterator and the row iterator, as XML file cannot be read backwards. + // Therefore, rewinding the row iterator has been disabled. + if ($this->hasAlreadyBeenRewound) { + throw new IteratorNotRewindableException(); + } + + $this->hasAlreadyBeenRewound = true; + $this->numReadRows = 0; + $this->rowDataBuffer = null; + $this->hasReachedEndOfFile = false; + + $this->next(); + } + + /** + * Checks if current position is valid + * @link http://php.net/manual/en/iterator.valid.php + * + * @return boolean + */ + public function valid() + { + return (!$this->hasReachedEndOfFile); + } + + /** + * Move forward to next element. Empty rows will be skipped. + * @link http://php.net/manual/en/iterator.next.php + * + * @return void + * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found + * @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML + */ + public function next() + { + $rowData = []; + $cellValue = null; + $numColumnsRepeated = 1; + $numCellsRead = 0; + $hasAlreadyReadOneCell = false; + + try { + while ($this->xmlReader->read()) { + if ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL)) { + // Start of a cell description + $currentNumColumnsRepeated = $this->getNumColumnsRepeatedForCurrentNode(); + + $node = $this->xmlReader->expand(); + $currentCellValue = $this->getCellValue($node); + + // process cell N only after having read cell N+1 (see below why) + if ($hasAlreadyReadOneCell) { + for ($i = 0; $i < $numColumnsRepeated; $i++) { + $rowData[] = $cellValue; + } + } + + $cellValue = $currentCellValue; + $numColumnsRepeated = $currentNumColumnsRepeated; + + $numCellsRead++; + $hasAlreadyReadOneCell = true; + + } else if ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_ROW)) { + // End of the row description + $isEmptyRow = ($numCellsRead <= 1 && $this->isEmptyCellValue($cellValue)); + if ($isEmptyRow) { + // skip empty rows + $this->next(); + return; + } + + // Only add the value if the last read cell is not a trailing empty cell repeater in Excel. + // The current count of read columns is determined by counting the values in $rowData. + // This is to avoid creating a lot of empty cells, as Excel adds a last empty "" + // with a number-columns-repeated value equals to the number of (supported columns - used columns). + // In Excel, the number of supported columns is 16384, but we don't want to returns rows with + // always 16384 cells. + if ((count($rowData) + $numColumnsRepeated) !== self::MAX_COLUMNS_EXCEL) { + for ($i = 0; $i < $numColumnsRepeated; $i++) { + $rowData[] = $cellValue; + } + $this->numReadRows++; + } + break; + + } else if ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_TABLE)) { + // The closing "" marks the end of the file + $this->hasReachedEndOfFile = true; + break; + } + } + + } catch (XMLProcessingException $exception) { + throw new IOException("The sheet's data cannot be read. [{$exception->getMessage()}]"); + } + + $this->rowDataBuffer = $rowData; + } + + /** + * @return int The value of "table:number-columns-repeated" attribute of the current node, or 1 if attribute missing + */ + protected function getNumColumnsRepeatedForCurrentNode() + { + $numColumnsRepeated = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_COLUMNS_REPEATED); + return ($numColumnsRepeated !== null) ? intval($numColumnsRepeated) : 1; + } + + /** + * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node. + * + * @param \DOMNode $node + * @return string|int|float|bool|\DateTime|\DateInterval|null The value associated with the cell, empty string if cell's type is void/undefined, null on error + */ + protected function getCellValue($node) + { + return $this->cellValueFormatter->extractAndFormatNodeValue($node); + } + + /** + * empty() replacement that honours 0 as a valid value + * + * @param $value The cell value + * @return bool + */ + protected function isEmptyCellValue($value) + { + return (!isset($value) || trim($value) === ''); + } + + /** + * Return the current element, from the buffer. + * @link http://php.net/manual/en/iterator.current.php + * + * @return array|null + */ + public function current() + { + return $this->rowDataBuffer; + } + + /** + * Return the key of the current element + * @link http://php.net/manual/en/iterator.key.php + * + * @return int + */ + public function key() + { + return $this->numReadRows; + } + + + /** + * Cleans up what was created to iterate over the object. + * + * @return void + */ + public function end() + { + $this->xmlReader->close(); + } +} diff --git a/lib/spout/src/Spout/Reader/ODS/Sheet.php b/lib/spout/src/Spout/Reader/ODS/Sheet.php new file mode 100644 index 00000000000..c78e4aa9f0e --- /dev/null +++ b/lib/spout/src/Spout/Reader/ODS/Sheet.php @@ -0,0 +1,66 @@ +" element + * @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based) + * @param string $sheetName Name of the sheet + */ + public function __construct($xmlReader, $sheetIndex, $sheetName) + { + $this->rowIterator = new RowIterator($xmlReader); + $this->index = $sheetIndex; + $this->name = $sheetName; + } + + /** + * @api + * @return \Box\Spout\Reader\ODS\RowIterator + */ + public function getRowIterator() + { + return $this->rowIterator; + } + + /** + * @api + * @return int Index of the sheet, based on order in the workbook (zero-based) + */ + public function getIndex() + { + return $this->index; + } + + /** + * @api + * @return string Name of the sheet + */ + public function getName() + { + return $this->name; + } +} diff --git a/lib/spout/src/Spout/Reader/ODS/SheetIterator.php b/lib/spout/src/Spout/Reader/ODS/SheetIterator.php new file mode 100644 index 00000000000..f8683f0cf77 --- /dev/null +++ b/lib/spout/src/Spout/Reader/ODS/SheetIterator.php @@ -0,0 +1,135 @@ +filePath = $filePath; + $this->xmlReader = new XMLReader(); + + /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ + $this->escaper = new \Box\Spout\Common\Escaper\ODS(); + } + + /** + * Rewind the Iterator to the first element + * @link http://php.net/manual/en/iterator.rewind.php + * + * @return void + * @throws \Box\Spout\Common\Exception\IOException If unable to open the XML file containing sheets' data + */ + public function rewind() + { + $this->xmlReader->close(); + + $contentXmlFilePath = $this->filePath . '#content.xml'; + if ($this->xmlReader->open('zip://' . $contentXmlFilePath) === false) { + throw new IOException("Could not open \"{$contentXmlFilePath}\"."); + } + + try { + $this->hasFoundSheet = $this->xmlReader->readUntilNodeFound(self::XML_NODE_TABLE); + } catch (XMLProcessingException $exception) { + throw new IOException("The content.xml file is invalid and cannot be read. [{$exception->getMessage()}]"); + } + + $this->currentSheetIndex = 0; + } + + /** + * Checks if current position is valid + * @link http://php.net/manual/en/iterator.valid.php + * + * @return boolean + */ + public function valid() + { + return $this->hasFoundSheet; + } + + /** + * Move forward to next element + * @link http://php.net/manual/en/iterator.next.php + * + * @return void + */ + public function next() + { + $this->hasFoundSheet = $this->xmlReader->readUntilNodeFound(self::XML_NODE_TABLE); + + if ($this->hasFoundSheet) { + $this->currentSheetIndex++; + } + } + + /** + * Return the current element + * @link http://php.net/manual/en/iterator.current.php + * + * @return \Box\Spout\Reader\ODS\Sheet + */ + public function current() + { + $escapedSheetName = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_TABLE_NAME); + $sheetName = $this->escaper->unescape($escapedSheetName); + + return new Sheet($this->xmlReader, $sheetName, $this->currentSheetIndex); + } + + /** + * Return the key of the current element + * @link http://php.net/manual/en/iterator.key.php + * + * @return int + */ + public function key() + { + return $this->currentSheetIndex + 1; + } + + /** + * Cleans up what was created to iterate over the object. + * + * @return void + */ + public function end() + { + $this->xmlReader->close(); + } +} diff --git a/lib/spout/src/Spout/Reader/ReaderFactory.php b/lib/spout/src/Spout/Reader/ReaderFactory.php new file mode 100644 index 00000000000..93a52cb8673 --- /dev/null +++ b/lib/spout/src/Spout/Reader/ReaderFactory.php @@ -0,0 +1,48 @@ +setGlobalFunctionsHelper(new GlobalFunctionsHelper()); + + return $reader; + } +} diff --git a/lib/spout/src/Spout/Reader/ReaderInterface.php b/lib/spout/src/Spout/Reader/ReaderInterface.php new file mode 100644 index 00000000000..8ecde308026 --- /dev/null +++ b/lib/spout/src/Spout/Reader/ReaderInterface.php @@ -0,0 +1,36 @@ +useXMLInternalErrors(); + + try { + $this->simpleXMLElement = new \SimpleXMLElement($xmlData); + } catch (\Exception $exception) { + // if the data is invalid, the constructor will throw an Exception + $this->resetXMLInternalErrorsSetting(); + throw new XMLProcessingException($this->getLastXMLErrorMessage()); + } + + $this->resetXMLInternalErrorsSetting(); + + return $this->simpleXMLElement; + } + + /** + * Returns the attribute for the given name. + * + * @param string $name Attribute name + * @param string|null|void $namespace An optional namespace for the retrieved attributes + * @return string|null The attribute value or NULL if attribute not found + */ + public function getAttribute($name, $namespace = null) + { + $isPrefix = ($namespace !== null); + $attributes = $this->simpleXMLElement->attributes($namespace, $isPrefix); + $attributeValue = $attributes->{$name}; + + return ($attributeValue !== null) ? (string) $attributeValue : null; + } + + /** + * Creates a prefix/ns context for the next XPath query + * @see \SimpleXMLElement::registerXPathNamespace + * + * @param string $prefix The namespace prefix to use in the XPath query for the namespace given in "namespace". + * @param string $namespace The namespace to use for the XPath query. This must match a namespace in + * use by the XML document or the XPath query using "prefix" will not return any results. + * @return bool TRUE on success or FALSE on failure. + */ + public function registerXPathNamespace($prefix, $namespace) + { + return $this->simpleXMLElement->registerXPathNamespace($prefix, $namespace); + } + + /** + * Runs XPath query on XML data + * @see \SimpleXMLElement::xpath + * + * @param string $path An XPath path + * @return SimpleXMLElement[]|bool an array of SimpleXMLElement objects or FALSE in case of an error. + */ + public function xpath($path) + { + $elements = $this->simpleXMLElement->xpath($path); + + if ($elements !== false) { + $wrappedElements = []; + foreach ($elements as $element) { + $wrappedElement = $this->wrapSimpleXMLElement($element); + + if ($wrappedElement !== null) { + $wrappedElements[] = $this->wrapSimpleXMLElement($element); + } + } + + $elements = $wrappedElements; + } + + return $elements; + } + + /** + * Wraps the given element into an instance of the wrapper + * + * @param \SimpleXMLElement $element Element to be wrapped + * @return SimpleXMLElement|null The wrapped element or NULL if the given element is invalid + */ + protected function wrapSimpleXMLElement(\SimpleXMLElement $element) + { + $wrappedElement = null; + $elementAsXML = $element->asXML(); + + if ($elementAsXML !== false) { + $wrappedElement = new SimpleXMLElement($elementAsXML); + } + + return $wrappedElement; + } + + /** + * Remove all nodes matching the given XPath query. + * It does not map to any \SimpleXMLElement function. + * + * @param string $path An XPath path + * @return void + */ + public function removeNodesMatchingXPath($path) + { + $nodesToRemove = $this->simpleXMLElement->xpath($path); + + foreach ($nodesToRemove as $nodeToRemove) { + unset($nodeToRemove[0]); + } + } + + /** + * Returns the first child matching the given tag name + * + * @param string $tagName + * @return SimpleXMLElement|null The first child matching the tag name or NULL if none found + */ + public function getFirstChildByTagName($tagName) + { + $doesElementExist = isset($this->simpleXMLElement->{$tagName}); + + /** @var \SimpleXMLElement $realElement */ + $realElement = $this->simpleXMLElement->{$tagName}; + + return $doesElementExist ? $this->wrapSimpleXMLElement($realElement) : null; + } + + /** + * Returns the immediate children. + * + * @return array The children + */ + public function children() + { + $children = []; + + foreach ($this->simpleXMLElement->children() as $child) { + $children[] = $this->wrapSimpleXMLElement($child); + } + + return $children; + } + + /** + * @return string + */ + public function __toString() + { + return $this->simpleXMLElement->__toString(); + } +} diff --git a/lib/spout/src/Spout/Reader/Wrapper/XMLInternalErrorsHelper.php b/lib/spout/src/Spout/Reader/Wrapper/XMLInternalErrorsHelper.php new file mode 100644 index 00000000000..8b4464a997d --- /dev/null +++ b/lib/spout/src/Spout/Reader/Wrapper/XMLInternalErrorsHelper.php @@ -0,0 +1,82 @@ +initialUseInternalErrorsValue = libxml_use_internal_errors(true); + } + + /** + * Throws an XMLProcessingException if an error occured. + * It also always resets the "libxml_use_internal_errors" setting back to its initial value. + * + * @return void + * @throws \Box\Spout\Reader\Exception\XMLProcessingException + */ + protected function resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured() + { + if ($this->hasXMLErrorOccured()) { + $this->resetXMLInternalErrorsSetting(); + throw new XMLProcessingException($this->getLastXMLErrorMessage()); + } + + $this->resetXMLInternalErrorsSetting(); + } + + /** + * Returns whether the a XML error has occured since the last time errors were cleared. + * + * @return bool TRUE if an error occured, FALSE otherwise + */ + private function hasXMLErrorOccured() + { + return (libxml_get_last_error() !== false); + } + + /** + * Returns the error message for the last XML error that occured. + * @see libxml_get_last_error + * + * @return String|null Last XML error message or null if no error + */ + private function getLastXMLErrorMessage() + { + $errorMessage = null; + $error = libxml_get_last_error(); + + if ($error !== false) { + $errorMessage = trim($error->message); + } + + return $errorMessage; + } + + /** + * @return void + */ + protected function resetXMLInternalErrorsSetting() + { + libxml_use_internal_errors($this->initialUseInternalErrorsValue); + } + +} diff --git a/lib/spout/src/Spout/Reader/Wrapper/XMLReader.php b/lib/spout/src/Spout/Reader/Wrapper/XMLReader.php new file mode 100644 index 00000000000..42bd92c2b28 --- /dev/null +++ b/lib/spout/src/Spout/Reader/Wrapper/XMLReader.php @@ -0,0 +1,184 @@ +convertURIToUseRealPath($URI); + + // HHVM does not check if file exists within zip file + // @link https://github.com/facebook/hhvm/issues/5779 + if ($this->isRunningHHVM() && $this->isZipStream($realPathURI)) { + if ($this->fileExistsWithinZip($realPathURI)) { + $wasOpenSuccessful = parent::open($realPathURI, $encoding, $options|LIBXML_NONET); + } + } else { + $wasOpenSuccessful = parent::open($realPathURI, $encoding, $options|LIBXML_NONET); + } + + return $wasOpenSuccessful; + } + + /** + * Updates the given URI to use a real path. + * This is to avoid issues on some Windows setup. + * + * @param string $URI URI + * @return string The URI using a real path + */ + protected function convertURIToUseRealPath($URI) + { + $realPathURI = $URI; + + if ($this->isZipStream($URI)) { + if (preg_match('/zip:\/\/(.*)#(.*)/', $URI, $matches)) { + $documentPath = $matches[1]; + $documentInsideZipPath = $matches[2]; + $realPathURI = 'zip://' . realpath($documentPath) . '#' . $documentInsideZipPath; + } + } else { + $realPathURI = realpath($URI); + } + + return $realPathURI; + } + + /** + * Returns whether the given URI is a zip stream. + * + * @param string $URI URI pointing to a document + * @return bool TRUE if URI is a zip stream, FALSE otherwise + */ + protected function isZipStream($URI) + { + return (strpos($URI, 'zip://') === 0); + } + + /** + * Returns whether the current environment is HHVM + * + * @return bool TRUE if running on HHVM, FALSE otherwise + */ + protected function isRunningHHVM() + { + return defined('HHVM_VERSION'); + } + + /** + * Returns whether the file at the given location exists + * + * @param string $zipStreamURI URI of a zip stream, e.g. "zip://file.zip#path/inside.xml" + * @return bool TRUE if the file exists, FALSE otherwise + */ + protected function fileExistsWithinZip($zipStreamURI) + { + $doesFileExists = false; + + $pattern = '/zip:\/\/([^#]+)#(.*)/'; + if (preg_match($pattern, $zipStreamURI, $matches)) { + $zipFilePath = $matches[1]; + $innerFilePath = $matches[2]; + + $zip = new \ZipArchive(); + if ($zip->open($zipFilePath) === true) { + $doesFileExists = ($zip->locateName($innerFilePath) !== false); + $zip->close(); + } + } + + return $doesFileExists; + } + + /** + * Move to next node in document + * @see \XMLReader::read + * + * @return bool TRUE on success or FALSE on failure + * @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred + */ + public function read() + { + $this->useXMLInternalErrors(); + + $wasReadSuccessful = parent::read(); + + $this->resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured(); + + return $wasReadSuccessful; + } + + /** + * Read until the element with the given name is found, or the end of the file. + * + * @param string $nodeName Name of the node to find + * @return bool TRUE on success or FALSE on failure + * @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred + */ + public function readUntilNodeFound($nodeName) + { + while (($wasReadSuccessful = $this->read()) && ($this->nodeType !== \XMLReader::ELEMENT || $this->name !== $nodeName)) { + // do nothing + } + + return $wasReadSuccessful; + } + + /** + * Move cursor to next node skipping all subtrees + * @see \XMLReader::next + * + * @param string|void $localName The name of the next node to move to + * @return bool TRUE on success or FALSE on failure + * @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred + */ + public function next($localName = null) + { + $this->useXMLInternalErrors(); + + $wasNextSuccessful = parent::next($localName); + + $this->resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured(); + + return $wasNextSuccessful; + } + + /** + * @param string $nodeName + * @return bool Whether the XML Reader is currently positioned on the starting node with given name + */ + public function isPositionedOnStartingNode($nodeName) + { + return ($this->nodeType === XMLReader::ELEMENT && $this->name === $nodeName); + } + + /** + * @param string $nodeName + * @return bool Whether the XML Reader is currently positioned on the ending node with given name + */ + public function isPositionedOnEndingNode($nodeName) + { + return ($this->nodeType === XMLReader::END_ELEMENT && $this->name === $nodeName); + } +} diff --git a/lib/spout/src/Spout/Reader/XLSX/Helper/CellHelper.php b/lib/spout/src/Spout/Reader/XLSX/Helper/CellHelper.php new file mode 100644 index 00000000000..c2f21f7fdf1 --- /dev/null +++ b/lib/spout/src/Spout/Reader/XLSX/Helper/CellHelper.php @@ -0,0 +1,103 @@ + 0, 'B' => 1, 'C' => 2, 'D' => 3, 'E' => 4, 'F' => 5, 'G' => 6, + 'H' => 7, 'I' => 8, 'J' => 9, 'K' => 10, 'L' => 11, 'M' => 12, 'N' => 13, + 'O' => 14, 'P' => 15, 'Q' => 16, 'R' => 17, 'S' => 18, 'T' => 19, 'U' => 20, + 'V' => 21, 'W' => 22, 'X' => 23, 'Y' => 24, 'Z' => 25, + ]; + + /** + * Fills the missing indexes of an array with a given value. + * For instance, $dataArray = []; $a[1] = 1; $a[3] = 3; + * Calling fillMissingArrayIndexes($dataArray, 'FILL') will return this array: ['FILL', 1, 'FILL', 3] + * + * @param array $dataArray The array to fill + * @param string|void $fillValue optional + * @return array + */ + public static function fillMissingArrayIndexes($dataArray, $fillValue = '') + { + $existingIndexes = array_keys($dataArray); + + $newIndexes = array_fill_keys(range(0, max($existingIndexes)), $fillValue); + $dataArray += $newIndexes; + + ksort($dataArray); + + return $dataArray; + } + + /** + * Returns the base 10 column index associated to the cell index (base 26). + * Excel uses A to Z letters for column indexing, where A is the 1st column, + * Z is the 26th and AA is the 27th. + * The mapping is zero based, so that A1 maps to 0, B2 maps to 1, Z13 to 25 and AA4 to 26. + * + * @param string $cellIndex The Excel cell index ('A1', 'BC13', ...) + * @return int + * @throws \Box\Spout\Common\Exception\InvalidArgumentException When the given cell index is invalid + */ + public static function getColumnIndexFromCellIndex($cellIndex) + { + if (!self::isValidCellIndex($cellIndex)) { + throw new InvalidArgumentException('Cannot get column index from an invalid cell index.'); + } + + $columnIndex = 0; + + // Remove row information + $columnLetters = preg_replace('/\d/', '', $cellIndex); + + // strlen() is super slow too... Using isset() is way faster and not too unreadable, + // since we checked before that there are between 1 and 3 letters. + $columnLength = isset($columnLetters[1]) ? (isset($columnLetters[2]) ? 3 : 2) : 1; + + // Looping over the different letters of the column is slower than this method. + // Also, not using the pow() function because it's slooooow... + switch ($columnLength) { + case 1: + $columnIndex = (self::$columnLetterToIndexMapping[$columnLetters]); + break; + case 2: + $firstLetterIndex = (self::$columnLetterToIndexMapping[$columnLetters[0]] + 1) * 26; + $secondLetterIndex = self::$columnLetterToIndexMapping[$columnLetters[1]]; + $columnIndex = $firstLetterIndex + $secondLetterIndex; + break; + case 3: + $firstLetterIndex = (self::$columnLetterToIndexMapping[$columnLetters[0]] + 1) * 676; + $secondLetterIndex = (self::$columnLetterToIndexMapping[$columnLetters[1]] + 1) * 26; + $thirdLetterIndex = self::$columnLetterToIndexMapping[$columnLetters[2]]; + $columnIndex = $firstLetterIndex + $secondLetterIndex + $thirdLetterIndex; + break; + } + + return $columnIndex; + } + + /** + * Returns whether a cell index is valid, in an Excel world. + * To be valid, the cell index should start with capital letters and be followed by numbers. + * There can only be 3 letters, as there can only be 16,384 rows, which is equivalent to 'XFE'. + * + * @param string $cellIndex The Excel cell index ('A1', 'BC13', ...) + * @return bool + */ + protected static function isValidCellIndex($cellIndex) + { + return (preg_match('/^[A-Z]{1,3}\d+$/', $cellIndex) === 1); + } +} diff --git a/lib/spout/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php b/lib/spout/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php new file mode 100644 index 00000000000..c6c40f41151 --- /dev/null +++ b/lib/spout/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php @@ -0,0 +1,241 @@ +sharedStringsHelper = $sharedStringsHelper; + $this->styleHelper = $styleHelper; + + /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ + $this->escaper = new \Box\Spout\Common\Escaper\XLSX(); + } + + /** + * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node. + * + * @param \DOMNode $node + * @return string|int|float|bool|\DateTime|null The value associated with the cell (null when the cell has an error) + */ + public function extractAndFormatNodeValue($node) + { + // Default cell type is "n" + $cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE) ?: self::CELL_TYPE_NUMERIC; + $cellStyleId = intval($node->getAttribute(self::XML_ATTRIBUTE_STYLE_ID)); + $vNodeValue = $this->getVNodeValue($node); + + if (($vNodeValue === '') && ($cellType !== self::CELL_TYPE_INLINE_STRING)) { + return $vNodeValue; + } + + switch ($cellType) { + case self::CELL_TYPE_INLINE_STRING: + return $this->formatInlineStringCellValue($node); + case self::CELL_TYPE_SHARED_STRING: + return $this->formatSharedStringCellValue($vNodeValue); + case self::CELL_TYPE_STR: + return $this->formatStrCellValue($vNodeValue); + case self::CELL_TYPE_BOOLEAN: + return $this->formatBooleanCellValue($vNodeValue); + case self::CELL_TYPE_NUMERIC: + return $this->formatNumericCellValue($vNodeValue, $cellStyleId); + case self::CELL_TYPE_DATE: + return $this->formatDateCellValue($vNodeValue); + default: + return null; + } + } + + /** + * Returns the cell's string value from a node's nested value node + * + * @param \DOMNode $node + * @return string The value associated with the cell + */ + protected function getVNodeValue($node) + { + // for cell types having a "v" tag containing the value. + // if not, the returned value should be empty string. + $vNode = $node->getElementsByTagName(self::XML_NODE_VALUE)->item(0); + return ($vNode !== null) ? $vNode->nodeValue : ''; + } + + /** + * Returns the cell String value where string is inline. + * + * @param \DOMNode $node + * @return string The value associated with the cell (null when the cell has an error) + */ + protected function formatInlineStringCellValue($node) + { + // inline strings are formatted this way: + // [INLINE_STRING] + $tNode = $node->getElementsByTagName(self::XML_NODE_INLINE_STRING_VALUE)->item(0); + $escapedCellValue = trim($tNode->nodeValue); + $cellValue = $this->escaper->unescape($escapedCellValue); + return $cellValue; + } + + /** + * Returns the cell String value from shared-strings file using nodeValue index. + * + * @param string $nodeValue + * @return string The value associated with the cell (null when the cell has an error) + */ + protected function formatSharedStringCellValue($nodeValue) + { + // shared strings are formatted this way: + // [SHARED_STRING_INDEX] + $sharedStringIndex = intval($nodeValue); + $escapedCellValue = $this->sharedStringsHelper->getStringAtIndex($sharedStringIndex); + $cellValue = $this->escaper->unescape($escapedCellValue); + return $cellValue; + } + + /** + * Returns the cell String value, where string is stored in value node. + * + * @param string $nodeValue + * @return string The value associated with the cell (null when the cell has an error) + */ + protected function formatStrCellValue($nodeValue) + { + $escapedCellValue = trim($nodeValue); + $cellValue = $this->escaper->unescape($escapedCellValue); + return $cellValue; + } + + /** + * Returns the cell Numeric value from string of nodeValue. + * The value can also represent a timestamp and a DateTime will be returned. + * + * @param string $nodeValue + * @param int $cellStyleId 0 being the default style + * @return int|float|\DateTime|null The value associated with the cell + */ + protected function formatNumericCellValue($nodeValue, $cellStyleId) + { + // Numeric values can represent numbers as well as timestamps. + // We need to look at the style of the cell to determine whether it is one or the other. + $shouldFormatAsDate = $this->styleHelper->shouldFormatNumericValueAsDate($cellStyleId); + + if ($shouldFormatAsDate) { + return $this->formatExcelTimestampValue(floatval($nodeValue)); + } else { + $nodeIntValue = intval($nodeValue); + return ($nodeIntValue == $nodeValue) ? $nodeIntValue : floatval($nodeValue); + } + } + + /** + * Returns a cell's PHP Date value, associated to the given timestamp. + * NOTE: The timestamp is a float representing the number of days since January 1st, 1900. + * + * @param float $nodeValue + * @return \DateTime|null The value associated with the cell or NULL if invalid date value + */ + protected function formatExcelTimestampValue($nodeValue) + { + // Fix for the erroneous leap year in Excel + if (ceil($nodeValue) > self::ERRONEOUS_EXCEL_LEAP_YEAR_DAY) { + --$nodeValue; + } + + // The value 1.0 represents 1900-01-01. Numbers below 1.0 are not valid Excel dates. + if ($nodeValue < 1.0) { + return null; + } + + // Do not use any unix timestamps for calculation to prevent + // issues with numbers exceeding 2^31. + $secondsRemainder = fmod($nodeValue, 1) * self::NUM_SECONDS_IN_ONE_DAY; + $secondsRemainder = round($secondsRemainder, 0); + + try { + $cellValue = \DateTime::createFromFormat('|Y-m-d', '1899-12-31'); + $cellValue->modify('+' . intval($nodeValue) . 'days'); + $cellValue->modify('+' . $secondsRemainder . 'seconds'); + + return $cellValue; + } catch (\Exception $e) { + return null; + } + } + + /** + * Returns the cell Boolean value from a specific node's Value. + * + * @param string $nodeValue + * @return bool The value associated with the cell + */ + protected function formatBooleanCellValue($nodeValue) + { + // !! is similar to boolval() + $cellValue = !!$nodeValue; + return $cellValue; + } + + /** + * Returns a cell's PHP Date value, associated to the given stored nodeValue. + * + * @param string $nodeValue + * @return \DateTime|null The value associated with the cell or NULL if invalid date value + */ + protected function formatDateCellValue($nodeValue) + { + // Mitigate thrown Exception on invalid date-time format (http://php.net/manual/en/datetime.construct.php) + try { + $cellValue = new \DateTime($nodeValue); + return $cellValue; + } catch (\Exception $e) { + return null; + } + } +} diff --git a/lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyFactory.php b/lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyFactory.php new file mode 100644 index 00000000000..8fffdb06cc1 --- /dev/null +++ b/lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyFactory.php @@ -0,0 +1,154 @@ + 20 * 600 ≈ 12KB + */ + const AMOUNT_MEMORY_NEEDED_PER_STRING_IN_KB = 12; + + /** + * To avoid running out of memory when extracting a huge number of shared strings, they can be saved to temporary files + * instead of in memory. Then, when accessing a string, the corresponding file contents will be loaded in memory + * and the string will be quickly retrieved. + * The performance bottleneck is not when creating these temporary files, but rather when loading their content. + * Because the contents of the last loaded file stays in memory until another file needs to be loaded, it works + * best when the indexes of the shared strings are sorted in the sheet data. + * 10,000 was chosen because it creates small files that are fast to be loaded in memory. + */ + const MAX_NUM_STRINGS_PER_TEMP_FILE = 10000; + + /** @var CachingStrategyFactory|null Singleton instance */ + protected static $instance = null; + + /** + * Private constructor for singleton + */ + private function __construct() + { + } + + /** + * Returns the singleton instance of the factory + * + * @return CachingStrategyFactory + */ + public static function getInstance() + { + if (self::$instance === null) { + self::$instance = new CachingStrategyFactory(); + } + + return self::$instance; + } + + /** + * Returns the best caching strategy, given the number of unique shared strings + * and the amount of memory available. + * + * @param int $sharedStringsUniqueCount Number of unique shared strings + * @param string|void $tempFolder Temporary folder where the temporary files to store shared strings will be stored + * @return CachingStrategyInterface The best caching strategy + */ + public function getBestCachingStrategy($sharedStringsUniqueCount, $tempFolder = null) + { + if ($this->isInMemoryStrategyUsageSafe($sharedStringsUniqueCount)) { + return new InMemoryStrategy($sharedStringsUniqueCount); + } else { + return new FileBasedStrategy($tempFolder, self::MAX_NUM_STRINGS_PER_TEMP_FILE); + } + } + + /** + * Returns whether it is safe to use in-memory caching, given the number of unique shared strings + * and the amount of memory available. + * + * @param int $sharedStringsUniqueCount Number of unique shared strings + * @return bool + */ + protected function isInMemoryStrategyUsageSafe($sharedStringsUniqueCount) + { + $memoryAvailable = $this->getMemoryLimitInKB(); + + if ($memoryAvailable === -1) { + // if cannot get memory limit or if memory limit set as unlimited, don't trust and play safe + return ($sharedStringsUniqueCount < self::MAX_NUM_STRINGS_PER_TEMP_FILE); + } else { + $memoryNeeded = $sharedStringsUniqueCount * self::AMOUNT_MEMORY_NEEDED_PER_STRING_IN_KB; + return ($memoryAvailable > $memoryNeeded); + } + } + + /** + * Returns the PHP "memory_limit" in Kilobytes + * + * @return float + */ + protected function getMemoryLimitInKB() + { + $memoryLimitFormatted = $this->getMemoryLimitFromIni(); + $memoryLimitFormatted = strtolower(trim($memoryLimitFormatted)); + + // No memory limit + if ($memoryLimitFormatted === '-1') { + return -1; + } + + if (preg_match('/(\d+)([bkmgt])b?/', $memoryLimitFormatted, $matches)) { + $amount = intval($matches[1]); + $unit = $matches[2]; + + switch ($unit) { + case 'b': return ($amount / 1024); + case 'k': return $amount; + case 'm': return ($amount * 1024); + case 'g': return ($amount * 1024 * 1024); + case 't': return ($amount * 1024 * 1024 * 1024); + } + } + + return -1; + } + + /** + * Returns the formatted "memory_limit" value + * + * @return string + */ + protected function getMemoryLimitFromIni() + { + return ini_get('memory_limit'); + } +} diff --git a/lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyInterface.php b/lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyInterface.php new file mode 100644 index 00000000000..631222a3210 --- /dev/null +++ b/lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyInterface.php @@ -0,0 +1,44 @@ +fileSystemHelper = new FileSystemHelper($rootTempFolder); + $this->tempFolder = $this->fileSystemHelper->createFolder($rootTempFolder, uniqid('sharedstrings')); + + $this->maxNumStringsPerTempFile = $maxNumStringsPerTempFile; + + $this->globalFunctionsHelper = new GlobalFunctionsHelper(); + $this->tempFilePointer = null; + } + + /** + * Adds the given string to the cache. + * + * @param string $sharedString The string to be added to the cache + * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file + * @return void + */ + public function addStringForIndex($sharedString, $sharedStringIndex) + { + $tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex); + + if (!$this->globalFunctionsHelper->file_exists($tempFilePath)) { + if ($this->tempFilePointer) { + $this->globalFunctionsHelper->fclose($this->tempFilePointer); + } + $this->tempFilePointer = $this->globalFunctionsHelper->fopen($tempFilePath, 'w'); + } + + // The shared string retrieval logic expects each cell data to be on one line only + // Encoding the line feed character allows to preserve this assumption + $lineFeedEncodedSharedString = $this->escapeLineFeed($sharedString); + + $this->globalFunctionsHelper->fwrite($this->tempFilePointer, $lineFeedEncodedSharedString . PHP_EOL); + } + + /** + * Returns the path for the temp file that should contain the string for the given index + * + * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file + * @return string The temp file path for the given index + */ + protected function getSharedStringTempFilePath($sharedStringIndex) + { + $numTempFile = intval($sharedStringIndex / $this->maxNumStringsPerTempFile); + return $this->tempFolder . '/sharedstrings' . $numTempFile; + } + + /** + * Closes the cache after the last shared string was added. + * This prevents any additional string from being added to the cache. + * + * @return void + */ + public function closeCache() + { + // close pointer to the last temp file that was written + if ($this->tempFilePointer) { + $this->globalFunctionsHelper->fclose($this->tempFilePointer); + } + } + + + /** + * Returns the string located at the given index from the cache. + * + * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file + * @return string The shared string at the given index + * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index + */ + public function getStringAtIndex($sharedStringIndex) + { + $tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex); + $indexInFile = $sharedStringIndex % $this->maxNumStringsPerTempFile; + + if (!$this->globalFunctionsHelper->file_exists($tempFilePath)) { + throw new SharedStringNotFoundException("Shared string temp file not found: $tempFilePath ; for index: $sharedStringIndex"); + } + + if ($this->inMemoryTempFilePath !== $tempFilePath) { + // free memory + unset($this->inMemoryTempFileContents); + + $this->inMemoryTempFileContents = explode(PHP_EOL, $this->globalFunctionsHelper->file_get_contents($tempFilePath)); + $this->inMemoryTempFilePath = $tempFilePath; + } + + $sharedString = null; + + // Using isset here because it is way faster than array_key_exists... + if (isset($this->inMemoryTempFileContents[$indexInFile])) { + $escapedSharedString = $this->inMemoryTempFileContents[$indexInFile]; + $sharedString = $this->unescapeLineFeed($escapedSharedString); + } + + if ($sharedString === null) { + throw new SharedStringNotFoundException("Shared string not found for index: $sharedStringIndex"); + } + + return rtrim($sharedString, PHP_EOL); + } + + /** + * Escapes the line feed characters (\n) + * + * @param string $unescapedString + * @return string + */ + private function escapeLineFeed($unescapedString) + { + return str_replace("\n", self::ESCAPED_LINE_FEED_CHARACTER, $unescapedString); + } + + /** + * Unescapes the line feed characters (\n) + * + * @param string $escapedString + * @return string + */ + private function unescapeLineFeed($escapedString) + { + return str_replace(self::ESCAPED_LINE_FEED_CHARACTER, "\n", $escapedString); + } + + /** + * Destroys the cache, freeing memory and removing any created artifacts + * + * @return void + */ + public function clearCache() + { + if ($this->tempFolder) { + $this->fileSystemHelper->deleteFolderRecursively($this->tempFolder); + } + } +} diff --git a/lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/InMemoryStrategy.php b/lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/InMemoryStrategy.php new file mode 100644 index 00000000000..c6a5321a554 --- /dev/null +++ b/lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/InMemoryStrategy.php @@ -0,0 +1,83 @@ +inMemoryCache = new \SplFixedArray($sharedStringsUniqueCount); + $this->isCacheClosed = false; + } + + /** + * Adds the given string to the cache. + * + * @param string $sharedString The string to be added to the cache + * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file + * @return void + */ + public function addStringForIndex($sharedString, $sharedStringIndex) + { + if (!$this->isCacheClosed) { + $this->inMemoryCache->offsetSet($sharedStringIndex, $sharedString); + } + } + + /** + * Closes the cache after the last shared string was added. + * This prevents any additional string from being added to the cache. + * + * @return void + */ + public function closeCache() + { + $this->isCacheClosed = true; + } + + /** + * Returns the string located at the given index from the cache. + * + * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file + * @return string The shared string at the given index + * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index + */ + public function getStringAtIndex($sharedStringIndex) + { + try { + return $this->inMemoryCache->offsetGet($sharedStringIndex); + } catch (\RuntimeException $e) { + throw new SharedStringNotFoundException("Shared string not found for index: $sharedStringIndex"); + } + } + + /** + * Destroys the cache, freeing memory and removing any created artifacts + * + * @return void + */ + public function clearCache() + { + unset($this->inMemoryCache); + $this->isCacheClosed = false; + } +} diff --git a/lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php b/lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php new file mode 100644 index 00000000000..6aafb52f74a --- /dev/null +++ b/lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php @@ -0,0 +1,248 @@ +filePath = $filePath; + $this->tempFolder = $tempFolder; + } + + /** + * Returns whether the XLSX file contains a shared strings XML file + * + * @return bool + */ + public function hasSharedStrings() + { + $hasSharedStrings = false; + $zip = new \ZipArchive(); + + if ($zip->open($this->filePath) === true) { + $hasSharedStrings = ($zip->locateName(self::SHARED_STRINGS_XML_FILE_PATH) !== false); + $zip->close(); + } + + return $hasSharedStrings; + } + + /** + * Builds an in-memory array containing all the shared strings of the sheet. + * All the strings are stored in a XML file, located at 'xl/sharedStrings.xml'. + * It is then accessed by the sheet data, via the string index in the built table. + * + * More documentation available here: http://msdn.microsoft.com/en-us/library/office/gg278314.aspx + * + * The XML file can be really big with sheets containing a lot of data. That is why + * we need to use a XML reader that provides streaming like the XMLReader library. + * Please note that SimpleXML does not provide such a functionality but since it is faster + * and more handy to parse few XML nodes, it is used in combination with XMLReader for that purpose. + * + * @return void + * @throws \Box\Spout\Common\Exception\IOException If sharedStrings.xml can't be read + */ + public function extractSharedStrings() + { + $xmlReader = new XMLReader(); + $sharedStringIndex = 0; + /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ + $escaper = new \Box\Spout\Common\Escaper\XLSX(); + + $sharedStringsFilePath = $this->getSharedStringsFilePath(); + if ($xmlReader->open($sharedStringsFilePath) === false) { + throw new IOException('Could not open "' . self::SHARED_STRINGS_XML_FILE_PATH . '".'); + } + + try { + $sharedStringsUniqueCount = $this->getSharedStringsUniqueCount($xmlReader); + $this->cachingStrategy = $this->getBestSharedStringsCachingStrategy($sharedStringsUniqueCount); + + $xmlReader->readUntilNodeFound('si'); + + while ($xmlReader->name === 'si') { + $node = $this->getSimpleXmlElementNodeFromXMLReader($xmlReader); + $node->registerXPathNamespace('ns', self::MAIN_NAMESPACE_FOR_SHARED_STRINGS_XML); + + // removes nodes that should not be read, like the pronunciation of the Kanji characters + $cleanNode = $this->removeSuperfluousTextNodes($node); + + // find all text nodes 't'; there can be multiple if the cell contains formatting + $textNodes = $cleanNode->xpath('//ns:t'); + + $textValue = ''; + foreach ($textNodes as $textNode) { + if ($this->shouldPreserveWhitespace($textNode)) { + $textValue .= $textNode->__toString(); + } else { + $textValue .= trim($textNode->__toString()); + } + } + + $unescapedTextValue = $escaper->unescape($textValue); + $this->cachingStrategy->addStringForIndex($unescapedTextValue, $sharedStringIndex); + + $sharedStringIndex++; + + // jump to the next 'si' tag + $xmlReader->next('si'); + } + + } catch (XMLProcessingException $exception) { + throw new IOException("The sharedStrings.xml file is invalid and cannot be read. [{$exception->getMessage()}]"); + } + + $this->cachingStrategy->closeCache(); + + $xmlReader->close(); + } + + /** + * @return string The path to the shared strings XML file + */ + protected function getSharedStringsFilePath() + { + return 'zip://' . $this->filePath . '#' . self::SHARED_STRINGS_XML_FILE_PATH; + } + + /** + * Returns the shared strings unique count, as specified in tag. + * + * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader instance + * @return int Number of unique shared strings in the sharedStrings.xml file + * @throws \Box\Spout\Common\Exception\IOException If sharedStrings.xml is invalid and can't be read + */ + protected function getSharedStringsUniqueCount($xmlReader) + { + $xmlReader->next('sst'); + + // Iterate over the "sst" elements to get the actual "sst ELEMENT" (skips any DOCTYPE) + while ($xmlReader->name === 'sst' && $xmlReader->nodeType !== XMLReader::ELEMENT) { + $xmlReader->read(); + } + + return intval($xmlReader->getAttribute('uniqueCount')); + } + + /** + * Returns the best shared strings caching strategy. + * + * @param int $sharedStringsUniqueCount + * @return CachingStrategyInterface + */ + protected function getBestSharedStringsCachingStrategy($sharedStringsUniqueCount) + { + return CachingStrategyFactory::getInstance() + ->getBestCachingStrategy($sharedStringsUniqueCount, $this->tempFolder); + } + + /** + * Returns a SimpleXMLElement node from the current node in the given XMLReader instance. + * This is to simplify the parsing of the subtree. + * + * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader + * @return \Box\Spout\Reader\Wrapper\SimpleXMLElement + * @throws \Box\Spout\Common\Exception\IOException If the current node cannot be read + */ + protected function getSimpleXmlElementNodeFromXMLReader($xmlReader) + { + $node = null; + try { + $node = new SimpleXMLElement($xmlReader->readOuterXml()); + } catch (XMLProcessingException $exception) { + throw new IOException("The sharedStrings.xml file contains unreadable data [{$exception->getMessage()}]."); + } + + return $node; + } + + /** + * Removes nodes that should not be read, like the pronunciation of the Kanji characters. + * By keeping them, their text content would be added to the read string. + * + * @param \Box\Spout\Reader\Wrapper\SimpleXMLElement $parentNode Parent node that may contain nodes to remove + * @return \Box\Spout\Reader\Wrapper\SimpleXMLElement Cleaned parent node + */ + protected function removeSuperfluousTextNodes($parentNode) + { + $tagsToRemove = [ + 'rPh', // Pronunciation of the text + ]; + + foreach ($tagsToRemove as $tagToRemove) { + $xpath = '//ns:' . $tagToRemove; + $parentNode->removeNodesMatchingXPath($xpath); + } + + return $parentNode; + } + + /** + * If the text node has the attribute 'xml:space="preserve"', then preserve whitespace. + * + * @param \Box\Spout\Reader\Wrapper\SimpleXMLElement $textNode The text node element () whitespace may be preserved + * @return bool Whether whitespace should be preserved + */ + protected function shouldPreserveWhitespace($textNode) + { + $spaceValue = $textNode->getAttribute('space', 'xml'); + return ($spaceValue === 'preserve'); + } + + /** + * Returns the shared string at the given index, using the previously chosen caching strategy. + * + * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file + * @return string The shared string at the given index + * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index + */ + public function getStringAtIndex($sharedStringIndex) + { + return $this->cachingStrategy->getStringAtIndex($sharedStringIndex); + } + + /** + * Destroys the cache, freeing memory and removing any created artifacts + * + * @return void + */ + public function cleanup() + { + if ($this->cachingStrategy) { + $this->cachingStrategy->clearCache(); + } + } +} diff --git a/lib/spout/src/Spout/Reader/XLSX/Helper/SheetHelper.php b/lib/spout/src/Spout/Reader/XLSX/Helper/SheetHelper.php new file mode 100644 index 00000000000..34005092bd1 --- /dev/null +++ b/lib/spout/src/Spout/Reader/XLSX/Helper/SheetHelper.php @@ -0,0 +1,180 @@ +filePath = $filePath; + $this->sharedStringsHelper = $sharedStringsHelper; + $this->globalFunctionsHelper = $globalFunctionsHelper; + } + + /** + * Returns the sheets metadata of the file located at the previously given file path. + * The paths to the sheets' data are read from the [Content_Types].xml file. + * + * @return Sheet[] Sheets within the XLSX file + */ + public function getSheets() + { + $sheets = []; + + $contentTypesAsXMLElement = $this->getFileAsXMLElementWithNamespace( + self::CONTENT_TYPES_XML_FILE_PATH, + self::MAIN_NAMESPACE_FOR_CONTENT_TYPES_XML + ); + + // find all nodes defining a sheet + $sheetNodes = $contentTypesAsXMLElement->xpath('//ns:Override[@ContentType="' . self::OVERRIDE_CONTENT_TYPES_ATTRIBUTE . '"]'); + $numSheetNodes = count($sheetNodes); + + for ($i = 0; $i < $numSheetNodes; $i++) { + $sheetNode = $sheetNodes[$i]; + $sheetDataXMLFilePath = $sheetNode->getAttribute('PartName'); + + $sheets[] = $this->getSheetFromXML($sheetDataXMLFilePath); + } + + // make sure the sheets are sorted by index + // (as the sheets are not necessarily in this order in the XML file) + usort($sheets, function ($sheet1, $sheet2) { + return ($sheet1->getIndex() - $sheet2->getIndex()); + }); + + return $sheets; + } + + /** + * Returns an instance of a sheet, given the path of its data XML file. + * We first look at "xl/_rels/workbook.xml.rels" to find the relationship ID of the sheet. + * Then we look at "xl/worbook.xml" to find the sheet entry associated to the found ID. + * The entry contains the ID and name of the sheet. + * + * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml + * @return \Box\Spout\Reader\XLSX\Sheet Sheet instance + */ + protected function getSheetFromXML($sheetDataXMLFilePath) + { + // In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml" + // In workbook.xml.rels, it is only "worksheets/sheet1.xml" + $sheetDataXMLFilePathInWorkbookXMLRels = ltrim($sheetDataXMLFilePath, '/xl/'); + + // find the node associated to the given file path + $workbookXMLResElement = $this->getWorkbookXMLRelsAsXMLElement(); + $relationshipNodes = $workbookXMLResElement->xpath('//ns:Relationship[@Target="' . $sheetDataXMLFilePathInWorkbookXMLRels . '"]'); + $relationshipNode = $relationshipNodes[0]; + + $relationshipSheetId = $relationshipNode->getAttribute('Id'); + + $workbookXMLElement = $this->getWorkbookXMLAsXMLElement(); + $sheetNodes = $workbookXMLElement->xpath('//ns:sheet[@r:id="' . $relationshipSheetId . '"]'); + $sheetNode = $sheetNodes[0]; + + $escapedSheetName = $sheetNode->getAttribute('name'); + $sheetIdOneBased = $sheetNode->getAttribute('sheetId'); + $sheetIndexZeroBased = $sheetIdOneBased - 1; + + /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ + $escaper = new \Box\Spout\Common\Escaper\XLSX(); + $sheetName = $escaper->unescape($escapedSheetName); + + return new Sheet($this->filePath, $sheetDataXMLFilePath, $this->sharedStringsHelper, $sheetIndexZeroBased, $sheetName); + } + + /** + * Returns a representation of the workbook.xml.rels file, ready to be parsed. + * The returned value is cached. + * + * @return \Box\Spout\Reader\Wrapper\SimpleXMLElement XML element representating the workbook.xml.rels file + */ + protected function getWorkbookXMLRelsAsXMLElement() + { + if (!$this->workbookXMLRelsAsXMLElement) { + $this->workbookXMLRelsAsXMLElement = $this->getFileAsXMLElementWithNamespace( + self::WORKBOOK_XML_RELS_FILE_PATH, + self::MAIN_NAMESPACE_FOR_WORKBOOK_XML_RELS + ); + } + + return $this->workbookXMLRelsAsXMLElement; + } + + /** + * Returns a representation of the workbook.xml file, ready to be parsed. + * The returned value is cached. + * + * @return \Box\Spout\Reader\Wrapper\SimpleXMLElement XML element representating the workbook.xml.rels file + */ + protected function getWorkbookXMLAsXMLElement() + { + if (!$this->workbookXMLAsXMLElement) { + $this->workbookXMLAsXMLElement = $this->getFileAsXMLElementWithNamespace( + self::WORKBOOK_XML_FILE_PATH, + self::MAIN_NAMESPACE_FOR_WORKBOOK_XML + ); + } + + return $this->workbookXMLAsXMLElement; + } + + /** + * Loads the contents of the given file in an XML parser and register the given XPath namespace. + * + * @param string $xmlFilePath The path of the XML file inside the XLSX file + * @param string $mainNamespace The main XPath namespace to register + * @return \Box\Spout\Reader\Wrapper\SimpleXMLElement The XML element representing the file + */ + protected function getFileAsXMLElementWithNamespace($xmlFilePath, $mainNamespace) + { + $xmlContents = $this->globalFunctionsHelper->file_get_contents('zip://' . $this->filePath . '#' . $xmlFilePath); + + $xmlElement = new SimpleXMLElement($xmlContents); + $xmlElement->registerXPathNamespace('ns', $mainNamespace); + + return $xmlElement; + } +} diff --git a/lib/spout/src/Spout/Reader/XLSX/Helper/StyleHelper.php b/lib/spout/src/Spout/Reader/XLSX/Helper/StyleHelper.php new file mode 100644 index 00000000000..403d6479953 --- /dev/null +++ b/lib/spout/src/Spout/Reader/XLSX/Helper/StyleHelper.php @@ -0,0 +1,226 @@ + FORMAT_CODE */ + protected $customNumberFormats; + + /** @var array Array containing a mapping STYLE_ID => [STYLE_ATTRIBUTES] */ + protected $stylesAttributes; + + /** + * @param string $filePath Path of the XLSX file being read + */ + public function __construct($filePath) + { + $this->filePath = $filePath; + } + + /** + * Reads the styles.xml file and extract the relevant information from the file. + * + * @return void + */ + protected function extractRelevantInfo() + { + $this->customNumberFormats = []; + $this->stylesAttributes = []; + + $stylesXmlFilePath = $this->filePath .'#' . self::STYLES_XML_FILE_PATH; + $xmlReader = new XMLReader(); + + if ($xmlReader->open('zip://' . $stylesXmlFilePath)) { + while ($xmlReader->read()) { + if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMTS)) { + $numFmtsNode = new SimpleXMLElement($xmlReader->readOuterXml()); + $this->extractNumberFormats($numFmtsNode); + + } else if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL_XFS)) { + $cellXfsNode = new SimpleXMLElement($xmlReader->readOuterXml()); + $this->extractStyleAttributes($cellXfsNode); + } + } + + $xmlReader->close(); + } + } + + /** + * Extracts number formats from the "numFmt" nodes. + * For simplicity, the styles attributes are kept in memory. This is possible thanks + * to the reuse of formats. So 1 million cells should not use 1 million formats. + * + * @param SimpleXMLElement $numFmtsNode The "numFmts" node + * @return void + */ + protected function extractNumberFormats($numFmtsNode) + { + foreach ($numFmtsNode->children() as $numFmtNode) { + $numFmtId = intval($numFmtNode->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID)); + $formatCode = $numFmtNode->getAttribute(self::XML_ATTRIBUTE_FORMAT_CODE); + $this->customNumberFormats[$numFmtId] = $formatCode; + } + } + + /** + * Extracts style attributes from the "xf" nodes, inside the "cellXfs" section. + * For simplicity, the styles attributes are kept in memory. This is possible thanks + * to the reuse of styles. So 1 million cells should not use 1 million styles. + * + * @param SimpleXMLElement $cellXfsNode The "cellXfs" node + * @return void + */ + protected function extractStyleAttributes($cellXfsNode) + { + foreach ($cellXfsNode->children() as $xfNode) { + $this->stylesAttributes[] = [ + self::XML_ATTRIBUTE_NUM_FMT_ID => intval($xfNode->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID)), + self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT => !!($xfNode->getAttribute(self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT)), + ]; + } + } + + /** + * @return array The custom number formats + */ + protected function getCustomNumberFormats() + { + if (!isset($this->customNumberFormats)) { + $this->extractRelevantInfo(); + } + + return $this->customNumberFormats; + } + + /** + * @return array The styles attributes + */ + protected function getStylesAttributes() + { + if (!isset($this->stylesAttributes)) { + $this->extractRelevantInfo(); + } + + return $this->stylesAttributes; + } + + /** + * Returns whether the style with the given ID should consider + * numeric values as timestamps and format the cell as a date. + * + * @param int $styleId Zero-based style ID + * @return bool Whether the cell with the given cell should display a date instead of a numeric value + */ + public function shouldFormatNumericValueAsDate($styleId) + { + $stylesAttributes = $this->getStylesAttributes(); + + // Default style (0) does not format numeric values as timestamps. Only custom styles do. + // Also if the style ID does not exist in the styles.xml file, format as numeric value. + // Using isset here because it is way faster than array_key_exists... + if ($styleId === self::DEFAULT_STYLE_ID || !isset($stylesAttributes[$styleId])) { + return false; + } + + $styleAttributes = $stylesAttributes[$styleId]; + + $applyNumberFormat = $styleAttributes[self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT]; + if (!$applyNumberFormat) { + return false; + } + + $numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID]; + return $this->doesNumFmtIdIndicateDate($numFmtId); + } + + /** + * @param int $numFmtId + * @return bool Whether the number format ID indicates that the number is a timestamp + */ + protected function doesNumFmtIdIndicateDate($numFmtId) + { + return ( + $this->isNumFmtIdBuiltInDateFormat($numFmtId) || + $this->isNumFmtIdCustomDateFormat($numFmtId) + ); + } + + /** + * @param int $numFmtId + * @return bool Whether the number format ID indicates that the number is a timestamp + */ + protected function isNumFmtIdBuiltInDateFormat($numFmtId) + { + $builtInDateFormatIds = [14, 15, 16, 17, 18, 19, 20, 21, 22, 45, 46, 47]; + return in_array($numFmtId, $builtInDateFormatIds); + } + + /** + * @param int $numFmtId + * @return bool Whether the number format ID indicates that the number is a timestamp + */ + protected function isNumFmtIdCustomDateFormat($numFmtId) + { + $customNumberFormats = $this->getCustomNumberFormats(); + + // Using isset here because it is way faster than array_key_exists... + if (!isset($customNumberFormats[$numFmtId])) { + return false; + } + + $customNumberFormat = $customNumberFormats[$numFmtId]; + + // Remove extra formatting (what's between [ ], the brackets should not be preceded by a "\") + $pattern = '((?tempFolder = $tempFolder; + return $this; + } + + /** + * Returns whether stream wrappers are supported + * + * @return bool + */ + protected function doesSupportStreamWrapper() + { + return false; + } + + /** + * Opens the file at the given file path to make it ready to be read. + * It also parses the sharedStrings.xml file to get all the shared strings available in memory + * and fetches all the available sheets. + * + * @param string $filePath Path of the file to be read + * @return void + * @throws \Box\Spout\Common\Exception\IOException If the file at the given path or its content cannot be read + * @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file + */ + protected function openReader($filePath) + { + $this->zip = new \ZipArchive(); + + if ($this->zip->open($filePath) === true) { + $this->sharedStringsHelper = new SharedStringsHelper($filePath, $this->tempFolder); + + if ($this->sharedStringsHelper->hasSharedStrings()) { + // Extracts all the strings from the sheets for easy access in the future + $this->sharedStringsHelper->extractSharedStrings(); + } + + $this->sheetIterator = new SheetIterator($filePath, $this->sharedStringsHelper, $this->globalFunctionsHelper); + } else { + throw new IOException("Could not open $filePath for reading."); + } + } + + /** + * Returns an iterator to iterate over sheets. + * + * @return SheetIterator To iterate over sheets + */ + public function getConcreteSheetIterator() + { + return $this->sheetIterator; + } + + /** + * Closes the reader. To be used after reading the file. + * + * @return void + */ + protected function closeReader() + { + if ($this->zip) { + $this->zip->close(); + } + + if ($this->sharedStringsHelper) { + $this->sharedStringsHelper->cleanup(); + } + } +} diff --git a/lib/spout/src/Spout/Reader/XLSX/RowIterator.php b/lib/spout/src/Spout/Reader/XLSX/RowIterator.php new file mode 100644 index 00000000000..d1913bdacf9 --- /dev/null +++ b/lib/spout/src/Spout/Reader/XLSX/RowIterator.php @@ -0,0 +1,227 @@ +filePath = $filePath; + $this->sheetDataXMLFilePath = $this->normalizeSheetDataXMLFilePath($sheetDataXMLFilePath); + + $this->xmlReader = new XMLReader(); + + $this->styleHelper = new StyleHelper($filePath); + $this->cellValueFormatter = new CellValueFormatter($sharedStringsHelper, $this->styleHelper); + } + + /** + * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml + * @return string Path of the XML file containing the sheet data, + * without the leading slash. + */ + protected function normalizeSheetDataXMLFilePath($sheetDataXMLFilePath) + { + return ltrim($sheetDataXMLFilePath, '/'); + } + + /** + * Rewind the Iterator to the first element. + * Initializes the XMLReader object that reads the associated sheet data. + * The XMLReader is configured to be safe from billion laughs attack. + * @link http://php.net/manual/en/iterator.rewind.php + * + * @return void + * @throws \Box\Spout\Common\Exception\IOException If the sheet data XML cannot be read + */ + public function rewind() + { + $this->xmlReader->close(); + + $sheetDataFilePath = 'zip://' . $this->filePath . '#' . $this->sheetDataXMLFilePath; + if ($this->xmlReader->open($sheetDataFilePath) === false) { + throw new IOException("Could not open \"{$this->sheetDataXMLFilePath}\"."); + } + + $this->numReadRows = 0; + $this->rowDataBuffer = null; + $this->hasReachedEndOfFile = false; + $this->numColumns = 0; + + $this->next(); + } + + /** + * Checks if current position is valid + * @link http://php.net/manual/en/iterator.valid.php + * + * @return boolean + */ + public function valid() + { + return (!$this->hasReachedEndOfFile); + } + + /** + * Move forward to next element. Empty rows will be skipped. + * @link http://php.net/manual/en/iterator.next.php + * + * @return void + * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found + * @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML + */ + public function next() + { + $rowData = []; + + try { + while ($this->xmlReader->read()) { + if ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_DIMENSION)) { + // Read dimensions of the sheet + $dimensionRef = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_REF); // returns 'A1:M13' for instance (or 'A1' for empty sheet) + if (preg_match('/[A-Z\d]+:([A-Z\d]+)/', $dimensionRef, $matches)) { + $lastCellIndex = $matches[1]; + $this->numColumns = CellHelper::getColumnIndexFromCellIndex($lastCellIndex) + 1; + } + + } else if ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_ROW)) { + // Start of the row description + + // Read spans info if present + $numberOfColumnsForRow = $this->numColumns; + $spans = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_SPANS); // returns '1:5' for instance + if ($spans) { + list(, $numberOfColumnsForRow) = explode(':', $spans); + $numberOfColumnsForRow = intval($numberOfColumnsForRow); + } + $rowData = ($numberOfColumnsForRow !== 0) ? array_fill(0, $numberOfColumnsForRow, '') : []; + + } else if ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL)) { + // Start of a cell description + $currentCellIndex = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_CELL_INDEX); + $currentColumnIndex = CellHelper::getColumnIndexFromCellIndex($currentCellIndex); + + $node = $this->xmlReader->expand(); + $rowData[$currentColumnIndex] = $this->getCellValue($node); + + } else if ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_ROW)) { + // End of the row description + // If needed, we fill the empty cells + $rowData = ($this->numColumns !== 0) ? $rowData : CellHelper::fillMissingArrayIndexes($rowData); + $this->numReadRows++; + break; + + } else if ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_WORKSHEET)) { + // The closing "" marks the end of the file + $this->hasReachedEndOfFile = true; + break; + } + } + + } catch (XMLProcessingException $exception) { + throw new IOException("The {$this->sheetDataXMLFilePath} file cannot be read. [{$exception->getMessage()}]"); + } + + $this->rowDataBuffer = $rowData; + } + + /** + * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node. + * + * @param \DOMNode $node + * @return string|int|float|bool|\DateTime|null The value associated with the cell (null when the cell has an error) + */ + protected function getCellValue($node) + { + return $this->cellValueFormatter->extractAndFormatNodeValue($node); + } + + /** + * Return the current element, from the buffer. + * @link http://php.net/manual/en/iterator.current.php + * + * @return array|null + */ + public function current() + { + return $this->rowDataBuffer; + } + + /** + * Return the key of the current element + * @link http://php.net/manual/en/iterator.key.php + * + * @return int + */ + public function key() + { + return $this->numReadRows; + } + + + /** + * Cleans up what was created to iterate over the object. + * + * @return void + */ + public function end() + { + $this->xmlReader->close(); + } +} diff --git a/lib/spout/src/Spout/Reader/XLSX/Sheet.php b/lib/spout/src/Spout/Reader/XLSX/Sheet.php new file mode 100644 index 00000000000..85a4dc9c9e7 --- /dev/null +++ b/lib/spout/src/Spout/Reader/XLSX/Sheet.php @@ -0,0 +1,64 @@ +rowIterator = new RowIterator($filePath, $sheetDataXMLFilePath, $sharedStringsHelper); + $this->index = $sheetIndex; + $this->name = $sheetName; + } + + /** + * @api + * @return \Box\Spout\Reader\XLSX\RowIterator + */ + public function getRowIterator() + { + return $this->rowIterator; + } + + /** + * @api + * @return int Index of the sheet, based on order in the workbook (zero-based) + */ + public function getIndex() + { + return $this->index; + } + + /** + * @api + * @return string Name of the sheet + */ + public function getName() + { + return $this->name; + } +} diff --git a/lib/spout/src/Spout/Reader/XLSX/SheetIterator.php b/lib/spout/src/Spout/Reader/XLSX/SheetIterator.php new file mode 100644 index 00000000000..7b3d3dd9083 --- /dev/null +++ b/lib/spout/src/Spout/Reader/XLSX/SheetIterator.php @@ -0,0 +1,113 @@ +sheets = $sheetHelper->getSheets(); + + if (count($this->sheets) === 0) { + throw new NoSheetsFoundException('The file must contain at least one sheet.'); + } + } + + /** + * Rewind the Iterator to the first element + * @link http://php.net/manual/en/iterator.rewind.php + * + * @return void + */ + public function rewind() + { + $this->currentSheetIndex = 0; + } + + /** + * Checks if current position is valid + * @link http://php.net/manual/en/iterator.valid.php + * + * @return boolean + */ + public function valid() + { + return ($this->currentSheetIndex < count($this->sheets)); + } + + /** + * Move forward to next element + * @link http://php.net/manual/en/iterator.next.php + * + * @return void + */ + public function next() + { + // Using isset here because it is way faster than array_key_exists... + if (isset($this->sheets[$this->currentSheetIndex])) { + $currentSheet = $this->sheets[$this->currentSheetIndex]; + $currentSheet->getRowIterator()->end(); + + $this->currentSheetIndex++; + } + } + + /** + * Return the current element + * @link http://php.net/manual/en/iterator.current.php + * + * @return \Box\Spout\Reader\XLSX\Sheet + */ + public function current() + { + return $this->sheets[$this->currentSheetIndex]; + } + + /** + * Return the key of the current element + * @link http://php.net/manual/en/iterator.key.php + * + * @return int + */ + public function key() + { + return $this->currentSheetIndex + 1; + } + + /** + * Cleans up what was created to iterate over the object. + * + * @return void + */ + public function end() + { + // make sure we are not leaking memory in case the iteration stopped before the end + foreach ($this->sheets as $sheet) { + $sheet->getRowIterator()->end(); + } + } +} diff --git a/lib/spout/src/Spout/Writer/AbstractMultiSheetsWriter.php b/lib/spout/src/Spout/Writer/AbstractMultiSheetsWriter.php new file mode 100644 index 00000000000..d11966055ad --- /dev/null +++ b/lib/spout/src/Spout/Writer/AbstractMultiSheetsWriter.php @@ -0,0 +1,119 @@ +throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); + + $this->shouldCreateNewSheetsAutomatically = $shouldCreateNewSheetsAutomatically; + return $this; + } + + /** + * Returns all the workbook's sheets + * + * @api + * @return Common\Sheet[] All the workbook's sheets + * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet + */ + public function getSheets() + { + $this->throwIfBookIsNotAvailable(); + + $externalSheets = []; + $worksheets = $this->getWorkbook()->getWorksheets(); + + /** @var Common\Internal\WorksheetInterface $worksheet */ + foreach ($worksheets as $worksheet) { + $externalSheets[] = $worksheet->getExternalSheet(); + } + + return $externalSheets; + } + + /** + * Creates a new sheet and make it the current sheet. The data will now be written to this sheet. + * + * @api + * @return Common\Sheet The created sheet + * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet + */ + public function addNewSheetAndMakeItCurrent() + { + $this->throwIfBookIsNotAvailable(); + $worksheet = $this->getWorkbook()->addNewSheetAndMakeItCurrent(); + + return $worksheet->getExternalSheet(); + } + + /** + * Returns the current sheet + * + * @api + * @return Common\Sheet The current sheet + * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet + */ + public function getCurrentSheet() + { + $this->throwIfBookIsNotAvailable(); + return $this->getWorkbook()->getCurrentWorksheet()->getExternalSheet(); + } + + /** + * Sets the given sheet as the current one. New data will be written to this sheet. + * The writing will resume where it stopped (i.e. data won't be truncated). + * + * @api + * @param Common\Sheet $sheet The sheet to set as current + * @return void + * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet + * @throws \Box\Spout\Writer\Exception\SheetNotFoundException If the given sheet does not exist in the workbook + */ + public function setCurrentSheet($sheet) + { + $this->throwIfBookIsNotAvailable(); + $this->getWorkbook()->setCurrentSheet($sheet); + } + + /** + * Checks if the book has been created. Throws an exception if not created yet. + * + * @return void + * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the book is not created yet + */ + protected function throwIfBookIsNotAvailable() + { + if (!$this->getWorkbook()) { + throw new WriterNotOpenedException('The writer must be opened before performing this action.'); + } + } +} + diff --git a/lib/spout/src/Spout/Writer/AbstractWriter.php b/lib/spout/src/Spout/Writer/AbstractWriter.php new file mode 100644 index 00000000000..effa45b0758 --- /dev/null +++ b/lib/spout/src/Spout/Writer/AbstractWriter.php @@ -0,0 +1,330 @@ +defaultRowStyle = $this->getDefaultRowStyle(); + $this->resetRowStyleToDefault(); + } + + /** + * @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper + * @return AbstractWriter + */ + public function setGlobalFunctionsHelper($globalFunctionsHelper) + { + $this->globalFunctionsHelper = $globalFunctionsHelper; + return $this; + } + + /** + * Inits the writer and opens it to accept data. + * By using this method, the data will be written to a file. + * + * @api + * @param string $outputFilePath Path of the output file that will contain the data + * @return AbstractWriter + * @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened or if the given path is not writable + */ + public function openToFile($outputFilePath) + { + $this->outputFilePath = $outputFilePath; + + $this->filePointer = $this->globalFunctionsHelper->fopen($this->outputFilePath, 'wb+'); + $this->throwIfFilePointerIsNotAvailable(); + + $this->openWriter(); + $this->isWriterOpened = true; + + return $this; + } + + /** + * Inits the writer and opens it to accept data. + * By using this method, the data will be outputted directly to the browser. + * + * @codeCoverageIgnore + * + * @api + * @param string $outputFileName Name of the output file that will contain the data. If a path is passed in, only the file name will be kept + * @return AbstractWriter + * @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened + */ + public function openToBrowser($outputFileName) + { + $this->outputFilePath = $this->globalFunctionsHelper->basename($outputFileName); + + $this->filePointer = $this->globalFunctionsHelper->fopen('php://output', 'w'); + $this->throwIfFilePointerIsNotAvailable(); + + // Set headers + $this->globalFunctionsHelper->header('Content-Type: ' . static::$headerContentType); + $this->globalFunctionsHelper->header('Content-Disposition: attachment; filename="' . $this->outputFilePath . '"'); + + /* + * When forcing the download of a file over SSL,IE8 and lower browsers fail + * if the Cache-Control and Pragma headers are not set. + * + * @see http://support.microsoft.com/KB/323308 + * @see https://github.com/liuggio/ExcelBundle/issues/45 + */ + $this->globalFunctionsHelper->header('Cache-Control: max-age=0'); + $this->globalFunctionsHelper->header('Pragma: public'); + + $this->openWriter(); + $this->isWriterOpened = true; + + return $this; + } + + /** + * Checks if the pointer to the file/stream to write to is available. + * Will throw an exception if not available. + * + * @return void + * @throws \Box\Spout\Common\Exception\IOException If the pointer is not available + */ + protected function throwIfFilePointerIsNotAvailable() + { + if (!$this->filePointer) { + throw new IOException('File pointer has not be opened'); + } + } + + /** + * Checks if the writer has already been opened, since some actions must be done before it gets opened. + * Throws an exception if already opened. + * + * @param string $message Error message + * @return void + * @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened and must not be. + */ + protected function throwIfWriterAlreadyOpened($message) + { + if ($this->isWriterOpened) { + throw new WriterAlreadyOpenedException($message); + } + } + + /** + * Write given data to the output. New data will be appended to end of stream. + * + * @param array $dataRow Array containing data to be streamed. + * If empty, no data is added (i.e. not even as a blank row) + * Example: $dataRow = ['data1', 1234, null, '', 'data5', false]; + * @api + * @return AbstractWriter + * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer + * @throws \Box\Spout\Common\Exception\IOException If unable to write data + */ + public function addRow(array $dataRow) + { + if ($this->isWriterOpened) { + // empty $dataRow should not add an empty line + if (!empty($dataRow)) { + $this->addRowToWriter($dataRow, $this->rowStyle); + } + } else { + throw new WriterNotOpenedException('The writer needs to be opened before adding row.'); + } + + return $this; + } + + /** + * Write given data to the output and apply the given style. + * @see addRow + * + * @api + * @param array $dataRow Array of array containing data to be streamed. + * @param Style\Style $style Style to be applied to the row. + * @return AbstractWriter + * @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid + * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer + * @throws \Box\Spout\Common\Exception\IOException If unable to write data + */ + public function addRowWithStyle(array $dataRow, $style) + { + if (!$style instanceof Style\Style) { + throw new InvalidArgumentException('The "$style" argument must be a Style instance and cannot be NULL.'); + } + + $this->setRowStyle($style); + $this->addRow($dataRow); + $this->resetRowStyleToDefault(); + + return $this; + } + + /** + * Write given data to the output. New data will be appended to end of stream. + * + * @api + * @param array $dataRows Array of array containing data to be streamed. + * If a row is empty, it won't be added (i.e. not even as a blank row) + * Example: $dataRows = [ + * ['data11', 12, , '', 'data13'], + * ['data21', 'data22', null, false], + * ]; + * @return AbstractWriter + * @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid + * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer + * @throws \Box\Spout\Common\Exception\IOException If unable to write data + */ + public function addRows(array $dataRows) + { + if (!empty($dataRows)) { + if (!is_array($dataRows[0])) { + throw new InvalidArgumentException('The input should be an array of arrays'); + } + + foreach ($dataRows as $dataRow) { + $this->addRow($dataRow); + } + } + + return $this; + } + + /** + * Write given data to the output and apply the given style. + * @see addRows + * + * @api + * @param array $dataRows Array of array containing data to be streamed. + * @param Style\Style $style Style to be applied to the rows. + * @return AbstractWriter + * @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid + * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer + * @throws \Box\Spout\Common\Exception\IOException If unable to write data + */ + public function addRowsWithStyle(array $dataRows, $style) + { + if (!$style instanceof Style\Style) { + throw new InvalidArgumentException('The "$style" argument must be a Style instance and cannot be NULL.'); + } + + $this->setRowStyle($style); + $this->addRows($dataRows); + $this->resetRowStyleToDefault(); + + return $this; + } + + /** + * Returns the default style to be applied to rows. + * Can be overriden by children to have a custom style. + * + * @return Style\Style + */ + protected function getDefaultRowStyle() + { + return (new StyleBuilder())->build(); + } + + /** + * Sets the style to be applied to the next written rows + * until it is changed or reset. + * + * @param Style\Style $style + * @return void + */ + private function setRowStyle($style) + { + // Merge given style with the default one to inherit custom properties + $this->rowStyle = $style->mergeWith($this->defaultRowStyle); + } + + /** + * Resets the style to be applied to the next written rows. + * + * @return void + */ + private function resetRowStyleToDefault() + { + $this->rowStyle = $this->defaultRowStyle; + } + + /** + * Closes the writer. This will close the streamer as well, preventing new data + * to be written to the file. + * + * @api + * @return void + */ + public function close() + { + $this->closeWriter(); + + if (is_resource($this->filePointer)) { + $this->globalFunctionsHelper->fclose($this->filePointer); + } + + $this->isWriterOpened = false; + } +} + diff --git a/lib/spout/src/Spout/Writer/CSV/Writer.php b/lib/spout/src/Spout/Writer/CSV/Writer.php new file mode 100644 index 00000000000..4327902f894 --- /dev/null +++ b/lib/spout/src/Spout/Writer/CSV/Writer.php @@ -0,0 +1,101 @@ +fieldDelimiter = $fieldDelimiter; + return $this; + } + + /** + * Sets the field enclosure for the CSV + * + * @api + * @param string $fieldEnclosure Character that enclose fields + * @return Writer + */ + public function setFieldEnclosure($fieldEnclosure) + { + $this->fieldEnclosure = $fieldEnclosure; + return $this; + } + + /** + * Opens the CSV streamer and makes it ready to accept data. + * + * @return void + */ + protected function openWriter() + { + // Adds UTF-8 BOM for Unicode compatibility + $this->globalFunctionsHelper->fputs($this->filePointer, EncodingHelper::BOM_UTF8); + } + + /** + * Adds data to the currently opened writer. + * + * @param array $dataRow Array containing data to be written. + * Example $dataRow = ['data1', 1234, null, '', 'data5']; + * @param \Box\Spout\Writer\Style\Style $style Ignored here since CSV does not support styling. + * @return void + * @throws \Box\Spout\Common\Exception\IOException If unable to write data + */ + protected function addRowToWriter(array $dataRow, $style) + { + $wasWriteSuccessful = $this->globalFunctionsHelper->fputcsv($this->filePointer, $dataRow, $this->fieldDelimiter, $this->fieldEnclosure); + if ($wasWriteSuccessful === false) { + throw new IOException('Unable to write data'); + } + + $this->lastWrittenRowIndex++; + if ($this->lastWrittenRowIndex % self::FLUSH_THRESHOLD === 0) { + $this->globalFunctionsHelper->fflush($this->filePointer); + } + } + + /** + * Closes the CSV streamer, preventing any additional writing. + * If set, sets the headers and redirects output to the browser. + * + * @return void + */ + protected function closeWriter() + { + $this->lastWrittenRowIndex = 0; + } +} diff --git a/lib/spout/src/Spout/Writer/Common/Helper/AbstractStyleHelper.php b/lib/spout/src/Spout/Writer/Common/Helper/AbstractStyleHelper.php new file mode 100644 index 00000000000..7cf0eedda15 --- /dev/null +++ b/lib/spout/src/Spout/Writer/Common/Helper/AbstractStyleHelper.php @@ -0,0 +1,138 @@ + [STYLE_ID] mapping table, keeping track of the registered styles */ + protected $serializedStyleToStyleIdMappingTable = []; + + /** @var array [STYLE_ID] => [STYLE] mapping table, keeping track of the registered styles */ + protected $styleIdToStyleMappingTable = []; + + /** + * @param \Box\Spout\Writer\Style\Style $defaultStyle + */ + public function __construct($defaultStyle) + { + // This ensures that the default style is the first one to be registered + $this->registerStyle($defaultStyle); + } + + /** + * Registers the given style as a used style. + * Duplicate styles won't be registered more than once. + * + * @param \Box\Spout\Writer\Style\Style $style The style to be registered + * @return \Box\Spout\Writer\Style\Style The registered style, updated with an internal ID. + */ + public function registerStyle($style) + { + $serializedStyle = $style->serialize(); + + if (!$this->hasStyleAlreadyBeenRegistered($style)) { + $nextStyleId = count($this->serializedStyleToStyleIdMappingTable); + $style->setId($nextStyleId); + + $this->serializedStyleToStyleIdMappingTable[$serializedStyle] = $nextStyleId; + $this->styleIdToStyleMappingTable[$nextStyleId] = $style; + } + + return $this->getStyleFromSerializedStyle($serializedStyle); + } + + /** + * Returns whether the given style has already been registered. + * + * @param \Box\Spout\Writer\Style\Style $style + * @return bool + */ + protected function hasStyleAlreadyBeenRegistered($style) + { + $serializedStyle = $style->serialize(); + + // Using isset here because it is way faster than array_key_exists... + return isset($this->serializedStyleToStyleIdMappingTable[$serializedStyle]); + } + + /** + * Returns the registered style associated to the given serialization. + * + * @param string $serializedStyle The serialized style from which the actual style should be fetched from + * @return \Box\Spout\Writer\Style\Style + */ + protected function getStyleFromSerializedStyle($serializedStyle) + { + $styleId = $this->serializedStyleToStyleIdMappingTable[$serializedStyle]; + return $this->styleIdToStyleMappingTable[$styleId]; + } + + /** + * @return \Box\Spout\Writer\Style\Style[] List of registered styles + */ + protected function getRegisteredStyles() + { + return array_values($this->styleIdToStyleMappingTable); + } + + /** + * Returns the default style + * + * @return \Box\Spout\Writer\Style\Style Default style + */ + protected function getDefaultStyle() + { + // By construction, the default style has ID 0 + return $this->styleIdToStyleMappingTable[0]; + } + + /** + * Apply additional styles if the given row needs it. + * Typically, set "wrap text" if a cell contains a new line. + * + * @param \Box\Spout\Writer\Style\Style $style The original style + * @param array $dataRow The row the style will be applied to + * @return \Box\Spout\Writer\Style\Style The updated style + */ + public function applyExtraStylesIfNeeded($style, $dataRow) + { + $updatedStyle = $this->applyWrapTextIfCellContainsNewLine($style, $dataRow); + return $updatedStyle; + } + + /** + * Set the "wrap text" option if a cell of the given row contains a new line. + * + * @NOTE: There is a bug on the Mac version of Excel (2011 and below) where new lines + * are ignored even when the "wrap text" option is set. This only occurs with + * inline strings (shared strings do work fine). + * A workaround would be to encode "\n" as "_x000D_" but it does not work + * on the Windows version of Excel... + * + * @param \Box\Spout\Writer\Style\Style $style The original style + * @param array $dataRow The row the style will be applied to + * @return \Box\Spout\Writer\Style\Style The eventually updated style + */ + protected function applyWrapTextIfCellContainsNewLine($style, $dataRow) + { + // if the "wrap text" option is already set, no-op + if ($style->shouldWrapText()) { + return $style; + } + + foreach ($dataRow as $cell) { + if (is_string($cell) && strpos($cell, "\n") !== false) { + $style->setShouldWrapText(); + break; + } + } + + return $style; + } +} diff --git a/lib/spout/src/Spout/Writer/Common/Helper/CellHelper.php b/lib/spout/src/Spout/Writer/Common/Helper/CellHelper.php new file mode 100644 index 00000000000..b32e9ac0977 --- /dev/null +++ b/lib/spout/src/Spout/Writer/Common/Helper/CellHelper.php @@ -0,0 +1,82 @@ + cell index */ + private static $columnIndexToCellIndexCache = []; + + /** + * Returns the cell index (base 26) associated to the base 10 column index. + * Excel uses A to Z letters for column indexing, where A is the 1st column, + * Z is the 26th and AA is the 27th. + * The mapping is zero based, so that 0 maps to A, B maps to 1, Z to 25 and AA to 26. + * + * @param int $columnIndex The Excel column index (0, 42, ...) + * @return string The associated cell index ('A', 'BC', ...) + */ + public static function getCellIndexFromColumnIndex($columnIndex) + { + $originalColumnIndex = $columnIndex; + + // Using isset here because it is way faster than array_key_exists... + if (!isset(self::$columnIndexToCellIndexCache[$originalColumnIndex])) { + $cellIndex = ''; + $capitalAAsciiValue = ord('A'); + + do { + $modulus = $columnIndex % 26; + $cellIndex = chr($capitalAAsciiValue + $modulus) . $cellIndex; + + // substracting 1 because it's zero-based + $columnIndex = intval($columnIndex / 26) - 1; + + } while ($columnIndex >= 0); + + self::$columnIndexToCellIndexCache[$originalColumnIndex] = $cellIndex; + } + + return self::$columnIndexToCellIndexCache[$originalColumnIndex]; + } + + /** + * @param $value + * @return bool Whether the given value is a non empty string + */ + public static function isNonEmptyString($value) + { + return (gettype($value) === 'string' && $value !== ''); + } + + /** + * Returns whether the given value is numeric. + * A numeric value is from type "integer" or "double" ("float" is not returned by gettype). + * + * @param $value + * @return bool Whether the given value is numeric + */ + public static function isNumeric($value) + { + $valueType = gettype($value); + return ($valueType === 'integer' || $valueType === 'double'); + } + + /** + * Returns whether the given value is boolean. + * "true"/"false" and 0/1 are not booleans. + * + * @param $value + * @return bool Whether the given value is boolean + */ + public static function isBoolean($value) + { + return gettype($value) === 'boolean'; + } +} diff --git a/lib/spout/src/Spout/Writer/Common/Helper/ZipHelper.php b/lib/spout/src/Spout/Writer/Common/Helper/ZipHelper.php new file mode 100644 index 00000000000..1670d179bf7 --- /dev/null +++ b/lib/spout/src/Spout/Writer/Common/Helper/ZipHelper.php @@ -0,0 +1,217 @@ +tmpFolderPath = $tmpFolderPath; + } + + /** + * Returns the already created ZipArchive instance or + * creates one if none exists. + * + * @return \ZipArchive + */ + protected function createOrGetZip() + { + if (!isset($this->zip)) { + $this->zip = new \ZipArchive(); + $zipFilePath = $this->getZipFilePath(); + + $this->zip->open($zipFilePath, \ZipArchive::CREATE|\ZipArchive::OVERWRITE); + } + + return $this->zip; + } + + /** + * @return string Path where the zip file of the given folder will be created + */ + public function getZipFilePath() + { + return $this->tmpFolderPath . self::ZIP_EXTENSION; + } + + /** + * Adds the given file, located under the given root folder to the archive. + * The file will be compressed. + * + * Example of use: + * addFileToArchive('/tmp/xlsx/foo', 'bar/baz.xml'); + * => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml' + * + * @param string $rootFolderPath Path of the root folder that will be ignored in the archive tree. + * @param string $localFilePath Path of the file to be added, under the root folder + * @param string|void $existingFileMode Controls what to do when trying to add an existing file + * @return void + */ + public function addFileToArchive($rootFolderPath, $localFilePath, $existingFileMode = self::EXISTING_FILES_OVERWRITE) + { + $this->addFileToArchiveWithCompressionMethod( + $rootFolderPath, + $localFilePath, + $existingFileMode, + \ZipArchive::CM_DEFAULT + ); + } + + /** + * Adds the given file, located under the given root folder to the archive. + * The file will NOT be compressed. + * + * Example of use: + * addUncompressedFileToArchive('/tmp/xlsx/foo', 'bar/baz.xml'); + * => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml' + * + * @param string $rootFolderPath Path of the root folder that will be ignored in the archive tree. + * @param string $localFilePath Path of the file to be added, under the root folder + * @param string|void $existingFileMode Controls what to do when trying to add an existing file + * @return void + */ + public function addUncompressedFileToArchive($rootFolderPath, $localFilePath, $existingFileMode = self::EXISTING_FILES_OVERWRITE) + { + $this->addFileToArchiveWithCompressionMethod( + $rootFolderPath, + $localFilePath, + $existingFileMode, + \ZipArchive::CM_STORE + ); + } + + /** + * Adds the given file, located under the given root folder to the archive. + * The file will NOT be compressed. + * + * Example of use: + * addUncompressedFileToArchive('/tmp/xlsx/foo', 'bar/baz.xml'); + * => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml' + * + * @param string $rootFolderPath Path of the root folder that will be ignored in the archive tree. + * @param string $localFilePath Path of the file to be added, under the root folder + * @param string $existingFileMode Controls what to do when trying to add an existing file + * @param int $compressionMethod The compression method + * @return void + */ + protected function addFileToArchiveWithCompressionMethod($rootFolderPath, $localFilePath, $existingFileMode, $compressionMethod) + { + $zip = $this->createOrGetZip(); + + if (!$this->shouldSkipFile($zip, $localFilePath, $existingFileMode)) { + $normalizedFullFilePath = $this->getNormalizedRealPath($rootFolderPath . '/' . $localFilePath); + $zip->addFile($normalizedFullFilePath, $localFilePath); + + if (self::canChooseCompressionMethod()) { + $zip->setCompressionName($localFilePath, $compressionMethod); + } + } + } + + /** + * @return bool Whether it is possible to choose the desired compression method to be used + */ + public static function canChooseCompressionMethod() + { + // setCompressionName() is a PHP7+ method... + return (method_exists(new \ZipArchive(), 'setCompressionName')); + } + + /** + * @param string $folderPath Path to the folder to be zipped + * @param string|void $existingFileMode Controls what to do when trying to add an existing file + * @return void + */ + public function addFolderToArchive($folderPath, $existingFileMode = self::EXISTING_FILES_OVERWRITE) + { + $zip = $this->createOrGetZip(); + + $folderRealPath = $this->getNormalizedRealPath($folderPath) . '/'; + $itemIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST); + + foreach ($itemIterator as $itemInfo) { + $itemRealPath = $this->getNormalizedRealPath($itemInfo->getPathname()); + $itemLocalPath = str_replace($folderRealPath, '', $itemRealPath); + + if ($itemInfo->isFile() && !$this->shouldSkipFile($zip, $itemLocalPath, $existingFileMode)) { + $zip->addFile($itemRealPath, $itemLocalPath); + } + } + } + + /** + * @param \ZipArchive $zip + * @param string $itemLocalPath + * @param string $existingFileMode + * @return bool Whether the file should be added to the archive or skipped + */ + protected function shouldSkipFile($zip, $itemLocalPath, $existingFileMode) + { + // Skip files if: + // - EXISTING_FILES_SKIP mode chosen + // - File already exists in the archive + return ($existingFileMode === self::EXISTING_FILES_SKIP && $zip->locateName($itemLocalPath) !== false); + } + + /** + * Returns canonicalized absolute pathname, containing only forward slashes. + * + * @param string $path Path to normalize + * @return string Normalized and canonicalized path + */ + protected function getNormalizedRealPath($path) + { + $realPath = realpath($path); + return str_replace(DIRECTORY_SEPARATOR, '/', $realPath); + } + + /** + * Closes the archive and copies it into the given stream + * + * @param resource $streamPointer Pointer to the stream to copy the zip + * @return void + */ + public function closeArchiveAndCopyToStream($streamPointer) + { + $zip = $this->createOrGetZip(); + $zip->close(); + unset($this->zip); + + $this->copyZipToStream($streamPointer); + } + + /** + * Streams the contents of the zip file into the given stream + * + * @param resource $pointer Pointer to the stream to copy the zip + * @return void + */ + protected function copyZipToStream($pointer) + { + $zipFilePointer = fopen($this->getZipFilePath(), 'r'); + stream_copy_to_stream($zipFilePointer, $pointer); + fclose($zipFilePointer); + } +} diff --git a/lib/spout/src/Spout/Writer/Common/Internal/AbstractWorkbook.php b/lib/spout/src/Spout/Writer/Common/Internal/AbstractWorkbook.php new file mode 100644 index 00000000000..c8f9f9fb937 --- /dev/null +++ b/lib/spout/src/Spout/Writer/Common/Internal/AbstractWorkbook.php @@ -0,0 +1,188 @@ +shouldCreateNewSheetsAutomatically = $shouldCreateNewSheetsAutomatically; + } + + /** + * @return \Box\Spout\Writer\Common\Helper\AbstractStyleHelper The specific style helper + */ + abstract protected function getStyleHelper(); + + /** + * @return int Maximum number of rows/columns a sheet can contain + */ + abstract protected function getMaxRowsPerWorksheet(); + + /** + * Creates a new sheet in the workbook. The current sheet remains unchanged. + * + * @return WorksheetInterface The created sheet + * @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing + */ + abstract public function addNewSheet(); + + /** + * Creates a new sheet in the workbook and make it the current sheet. + * The writing will resume where it stopped (i.e. data won't be truncated). + * + * @return WorksheetInterface The created sheet + * @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing + */ + public function addNewSheetAndMakeItCurrent() + { + $worksheet = $this->addNewSheet(); + $this->setCurrentWorksheet($worksheet); + + return $worksheet; + } + + /** + * @return WorksheetInterface[] All the workbook's sheets + */ + public function getWorksheets() + { + return $this->worksheets; + } + + /** + * Returns the current sheet + * + * @return WorksheetInterface The current sheet + */ + public function getCurrentWorksheet() + { + return $this->currentWorksheet; + } + + /** + * Sets the given sheet as the current one. New data will be written to this sheet. + * The writing will resume where it stopped (i.e. data won't be truncated). + * + * @param \Box\Spout\Writer\Common\Sheet $sheet The "external" sheet to set as current + * @return void + * @throws \Box\Spout\Writer\Exception\SheetNotFoundException If the given sheet does not exist in the workbook + */ + public function setCurrentSheet($sheet) + { + $worksheet = $this->getWorksheetFromExternalSheet($sheet); + if ($worksheet !== null) { + $this->currentWorksheet = $worksheet; + } else { + throw new SheetNotFoundException('The given sheet does not exist in the workbook.'); + } + } + + /** + * @param WorksheetInterface $worksheet + * @return void + */ + protected function setCurrentWorksheet($worksheet) + { + $this->currentWorksheet = $worksheet; + } + + /** + * Returns the worksheet associated to the given external sheet. + * + * @param \Box\Spout\Writer\Common\Sheet $sheet + * @return WorksheetInterface|null The worksheet associated to the given external sheet or null if not found. + */ + protected function getWorksheetFromExternalSheet($sheet) + { + $worksheetFound = null; + + foreach ($this->worksheets as $worksheet) { + if ($worksheet->getExternalSheet() === $sheet) { + $worksheetFound = $worksheet; + break; + } + } + + return $worksheetFound; + } + + /** + * Adds data to the current sheet. + * If shouldCreateNewSheetsAutomatically option is set to true, it will handle pagination + * with the creation of new worksheets if one worksheet has reached its maximum capicity. + * + * @param array $dataRow Array containing data to be written. Cannot be empty. + * Example $dataRow = ['data1', 1234, null, '', 'data5']; + * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. + * @return void + * @throws \Box\Spout\Common\Exception\IOException If trying to create a new sheet and unable to open the sheet for writing + * @throws \Box\Spout\Writer\Exception\WriterException If unable to write data + */ + public function addRowToCurrentWorksheet($dataRow, $style) + { + $currentWorksheet = $this->getCurrentWorksheet(); + $hasReachedMaxRows = $this->hasCurrentWorkseetReachedMaxRows(); + $styleHelper = $this->getStyleHelper(); + + // if we reached the maximum number of rows for the current sheet... + if ($hasReachedMaxRows) { + // ... continue writing in a new sheet if option set + if ($this->shouldCreateNewSheetsAutomatically) { + $currentWorksheet = $this->addNewSheetAndMakeItCurrent(); + + $updatedStyle = $styleHelper->applyExtraStylesIfNeeded($style, $dataRow); + $registeredStyle = $styleHelper->registerStyle($updatedStyle); + $currentWorksheet->addRow($dataRow, $registeredStyle); + } else { + // otherwise, do nothing as the data won't be read anyways + } + } else { + $updatedStyle = $styleHelper->applyExtraStylesIfNeeded($style, $dataRow); + $registeredStyle = $styleHelper->registerStyle($updatedStyle); + $currentWorksheet->addRow($dataRow, $registeredStyle); + } + } + + /** + * @return bool Whether the current worksheet has reached the maximum number of rows per sheet. + */ + protected function hasCurrentWorkseetReachedMaxRows() + { + $currentWorksheet = $this->getCurrentWorksheet(); + return ($currentWorksheet->getLastWrittenRowIndex() >= $this->getMaxRowsPerWorksheet()); + } + + /** + * Closes the workbook and all its associated sheets. + * All the necessary files are written to disk and zipped together to create the ODS file. + * All the temporary files are then deleted. + * + * @param resource $finalFilePointer Pointer to the ODS that will be created + * @return void + */ + abstract public function close($finalFilePointer); +} diff --git a/lib/spout/src/Spout/Writer/Common/Internal/WorkbookInterface.php b/lib/spout/src/Spout/Writer/Common/Internal/WorkbookInterface.php new file mode 100644 index 00000000000..fda07934861 --- /dev/null +++ b/lib/spout/src/Spout/Writer/Common/Internal/WorkbookInterface.php @@ -0,0 +1,74 @@ + [SHEET_NAME] keeping track of sheets' name to enforce uniqueness */ + protected static $SHEETS_NAME_USED = []; + + /** @var int Index of the sheet, based on order in the workbook (zero-based) */ + protected $index; + + /** @var string Name of the sheet */ + protected $name; + + /** @var \Box\Spout\Common\Helper\StringHelper */ + protected $stringHelper; + + /** + * @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based) + */ + public function __construct($sheetIndex) + { + $this->index = $sheetIndex; + $this->stringHelper = new StringHelper(); + $this->setName(self::DEFAULT_SHEET_NAME_PREFIX . ($sheetIndex + 1)); + } + + /** + * @api + * @return int Index of the sheet, based on order in the workbook (zero-based) + */ + public function getIndex() + { + return $this->index; + } + + /** + * @api + * @return string Name of the sheet + */ + public function getName() + { + return $this->name; + } + + /** + * Sets the name of the sheet. Note that Excel has some restrictions on the name: + * - it should not be blank + * - it should not exceed 31 characters + * - it should not contain these characters: \ / ? * : [ or ] + * - it should be unique + * + * @api + * @param string $name Name of the sheet + * @return Sheet + * @throws \Box\Spout\Writer\Exception\InvalidSheetNameException If the sheet's name is invalid. + */ + public function setName($name) + { + if (!$this->isNameValid($name)) { + $errorMessage = "The sheet's name is invalid. It did not meet at least one of these requirements:\n"; + $errorMessage .= " - It should not be blank\n"; + $errorMessage .= " - It should not exceed 31 characters\n"; + $errorMessage .= " - It should not contain these characters: \\ / ? * : [ or ]\n"; + $errorMessage .= " - It should be unique"; + throw new InvalidSheetNameException($errorMessage); + } + + $this->name = $name; + self::$SHEETS_NAME_USED[$this->index] = $name; + + return $this; + } + + /** + * Returns whether the given sheet's name is valid. + * @see Sheet::setName for validity rules. + * + * @param string $name + * @return bool TRUE if the name is valid, FALSE otherwise. + */ + protected function isNameValid($name) + { + if (!is_string($name)) { + return false; + } + + $nameLength = $this->stringHelper->getStringLength($name); + + return ( + $nameLength > 0 && + $nameLength <= self::MAX_LENGTH_SHEET_NAME && + !$this->doesContainInvalidCharacters($name) && + $this->isNameUnique($name) && + !$this->doesStartOrEndWithSingleQuote($name) + ); + } + + /** + * Returns whether the given name contains at least one invalid character. + * @see Sheet::$INVALID_CHARACTERS_IN_SHEET_NAME for the full list. + * + * @param string $name + * @return bool TRUE if the name contains invalid characters, FALSE otherwise. + */ + protected function doesContainInvalidCharacters($name) + { + return (str_replace(self::$INVALID_CHARACTERS_IN_SHEET_NAME, '', $name) !== $name); + } + + /** + * Returns whether the given name starts or ends with a single quote + * + * @param string $name + * @return bool TRUE if the name starts or ends with a single quote, FALSE otherwise. + */ + protected function doesStartOrEndWithSingleQuote($name) + { + $startsWithSingleQuote = ($this->stringHelper->getCharFirstOccurrencePosition('\'', $name) === 0); + $endsWithSingleQuote = ($this->stringHelper->getCharLastOccurrencePosition('\'', $name) === ($this->stringHelper->getStringLength($name) - 1)); + + return ($startsWithSingleQuote || $endsWithSingleQuote); + } + + /** + * Returns whether the given name is unique. + * + * @param string $name + * @return bool TRUE if the name is unique, FALSE otherwise. + */ + protected function isNameUnique($name) + { + foreach (self::$SHEETS_NAME_USED as $sheetIndex => $sheetName) { + if ($sheetIndex !== $this->index && $sheetName === $name) { + return false; + } + } + + return true; + } +} diff --git a/lib/spout/src/Spout/Writer/Exception/InvalidColorException.php b/lib/spout/src/Spout/Writer/Exception/InvalidColorException.php new file mode 100644 index 00000000000..49b4a0d95ae --- /dev/null +++ b/lib/spout/src/Spout/Writer/Exception/InvalidColorException.php @@ -0,0 +1,13 @@ +rootFolder; + } + + /** + * @return string + */ + public function getSheetsContentTempFolder() + { + return $this->sheetsContentTempFolder; + } + + /** + * Creates all the folders needed to create a ODS file, as well as the files that won't change. + * + * @return void + * @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders + */ + public function createBaseFilesAndFolders() + { + $this + ->createRootFolder() + ->createMetaInfoFolderAndFile() + ->createSheetsContentTempFolder() + ->createMetaFile() + ->createMimetypeFile(); + } + + /** + * Creates the folder that will be used as root + * + * @return FileSystemHelper + * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder + */ + protected function createRootFolder() + { + $this->rootFolder = $this->createFolder($this->baseFolderPath, uniqid('ods')); + return $this; + } + + /** + * Creates the "META-INF" folder under the root folder as well as the "manifest.xml" file in it + * + * @return FileSystemHelper + * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or the "manifest.xml" file + */ + protected function createMetaInfoFolderAndFile() + { + $this->metaInfFolder = $this->createFolder($this->rootFolder, self::META_INF_FOLDER_NAME); + + $this->createManifestFile(); + + return $this; + } + + /** + * Creates the "manifest.xml" file under the "META-INF" folder (under root) + * + * @return FileSystemHelper + * @throws \Box\Spout\Common\Exception\IOException If unable to create the file + */ + protected function createManifestFile() + { + $manifestXmlFileContents = << + + + + + + +EOD; + + $this->createFileWithContents($this->metaInfFolder, self::MANIFEST_XML_FILE_NAME, $manifestXmlFileContents); + + return $this; + } + + /** + * Creates the temp folder where specific sheets content will be written to. + * This folder is not part of the final ODS file and is only used to be able to jump between sheets. + * + * @return FileSystemHelper + * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder + */ + protected function createSheetsContentTempFolder() + { + $this->sheetsContentTempFolder = $this->createFolder($this->rootFolder, self::SHEETS_CONTENT_TEMP_FOLDER_NAME); + return $this; + } + + /** + * Creates the "meta.xml" file under the root folder + * + * @return FileSystemHelper + * @throws \Box\Spout\Common\Exception\IOException If unable to create the file + */ + protected function createMetaFile() + { + $appName = self::APP_NAME; + $createdDate = (new \DateTime())->format(\DateTime::W3C); + + $metaXmlFileContents = << + + + $appName + $createdDate + $createdDate + + +EOD; + + $this->createFileWithContents($this->rootFolder, self::META_XML_FILE_NAME, $metaXmlFileContents); + + return $this; + } + + /** + * Creates the "mimetype" file under the root folder + * + * @return FileSystemHelper + * @throws \Box\Spout\Common\Exception\IOException If unable to create the file + */ + protected function createMimetypeFile() + { + $this->createFileWithContents($this->rootFolder, self::MIMETYPE_FILE_NAME, self::MIMETYPE); + return $this; + } + + /** + * Creates the "content.xml" file under the root folder + * + * @param Worksheet[] $worksheets + * @param StyleHelper $styleHelper + * @return FileSystemHelper + */ + public function createContentFile($worksheets, $styleHelper) + { + $contentXmlFileContents = << + +EOD; + + $contentXmlFileContents .= $styleHelper->getContentXmlFontFaceSectionContent(); + $contentXmlFileContents .= $styleHelper->getContentXmlAutomaticStylesSectionContent(count($worksheets)); + + $contentXmlFileContents .= ''; + + $this->createFileWithContents($this->rootFolder, self::CONTENT_XML_FILE_NAME, $contentXmlFileContents); + + // Append sheets content to "content.xml" + $contentXmlFilePath = $this->rootFolder . '/' . self::CONTENT_XML_FILE_NAME; + $contentXmlHandle = fopen($contentXmlFilePath, 'a'); + + foreach ($worksheets as $worksheet) { + // write the "" node, with the final sheet's name + fwrite($contentXmlHandle, $worksheet->getTableElementStartAsString()); + + $worksheetFilePath = $worksheet->getWorksheetFilePath(); + $this->copyFileContentsToTarget($worksheetFilePath, $contentXmlHandle); + + fwrite($contentXmlHandle, ''); + } + + $contentXmlFileContents = ''; + + fwrite($contentXmlHandle, $contentXmlFileContents); + fclose($contentXmlHandle); + + return $this; + } + + /** + * Streams the content of the file at the given path into the target resource. + * Depending on which mode the target resource was created with, it will truncate then copy + * or append the content to the target file. + * + * @param string $sourceFilePath Path of the file whose content will be copied + * @param resource $targetResource Target resource that will receive the content + * @return void + */ + protected function copyFileContentsToTarget($sourceFilePath, $targetResource) + { + $sourceHandle = fopen($sourceFilePath, 'r'); + stream_copy_to_stream($sourceHandle, $targetResource); + fclose($sourceHandle); + } + + /** + * Deletes the temporary folder where sheets content was stored. + * + * @return FileSystemHelper + */ + public function deleteWorksheetTempFolder() + { + $this->deleteFolderRecursively($this->sheetsContentTempFolder); + return $this; + } + + + /** + * Creates the "styles.xml" file under the root folder + * + * @param StyleHelper $styleHelper + * @param int $numWorksheets Number of created worksheets + * @return FileSystemHelper + */ + public function createStylesFile($styleHelper, $numWorksheets) + { + $stylesXmlFileContents = $styleHelper->getStylesXMLFileContent($numWorksheets); + $this->createFileWithContents($this->rootFolder, self::STYLES_XML_FILE_NAME, $stylesXmlFileContents); + + return $this; + } + + /** + * Zips the root folder and streams the contents of the zip into the given stream + * + * @param resource $streamPointer Pointer to the stream to copy the zip + * @return void + */ + public function zipRootFolderAndCopyToStream($streamPointer) + { + $zipHelper = new ZipHelper($this->rootFolder); + + // In order to have the file's mime type detected properly, files need to be added + // to the zip file in a particular order. + // @see http://www.jejik.com/articles/2010/03/how_to_correctly_create_odf_documents_using_zip/ + $zipHelper->addUncompressedFileToArchive($this->rootFolder, self::MIMETYPE_FILE_NAME); + + $zipHelper->addFolderToArchive($this->rootFolder, ZipHelper::EXISTING_FILES_SKIP); + $zipHelper->closeArchiveAndCopyToStream($streamPointer); + + // once the zip is copied, remove it + $this->deleteFile($zipHelper->getZipFilePath()); + } +} diff --git a/lib/spout/src/Spout/Writer/ODS/Helper/StyleHelper.php b/lib/spout/src/Spout/Writer/ODS/Helper/StyleHelper.php new file mode 100644 index 00000000000..f8b0c4d094f --- /dev/null +++ b/lib/spout/src/Spout/Writer/ODS/Helper/StyleHelper.php @@ -0,0 +1,264 @@ + [] Map whose keys contain all the fonts used */ + protected $usedFontsSet = []; + + /** + * Registers the given style as a used style. + * Duplicate styles won't be registered more than once. + * + * @param \Box\Spout\Writer\Style\Style $style The style to be registered + * @return \Box\Spout\Writer\Style\Style The registered style, updated with an internal ID. + */ + public function registerStyle($style) + { + $this->usedFontsSet[$style->getFontName()] = true; + return parent::registerStyle($style); + } + + /** + * @return string[] List of used fonts name + */ + protected function getUsedFonts() + { + return array_keys($this->usedFontsSet); + } + + /** + * Returns the content of the "styles.xml" file, given a list of styles. + * + * @param int $numWorksheets Number of worksheets created + * @return string + */ + public function getStylesXMLFileContent($numWorksheets) + { + $content = << + +EOD; + + $content .= $this->getFontFaceSectionContent(); + $content .= $this->getStylesSectionContent(); + $content .= $this->getAutomaticStylesSectionContent($numWorksheets); + $content .= $this->getMasterStylesSectionContent($numWorksheets); + + $content .= << +EOD; + + return $content; + } + + /** + * Returns the content of the "" section, inside "styles.xml" file. + * + * @return string + */ + protected function getFontFaceSectionContent() + { + $content = ''; + foreach ($this->getUsedFonts() as $fontName) { + $content .= ''; + } + $content .= ''; + + return $content; + } + + /** + * Returns the content of the "" section, inside "styles.xml" file. + * + * @return string + */ + protected function getStylesSectionContent() + { + $defaultStyle = $this->getDefaultStyle(); + + return << + + + + + + + + +EOD; + } + + /** + * Returns the content of the "" section, inside "styles.xml" file. + * + * @param int $numWorksheets Number of worksheets created + * @return string + */ + protected function getAutomaticStylesSectionContent($numWorksheets) + { + $content = ''; + + for ($i = 1; $i <= $numWorksheets; $i++) { + $content .= << + + + + +EOD; + } + + $content .= ''; + + return $content; + } + + /** + * Returns the content of the "" section, inside "styles.xml" file. + * + * @param int $numWorksheets Number of worksheets created + * @return string + */ + protected function getMasterStylesSectionContent($numWorksheets) + { + $content = ''; + + for ($i = 1; $i <= $numWorksheets; $i++) { + $content .= << + + + + + +EOD; + } + + $content .= ''; + + return $content; + } + + + /** + * Returns the contents of the "" section, inside "content.xml" file. + * + * @return string + */ + public function getContentXmlFontFaceSectionContent() + { + $content = ''; + foreach ($this->getUsedFonts() as $fontName) { + $content .= ''; + } + $content .= ''; + + return $content; + } + + /** + * Returns the contents of the "" section, inside "content.xml" file. + * + * @param int $numWorksheets Number of worksheets created + * @return string + */ + public function getContentXmlAutomaticStylesSectionContent($numWorksheets) + { + $content = ''; + + foreach ($this->getRegisteredStyles() as $style) { + $content .= $this->getStyleSectionContent($style); + } + + $content .= << + + + + + +EOD; + + for ($i = 1; $i <= $numWorksheets; $i++) { + $content .= << + + +EOD; + } + + $content .= ''; + + return $content; + } + + /** + * Returns the contents of the "" section, inside "" section + * + * @param \Box\Spout\Writer\Style\Style $style + * @return string + */ + protected function getStyleSectionContent($style) + { + $defaultStyle = $this->getDefaultStyle(); + $styleIndex = $style->getId() + 1; // 1-based + + $content = ''; + + if ($style->shouldApplyFont()) { + $content .= 'getFontColor(); + if ($fontColor !== $defaultStyle->getFontColor()) { + $content .= ' fo:color="#' . $fontColor . '"'; + } + + $fontName = $style->getFontName(); + if ($fontName !== $defaultStyle->getFontName()) { + $content .= ' style:font-name="' . $fontName . '" style:font-name-asian="' . $fontName . '" style:font-name-complex="' . $fontName . '"'; + } + + $fontSize = $style->getFontSize(); + if ($fontSize !== $defaultStyle->getFontSize()) { + $content .= ' fo:font-size="' . $fontSize . 'pt" style:font-size-asian="' . $fontSize . 'pt" style:font-size-complex="' . $fontSize . 'pt"'; + } + + if ($style->isFontBold()) { + $content .= ' fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold"'; + } + if ($style->isFontItalic()) { + $content .= ' fo:font-style="italic" style:font-style-asian="italic" style:font-style-complex="italic"'; + } + if ($style->isFontUnderline()) { + $content .= ' style:text-underline-style="solid" style:text-underline-type="single"'; + } + if ($style->isFontStrikethrough()) { + $content .= ' style:text-line-through-style="solid"'; + } + + $content .= '/>'; + } + + if ($style->shouldWrapText()) { + $content .= ''; + } + + $content .= ''; + + return $content; + } + +} diff --git a/lib/spout/src/Spout/Writer/ODS/Internal/Workbook.php b/lib/spout/src/Spout/Writer/ODS/Internal/Workbook.php new file mode 100644 index 00000000000..ce24f2f62d5 --- /dev/null +++ b/lib/spout/src/Spout/Writer/ODS/Internal/Workbook.php @@ -0,0 +1,119 @@ +fileSystemHelper = new FileSystemHelper($tempFolder); + $this->fileSystemHelper->createBaseFilesAndFolders(); + + $this->styleHelper = new StyleHelper($defaultRowStyle); + } + + /** + * @return \Box\Spout\Writer\ODS\Helper\StyleHelper Helper to apply styles to ODS files + */ + protected function getStyleHelper() + { + return $this->styleHelper; + } + + /** + * @return int Maximum number of rows/columns a sheet can contain + */ + protected function getMaxRowsPerWorksheet() + { + return self::$maxRowsPerWorksheet; + } + + /** + * Creates a new sheet in the workbook. The current sheet remains unchanged. + * + * @return Worksheet The created sheet + * @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing + */ + public function addNewSheet() + { + $newSheetIndex = count($this->worksheets); + $sheet = new Sheet($newSheetIndex); + + $sheetsContentTempFolder = $this->fileSystemHelper->getSheetsContentTempFolder(); + $worksheet = new Worksheet($sheet, $sheetsContentTempFolder); + $this->worksheets[] = $worksheet; + + return $worksheet; + } + + /** + * Closes the workbook and all its associated sheets. + * All the necessary files are written to disk and zipped together to create the ODS file. + * All the temporary files are then deleted. + * + * @param resource $finalFilePointer Pointer to the ODS that will be created + * @return void + */ + public function close($finalFilePointer) + { + /** @var Worksheet[] $worksheets */ + $worksheets = $this->worksheets; + $numWorksheets = count($worksheets); + + foreach ($worksheets as $worksheet) { + $worksheet->close(); + } + + // Finish creating all the necessary files before zipping everything together + $this->fileSystemHelper + ->createContentFile($worksheets, $this->styleHelper) + ->deleteWorksheetTempFolder() + ->createStylesFile($this->styleHelper, $numWorksheets) + ->zipRootFolderAndCopyToStream($finalFilePointer); + + $this->cleanupTempFolder(); + } + + /** + * Deletes the root folder created in the temp folder and all its contents. + * + * @return void + */ + protected function cleanupTempFolder() + { + $xlsxRootFolder = $this->fileSystemHelper->getRootFolder(); + $this->fileSystemHelper->deleteFolderRecursively($xlsxRootFolder); + } +} diff --git a/lib/spout/src/Spout/Writer/ODS/Internal/Worksheet.php b/lib/spout/src/Spout/Writer/ODS/Internal/Worksheet.php new file mode 100644 index 00000000000..3a65726ce12 --- /dev/null +++ b/lib/spout/src/Spout/Writer/ODS/Internal/Worksheet.php @@ -0,0 +1,223 @@ +externalSheet = $externalSheet; + /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ + $this->stringsEscaper = new \Box\Spout\Common\Escaper\ODS(); + $this->worksheetFilePath = $worksheetFilesFolder . '/sheet' . $externalSheet->getIndex() . '.xml'; + + $this->stringHelper = new StringHelper(); + + $this->startSheet(); + } + + /** + * Prepares the worksheet to accept data + * The XML file does not contain the "" node as it contains the sheet's name + * which may change during the execution of the program. It will be added at the end. + * + * @return void + * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing + */ + protected function startSheet() + { + $this->sheetFilePointer = fopen($this->worksheetFilePath, 'w'); + $this->throwIfSheetFilePointerIsNotAvailable(); + } + + /** + * Checks if the book has been created. Throws an exception if not created yet. + * + * @return void + * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing + */ + protected function throwIfSheetFilePointerIsNotAvailable() + { + if (!$this->sheetFilePointer) { + throw new IOException('Unable to open sheet for writing.'); + } + } + + /** + * @return string Path to the temporary sheet content XML file + */ + public function getWorksheetFilePath() + { + return $this->worksheetFilePath; + } + + /** + * Returns the table XML root node as string. + * + * @return string node as string + */ + public function getTableElementStartAsString() + { + $escapedSheetName = $this->stringsEscaper->escape($this->externalSheet->getName()); + $tableStyleName = 'ta' . ($this->externalSheet->getIndex() + 1); + + $tableElement = ''; + $tableElement .= ''; + + return $tableElement; + } + + /** + * @return \Box\Spout\Writer\Common\Sheet The "external" sheet + */ + public function getExternalSheet() + { + return $this->externalSheet; + } + + /** + * @return int The index of the last written row + */ + public function getLastWrittenRowIndex() + { + return $this->lastWrittenRowIndex; + } + + /** + * Adds data to the worksheet. + * + * @param array $dataRow Array containing data to be written. Cannot be empty. + * Example $dataRow = ['data1', 1234, null, '', 'data5']; + * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. NULL means use default style. + * @return void + * @throws \Box\Spout\Common\Exception\IOException If the data cannot be written + * @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported + */ + public function addRow($dataRow, $style) + { + $styleIndex = ($style->getId() + 1); // 1-based + $cellsCount = count($dataRow); + $this->maxNumColumns = max($this->maxNumColumns, $cellsCount); + + $data = ''; + + $currentCellIndex = 0; + $nextCellIndex = 1; + + for ($i = 0; $i < $cellsCount; $i++) { + $currentCellValue = $dataRow[$currentCellIndex]; + + // Using isset here because it is way faster than array_key_exists... + if (!isset($dataRow[$nextCellIndex]) || $currentCellValue !== $dataRow[$nextCellIndex]) { + $numTimesValueRepeated = ($nextCellIndex - $currentCellIndex); + $data .= $this->getCellContent($currentCellValue, $styleIndex, $numTimesValueRepeated); + + $currentCellIndex = $nextCellIndex; + } + + $nextCellIndex++; + } + + $data .= ''; + + $wasWriteSuccessful = fwrite($this->sheetFilePointer, $data); + if ($wasWriteSuccessful === false) { + throw new IOException("Unable to write data in {$this->worksheetFilePath}"); + } + + // only update the count if the write worked + $this->lastWrittenRowIndex++; + } + + /** + * Returns the cell XML content, given its value. + * + * @param mixed $cellValue The value to be written + * @param int $styleIndex Index of the used style + * @param int $numTimesValueRepeated Number of times the value is consecutively repeated + * @return string The cell XML content + * @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported + */ + protected function getCellContent($cellValue, $styleIndex, $numTimesValueRepeated) + { + $data = 'stringsEscaper->escape($cellValueLine) . ''; + } + + $data .= ''; + } else if (CellHelper::isBoolean($cellValue)) { + $data .= ' office:value-type="boolean" calcext:value-type="boolean" office:boolean-value="' . $cellValue . '">'; + $data .= '' . $cellValue . ''; + $data .= ''; + } else if (CellHelper::isNumeric($cellValue)) { + $data .= ' office:value-type="float" calcext:value-type="float" office:value="' . $cellValue . '">'; + $data .= '' . $cellValue . ''; + $data .= ''; + } else if (empty($cellValue)) { + $data .= '/>'; + } else { + throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cellValue)); + } + + return $data; + } + + /** + * Closes the worksheet + * + * @return void + */ + public function close() + { + fclose($this->sheetFilePointer); + } +} diff --git a/lib/spout/src/Spout/Writer/ODS/Writer.php b/lib/spout/src/Spout/Writer/ODS/Writer.php new file mode 100644 index 00000000000..4b35dfdd881 --- /dev/null +++ b/lib/spout/src/Spout/Writer/ODS/Writer.php @@ -0,0 +1,93 @@ +throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); + + $this->tempFolder = $tempFolder; + return $this; + } + + /** + * Configures the write and sets the current sheet pointer to a new sheet. + * + * @return void + * @throws \Box\Spout\Common\Exception\IOException If unable to open the file for writing + */ + protected function openWriter() + { + $tempFolder = ($this->tempFolder) ? : sys_get_temp_dir(); + $this->book = new Workbook($tempFolder, $this->shouldCreateNewSheetsAutomatically, $this->defaultRowStyle); + $this->book->addNewSheetAndMakeItCurrent(); + } + + /** + * @return Internal\Workbook The workbook representing the file to be written + */ + protected function getWorkbook() + { + return $this->book; + } + + /** + * Adds data to the currently opened writer. + * If shouldCreateNewSheetsAutomatically option is set to true, it will handle pagination + * with the creation of new worksheets if one worksheet has reached its maximum capicity. + * + * @param array $dataRow Array containing data to be written. + * Example $dataRow = ['data1', 1234, null, '', 'data5']; + * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. + * @return void + * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the book is not created yet + * @throws \Box\Spout\Common\Exception\IOException If unable to write data + */ + protected function addRowToWriter(array $dataRow, $style) + { + $this->throwIfBookIsNotAvailable(); + $this->book->addRowToCurrentWorksheet($dataRow, $style); + } + + /** + * Closes the writer, preventing any additional writing. + * + * @return void + */ + protected function closeWriter() + { + if ($this->book) { + $this->book->close($this->filePointer); + } + } +} diff --git a/lib/spout/src/Spout/Writer/Style/Color.php b/lib/spout/src/Spout/Writer/Style/Color.php new file mode 100644 index 00000000000..3c86eb706b9 --- /dev/null +++ b/lib/spout/src/Spout/Writer/Style/Color.php @@ -0,0 +1,87 @@ + 255) { + throw new InvalidColorException("The RGB components must be between 0 and 255. Received: $colorComponent"); + } + } + + /** + * Converts the color component to its corresponding hexadecimal value + * + * @param int $colorComponent Color component, 0 - 255 + * @return string Corresponding hexadecimal value, with a leading 0 if needed. E.g "0f", "2d" + */ + protected static function convertColorComponentToHex($colorComponent) + { + return str_pad(dechex($colorComponent), 2, '0', STR_PAD_LEFT); + } + + /** + * Returns the ARGB color of the given RGB color, + * assuming that alpha value is always 1. + * + * @param string $rgbColor RGB color like "FF08B2" + * @return string ARGB color + */ + public static function toARGB($rgbColor) + { + return 'FF' . $rgbColor; + } +} diff --git a/lib/spout/src/Spout/Writer/Style/Style.php b/lib/spout/src/Spout/Writer/Style/Style.php new file mode 100644 index 00000000000..91e9475fb17 --- /dev/null +++ b/lib/spout/src/Spout/Writer/Style/Style.php @@ -0,0 +1,308 @@ +id; + } + + /** + * @param int $id + * @return Style + */ + public function setId($id) + { + $this->id = $id; + return $this; + } + + /** + * @return boolean + */ + public function isFontBold() + { + return $this->fontBold; + } + + /** + * @return Style + */ + public function setFontBold() + { + $this->fontBold = true; + $this->hasSetFontBold = true; + $this->shouldApplyFont = true; + return $this; + } + + /** + * @return boolean + */ + public function isFontItalic() + { + return $this->fontItalic; + } + + /** + * @return Style + */ + public function setFontItalic() + { + $this->fontItalic = true; + $this->hasSetFontItalic = true; + $this->shouldApplyFont = true; + return $this; + } + + /** + * @return boolean + */ + public function isFontUnderline() + { + return $this->fontUnderline; + } + + /** + * @return Style + */ + public function setFontUnderline() + { + $this->fontUnderline = true; + $this->hasSetFontUnderline = true; + $this->shouldApplyFont = true; + return $this; + } + + /** + * @return boolean + */ + public function isFontStrikethrough() + { + return $this->fontStrikethrough; + } + + /** + * @return Style + */ + public function setFontStrikethrough() + { + $this->fontStrikethrough = true; + $this->hasSetFontStrikethrough = true; + $this->shouldApplyFont = true; + return $this; + } + + /** + * @return int + */ + public function getFontSize() + { + return $this->fontSize; + } + + /** + * @param int $fontSize Font size, in pixels + * @return Style + */ + public function setFontSize($fontSize) + { + $this->fontSize = $fontSize; + $this->hasSetFontSize = true; + $this->shouldApplyFont = true; + return $this; + } + + /** + * @return string + */ + public function getFontColor() + { + return $this->fontColor; + } + + /** + * Sets the font color. + * + * @param string $fontColor ARGB color (@see Color) + * @return Style + */ + public function setFontColor($fontColor) + { + $this->fontColor = $fontColor; + $this->hasSetFontColor = true; + $this->shouldApplyFont = true; + return $this; + } + + /** + * @return string + */ + public function getFontName() + { + return $this->fontName; + } + + /** + * @param string $fontName Name of the font to use + * @return Style + */ + public function setFontName($fontName) + { + $this->fontName = $fontName; + $this->hasSetFontName = true; + $this->shouldApplyFont = true; + return $this; + } + + /** + * @return boolean + */ + public function shouldWrapText() + { + return $this->shouldWrapText; + } + + /** + * @return Style + */ + public function setShouldWrapText() + { + $this->shouldWrapText = true; + $this->hasSetWrapText = true; + return $this; + } + + /** + * @return bool Whether specific font properties should be applied + */ + public function shouldApplyFont() + { + return $this->shouldApplyFont; + } + + /** + * Serializes the style for future comparison with other styles. + * The ID is excluded from the comparison, as we only care about + * actual style properties. + * + * @return string The serialized style + */ + public function serialize() + { + // In order to be able to properly compare style, set static ID value + $currentId = $this->id; + $this->setId(0); + + $serializedStyle = serialize($this); + + $this->setId($currentId); + + return $serializedStyle; + } + + /** + * Merges the current style with the given style, using the given style as a base. This means that: + * - if current style and base style both have property A set, use current style property's value + * - if current style has property A set but base style does not, use current style property's value + * - if base style has property A set but current style does not, use base style property's value + * + * @NOTE: This function returns a new style. + * + * @param Style $baseStyle + * @return Style New style corresponding to the merge of the 2 styles + */ + public function mergeWith($baseStyle) + { + $mergedStyle = clone $this; + + if (!$this->hasSetFontBold && $baseStyle->isFontBold()) { + $mergedStyle->setFontBold(); + } + if (!$this->hasSetFontItalic && $baseStyle->isFontItalic()) { + $mergedStyle->setFontItalic(); + } + if (!$this->hasSetFontUnderline && $baseStyle->isFontUnderline()) { + $mergedStyle->setFontUnderline(); + } + if (!$this->hasSetFontStrikethrough && $baseStyle->isFontStrikethrough()) { + $mergedStyle->setFontStrikethrough(); + } + if (!$this->hasSetFontSize && $baseStyle->getFontSize() !== self::DEFAULT_FONT_SIZE) { + $mergedStyle->setFontSize($baseStyle->getFontSize()); + } + if (!$this->hasSetFontColor && $baseStyle->getFontColor() !== self::DEFAULT_FONT_COLOR) { + $mergedStyle->setFontColor($baseStyle->getFontColor()); + } + if (!$this->hasSetFontName && $baseStyle->getFontName() !== self::DEFAULT_FONT_NAME) { + $mergedStyle->setFontName($baseStyle->getFontName()); + } + if (!$this->hasSetWrapText && $baseStyle->shouldWrapText()) { + $mergedStyle->setShouldWrapText(); + } + + return $mergedStyle; + } +} diff --git a/lib/spout/src/Spout/Writer/Style/StyleBuilder.php b/lib/spout/src/Spout/Writer/Style/StyleBuilder.php new file mode 100644 index 00000000000..4619f431bc1 --- /dev/null +++ b/lib/spout/src/Spout/Writer/Style/StyleBuilder.php @@ -0,0 +1,134 @@ +style = new Style(); + } + + /** + * Makes the font bold. + * + * @api + * @return StyleBuilder + */ + public function setFontBold() + { + $this->style->setFontBold(); + return $this; + } + + /** + * Makes the font italic. + * + * @api + * @return StyleBuilder + */ + public function setFontItalic() + { + $this->style->setFontItalic(); + return $this; + } + + /** + * Makes the font underlined. + * + * @api + * @return StyleBuilder + */ + public function setFontUnderline() + { + $this->style->setFontUnderline(); + return $this; + } + + /** + * Makes the font struck through. + * + * @api + * @return StyleBuilder + */ + public function setFontStrikethrough() + { + $this->style->setFontStrikethrough(); + return $this; + } + + /** + * Sets the font size. + * + * @api + * @param int $fontSize Font size, in pixels + * @return StyleBuilder + */ + public function setFontSize($fontSize) + { + $this->style->setFontSize($fontSize); + return $this; + } + + /** + * Sets the font color. + * + * @api + * @param string $fontColor ARGB color (@see Color) + * @return StyleBuilder + */ + public function setFontColor($fontColor) + { + $this->style->setFontColor($fontColor); + return $this; + } + + /** + * Sets the font name. + * + * @api + * @param string $fontName Name of the font to use + * @return StyleBuilder + */ + public function setFontName($fontName) + { + $this->style->setFontName($fontName); + return $this; + } + + /** + * Makes the text wrap in the cell if it's too long or + * on multiple lines. + * + * @api + * @return StyleBuilder + */ + public function setShouldWrapText() + { + $this->style->setShouldWrapText(); + return $this; + } + + /** + * Returns the configured style. The style is cached and can be reused. + * + * @api + * @return Style + */ + public function build() + { + return $this->style; + } +} diff --git a/lib/spout/src/Spout/Writer/WriterFactory.php b/lib/spout/src/Spout/Writer/WriterFactory.php new file mode 100644 index 00000000000..7588940c7c1 --- /dev/null +++ b/lib/spout/src/Spout/Writer/WriterFactory.php @@ -0,0 +1,48 @@ +setGlobalFunctionsHelper(new GlobalFunctionsHelper()); + + return $writer; + } +} diff --git a/lib/spout/src/Spout/Writer/WriterInterface.php b/lib/spout/src/Spout/Writer/WriterInterface.php new file mode 100644 index 00000000000..e2d9f8d291b --- /dev/null +++ b/lib/spout/src/Spout/Writer/WriterInterface.php @@ -0,0 +1,91 @@ +rootFolder; + } + + /** + * @return string + */ + public function getXlFolder() + { + return $this->xlFolder; + } + + /** + * @return string + */ + public function getXlWorksheetsFolder() + { + return $this->xlWorksheetsFolder; + } + + /** + * Creates all the folders needed to create a XLSX file, as well as the files that won't change. + * + * @return void + * @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders + */ + public function createBaseFilesAndFolders() + { + $this + ->createRootFolder() + ->createRelsFolderAndFile() + ->createDocPropsFolderAndFiles() + ->createXlFolderAndSubFolders(); + } + + /** + * Creates the folder that will be used as root + * + * @return FileSystemHelper + * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder + */ + protected function createRootFolder() + { + $this->rootFolder = $this->createFolder($this->baseFolderPath, uniqid('xlsx', true)); + return $this; + } + + /** + * Creates the "_rels" folder under the root folder as well as the ".rels" file in it + * + * @return FileSystemHelper + * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or the ".rels" file + */ + protected function createRelsFolderAndFile() + { + $this->relsFolder = $this->createFolder($this->rootFolder, self::RELS_FOLDER_NAME); + + $this->createRelsFile(); + + return $this; + } + + /** + * Creates the ".rels" file under the "_rels" folder (under root) + * + * @return FileSystemHelper + * @throws \Box\Spout\Common\Exception\IOException If unable to create the file + */ + protected function createRelsFile() + { + $relsFileContents = << + + + + + +EOD; + + $this->createFileWithContents($this->relsFolder, self::RELS_FILE_NAME, $relsFileContents); + + return $this; + } + + /** + * Creates the "docProps" folder under the root folder as well as the "app.xml" and "core.xml" files in it + * + * @return FileSystemHelper + * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or one of the files + */ + protected function createDocPropsFolderAndFiles() + { + $this->docPropsFolder = $this->createFolder($this->rootFolder, self::DOC_PROPS_FOLDER_NAME); + + $this->createAppXmlFile(); + $this->createCoreXmlFile(); + + return $this; + } + + /** + * Creates the "app.xml" file under the "docProps" folder + * + * @return FileSystemHelper + * @throws \Box\Spout\Common\Exception\IOException If unable to create the file + */ + protected function createAppXmlFile() + { + $appName = self::APP_NAME; + $appXmlFileContents = << + + $appName + 0 + +EOD; + + $this->createFileWithContents($this->docPropsFolder, self::APP_XML_FILE_NAME, $appXmlFileContents); + + return $this; + } + + /** + * Creates the "core.xml" file under the "docProps" folder + * + * @return FileSystemHelper + * @throws \Box\Spout\Common\Exception\IOException If unable to create the file + */ + protected function createCoreXmlFile() + { + $createdDate = (new \DateTime())->format(\DateTime::W3C); + $coreXmlFileContents = << + + $createdDate + $createdDate + 0 + +EOD; + + $this->createFileWithContents($this->docPropsFolder, self::CORE_XML_FILE_NAME, $coreXmlFileContents); + + return $this; + } + + /** + * Creates the "xl" folder under the root folder as well as its subfolders + * + * @return FileSystemHelper + * @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the folders + */ + protected function createXlFolderAndSubFolders() + { + $this->xlFolder = $this->createFolder($this->rootFolder, self::XL_FOLDER_NAME); + $this->createXlRelsFolder(); + $this->createXlWorksheetsFolder(); + + return $this; + } + + /** + * Creates the "_rels" folder under the "xl" folder + * + * @return FileSystemHelper + * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder + */ + protected function createXlRelsFolder() + { + $this->xlRelsFolder = $this->createFolder($this->xlFolder, self::RELS_FOLDER_NAME); + return $this; + } + + /** + * Creates the "worksheets" folder under the "xl" folder + * + * @return FileSystemHelper + * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder + */ + protected function createXlWorksheetsFolder() + { + $this->xlWorksheetsFolder = $this->createFolder($this->xlFolder, self::WORKSHEETS_FOLDER_NAME); + return $this; + } + + /** + * Creates the "[Content_Types].xml" file under the root folder + * + * @param Worksheet[] $worksheets + * @return FileSystemHelper + */ + public function createContentTypesFile($worksheets) + { + $contentTypesXmlFileContents = << + + + + +EOD; + + /** @var Worksheet $worksheet */ + foreach ($worksheets as $worksheet) { + $contentTypesXmlFileContents .= ''; + } + + $contentTypesXmlFileContents .= << + + + + +EOD; + + $this->createFileWithContents($this->rootFolder, self::CONTENT_TYPES_XML_FILE_NAME, $contentTypesXmlFileContents); + + return $this; + } + + /** + * Creates the "workbook.xml" file under the "xl" folder + * + * @param Worksheet[] $worksheets + * @return FileSystemHelper + */ + public function createWorkbookFile($worksheets) + { + $workbookXmlFileContents = << + + +EOD; + + /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ + $escaper = new \Box\Spout\Common\Escaper\XLSX(); + + /** @var Worksheet $worksheet */ + foreach ($worksheets as $worksheet) { + $worksheetName = $worksheet->getExternalSheet()->getName(); + $worksheetId = $worksheet->getId(); + $workbookXmlFileContents .= ''; + } + + $workbookXmlFileContents .= << + +EOD; + + $this->createFileWithContents($this->xlFolder, self::WORKBOOK_XML_FILE_NAME, $workbookXmlFileContents); + + return $this; + } + + /** + * Creates the "workbook.xml.res" file under the "xl/_res" folder + * + * @param Worksheet[] $worksheets + * @return FileSystemHelper + */ + public function createWorkbookRelsFile($worksheets) + { + $workbookRelsXmlFileContents = << + + + +EOD; + + /** @var Worksheet $worksheet */ + foreach ($worksheets as $worksheet) { + $worksheetId = $worksheet->getId(); + $workbookRelsXmlFileContents .= ''; + } + + $workbookRelsXmlFileContents .= ''; + + $this->createFileWithContents($this->xlRelsFolder, self::WORKBOOK_RELS_XML_FILE_NAME, $workbookRelsXmlFileContents); + + return $this; + } + + /** + * Creates the "styles.xml" file under the "xl" folder + * + * @param StyleHelper $styleHelper + * @return FileSystemHelper + */ + public function createStylesFile($styleHelper) + { + $stylesXmlFileContents = $styleHelper->getStylesXMLFileContent(); + $this->createFileWithContents($this->xlFolder, self::STYLES_XML_FILE_NAME, $stylesXmlFileContents); + + return $this; + } + + /** + * Zips the root folder and streams the contents of the zip into the given stream + * + * @param resource $streamPointer Pointer to the stream to copy the zip + * @return void + */ + public function zipRootFolderAndCopyToStream($streamPointer) + { + $zipHelper = new ZipHelper($this->rootFolder); + + // In order to have the file's mime type detected properly, files need to be added + // to the zip file in a particular order. + // "[Content_Types].xml" then at least 2 files located in "xl" folder should be zipped first. + $zipHelper->addFileToArchive($this->rootFolder, self::CONTENT_TYPES_XML_FILE_NAME); + $zipHelper->addFileToArchive($this->rootFolder, self::XL_FOLDER_NAME . '/' . self::WORKBOOK_XML_FILE_NAME); + $zipHelper->addFileToArchive($this->rootFolder, self::XL_FOLDER_NAME . '/' . self::STYLES_XML_FILE_NAME); + + $zipHelper->addFolderToArchive($this->rootFolder, ZipHelper::EXISTING_FILES_SKIP); + $zipHelper->closeArchiveAndCopyToStream($streamPointer); + + // once the zip is copied, remove it + $this->deleteFile($zipHelper->getZipFilePath()); + } +} diff --git a/lib/spout/src/Spout/Writer/XLSX/Helper/SharedStringsHelper.php b/lib/spout/src/Spout/Writer/XLSX/Helper/SharedStringsHelper.php new file mode 100644 index 00000000000..a17493a9242 --- /dev/null +++ b/lib/spout/src/Spout/Writer/XLSX/Helper/SharedStringsHelper.php @@ -0,0 +1,103 @@ + +sharedStringsFilePointer = fopen($sharedStringsFilePath, 'w'); + + $this->throwIfSharedStringsFilePointerIsNotAvailable(); + + // the headers is split into different parts so that we can fseek and put in the correct count and uniqueCount later + $header = self::SHARED_STRINGS_XML_FILE_FIRST_PART_HEADER . ' ' . self::DEFAULT_STRINGS_COUNT_PART . '>'; + fwrite($this->sharedStringsFilePointer, $header); + + /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ + $this->stringsEscaper = new \Box\Spout\Common\Escaper\XLSX(); + } + + /** + * Checks if the book has been created. Throws an exception if not created yet. + * + * @return void + * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing + */ + protected function throwIfSharedStringsFilePointerIsNotAvailable() + { + if (!$this->sharedStringsFilePointer) { + throw new IOException('Unable to open shared strings file for writing.'); + } + } + + /** + * Writes the given string into the sharedStrings.xml file. + * Starting and ending whitespaces are preserved. + * + * @param string $string + * @return int ID of the written shared string + */ + public function writeString($string) + { + fwrite($this->sharedStringsFilePointer, '' . $this->stringsEscaper->escape($string) . ''); + $this->numSharedStrings++; + + // Shared string ID is zero-based + return ($this->numSharedStrings - 1); + } + + /** + * Finishes writing the data in the sharedStrings.xml file and closes the file. + * + * @return void + */ + public function close() + { + fwrite($this->sharedStringsFilePointer, ''); + + // Replace the default strings count with the actual number of shared strings in the file header + $firstPartHeaderLength = strlen(self::SHARED_STRINGS_XML_FILE_FIRST_PART_HEADER); + $defaultStringsCountPartLength = strlen(self::DEFAULT_STRINGS_COUNT_PART); + + // Adding 1 to take into account the space between the last xml attribute and "count" + fseek($this->sharedStringsFilePointer, $firstPartHeaderLength + 1); + fwrite($this->sharedStringsFilePointer, sprintf("%-{$defaultStringsCountPartLength}s", 'count="' . $this->numSharedStrings . '" uniqueCount="' . $this->numSharedStrings . '"')); + + fclose($this->sharedStringsFilePointer); + } +} diff --git a/lib/spout/src/Spout/Writer/XLSX/Helper/StyleHelper.php b/lib/spout/src/Spout/Writer/XLSX/Helper/StyleHelper.php new file mode 100644 index 00000000000..f3da2b5fb05 --- /dev/null +++ b/lib/spout/src/Spout/Writer/XLSX/Helper/StyleHelper.php @@ -0,0 +1,175 @@ + + +EOD; + + $content .= $this->getFontsSectionContent(); + $content .= $this->getFillsSectionContent(); + $content .= $this->getBordersSectionContent(); + $content .= $this->getCellStyleXfsSectionContent(); + $content .= $this->getCellXfsSectionContent(); + $content .= $this->getCellStylesSectionContent(); + + $content .= << +EOD; + + return $content; + } + + /** + * Returns the content of the "" section. + * + * @return string + */ + protected function getFontsSectionContent() + { + $content = ''; + + /** @var \Box\Spout\Writer\Style\Style $style */ + foreach ($this->getRegisteredStyles() as $style) { + $content .= ''; + + $content .= ''; + $content .= ''; + $content .= ''; + + if ($style->isFontBold()) { + $content .= ''; + } + if ($style->isFontItalic()) { + $content .= ''; + } + if ($style->isFontUnderline()) { + $content .= ''; + } + if ($style->isFontStrikethrough()) { + $content .= ''; + } + + $content .= ''; + } + + $content .= ''; + + return $content; + } + + /** + * Returns the content of the "" section. + * + * @return string + */ + protected function getFillsSectionContent() + { + return << + + + + +EOD; + } + + /** + * Returns the content of the "" section. + * + * @return string + */ + protected function getBordersSectionContent() + { + return << + + + + + + + + +EOD; + } + + /** + * Returns the content of the "" section. + * + * @return string + */ + protected function getCellStyleXfsSectionContent() + { + return << + + +EOD; + } + + /** + * Returns the content of the "" section. + * + * @return string + */ + protected function getCellXfsSectionContent() + { + $registeredStyles = $this->getRegisteredStyles(); + + $content = ''; + + foreach ($registeredStyles as $style) { + $content .= 'shouldApplyFont()) { + $content .= ' applyFont="1"'; + } + + if ($style->shouldWrapText()) { + $content .= ' applyAlignment="1">'; + $content .= ''; + $content .= ''; + } else { + $content .= '/>'; + } + } + + $content .= ''; + + return $content; + } + + /** + * Returns the content of the "" section. + * + * @return string + */ + protected function getCellStylesSectionContent() + { + return << + + +EOD; + } +} diff --git a/lib/spout/src/Spout/Writer/XLSX/Internal/Workbook.php b/lib/spout/src/Spout/Writer/XLSX/Internal/Workbook.php new file mode 100644 index 00000000000..5208d4f32bb --- /dev/null +++ b/lib/spout/src/Spout/Writer/XLSX/Internal/Workbook.php @@ -0,0 +1,135 @@ +shouldUseInlineStrings = $shouldUseInlineStrings; + + $this->fileSystemHelper = new FileSystemHelper($tempFolder); + $this->fileSystemHelper->createBaseFilesAndFolders(); + + $this->styleHelper = new StyleHelper($defaultRowStyle); + + // This helper will be shared by all sheets + $xlFolder = $this->fileSystemHelper->getXlFolder(); + $this->sharedStringsHelper = new SharedStringsHelper($xlFolder); + } + + /** + * @return \Box\Spout\Writer\XLSX\Helper\StyleHelper Helper to apply styles to XLSX files + */ + protected function getStyleHelper() + { + return $this->styleHelper; + } + + /** + * @return int Maximum number of rows/columns a sheet can contain + */ + protected function getMaxRowsPerWorksheet() + { + return self::$maxRowsPerWorksheet; + } + + /** + * Creates a new sheet in the workbook. The current sheet remains unchanged. + * + * @return Worksheet The created sheet + * @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing + */ + public function addNewSheet() + { + $newSheetIndex = count($this->worksheets); + $sheet = new Sheet($newSheetIndex); + + $worksheetFilesFolder = $this->fileSystemHelper->getXlWorksheetsFolder(); + $worksheet = new Worksheet($sheet, $worksheetFilesFolder, $this->sharedStringsHelper, $this->shouldUseInlineStrings); + $this->worksheets[] = $worksheet; + + return $worksheet; + } + + /** + * Closes the workbook and all its associated sheets. + * All the necessary files are written to disk and zipped together to create the XLSX file. + * All the temporary files are then deleted. + * + * @param resource $finalFilePointer Pointer to the XLSX that will be created + * @return void + */ + public function close($finalFilePointer) + { + /** @var Worksheet[] $worksheets */ + $worksheets = $this->worksheets; + + foreach ($worksheets as $worksheet) { + $worksheet->close(); + } + + $this->sharedStringsHelper->close(); + + // Finish creating all the necessary files before zipping everything together + $this->fileSystemHelper + ->createContentTypesFile($worksheets) + ->createWorkbookFile($worksheets) + ->createWorkbookRelsFile($worksheets) + ->createStylesFile($this->styleHelper) + ->zipRootFolderAndCopyToStream($finalFilePointer); + + $this->cleanupTempFolder(); + } + + /** + * Deletes the root folder created in the temp folder and all its contents. + * + * @return void + */ + protected function cleanupTempFolder() + { + $xlsxRootFolder = $this->fileSystemHelper->getRootFolder(); + $this->fileSystemHelper->deleteFolderRecursively($xlsxRootFolder); + } +} diff --git a/lib/spout/src/Spout/Writer/XLSX/Internal/Worksheet.php b/lib/spout/src/Spout/Writer/XLSX/Internal/Worksheet.php new file mode 100644 index 00000000000..3ffd97b6d8e --- /dev/null +++ b/lib/spout/src/Spout/Writer/XLSX/Internal/Worksheet.php @@ -0,0 +1,185 @@ + + +EOD; + + /** @var \Box\Spout\Writer\Common\Sheet The "external" sheet */ + protected $externalSheet; + + /** @var string Path to the XML file that will contain the sheet data */ + protected $worksheetFilePath; + + /** @var \Box\Spout\Writer\XLSX\Helper\SharedStringsHelper Helper to write shared strings */ + protected $sharedStringsHelper; + + /** @var bool Whether inline or shared strings should be used */ + protected $shouldUseInlineStrings; + + /** @var \Box\Spout\Common\Escaper\XLSX Strings escaper */ + protected $stringsEscaper; + + /** @var Resource Pointer to the sheet data file (e.g. xl/worksheets/sheet1.xml) */ + protected $sheetFilePointer; + + /** @var int Index of the last written row */ + protected $lastWrittenRowIndex = 0; + + /** + * @param \Box\Spout\Writer\Common\Sheet $externalSheet The associated "external" sheet + * @param string $worksheetFilesFolder Temporary folder where the files to create the XLSX will be stored + * @param \Box\Spout\Writer\XLSX\Helper\SharedStringsHelper $sharedStringsHelper Helper for shared strings + * @param bool $shouldUseInlineStrings Whether inline or shared strings should be used + * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing + */ + public function __construct($externalSheet, $worksheetFilesFolder, $sharedStringsHelper, $shouldUseInlineStrings) + { + $this->externalSheet = $externalSheet; + $this->sharedStringsHelper = $sharedStringsHelper; + $this->shouldUseInlineStrings = $shouldUseInlineStrings; + + /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ + $this->stringsEscaper = new \Box\Spout\Common\Escaper\XLSX(); + + $this->worksheetFilePath = $worksheetFilesFolder . '/' . strtolower($this->externalSheet->getName()) . '.xml'; + $this->startSheet(); + } + + /** + * Prepares the worksheet to accept data + * + * @return void + * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing + */ + protected function startSheet() + { + $this->sheetFilePointer = fopen($this->worksheetFilePath, 'w'); + $this->throwIfSheetFilePointerIsNotAvailable(); + + fwrite($this->sheetFilePointer, self::SHEET_XML_FILE_HEADER); + fwrite($this->sheetFilePointer, ''); + } + + /** + * Checks if the book has been created. Throws an exception if not created yet. + * + * @return void + * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing + */ + protected function throwIfSheetFilePointerIsNotAvailable() + { + if (!$this->sheetFilePointer) { + throw new IOException('Unable to open sheet for writing.'); + } + } + + /** + * @return \Box\Spout\Writer\Common\Sheet The "external" sheet + */ + public function getExternalSheet() + { + return $this->externalSheet; + } + + /** + * @return int The index of the last written row + */ + public function getLastWrittenRowIndex() + { + return $this->lastWrittenRowIndex; + } + + /** + * @return int The ID of the worksheet + */ + public function getId() + { + // sheet index is zero-based, while ID is 1-based + return $this->externalSheet->getIndex() + 1; + } + + /** + * Adds data to the worksheet. + * + * @param array $dataRow Array containing data to be written. Cannot be empty. + * Example $dataRow = ['data1', 1234, null, '', 'data5']; + * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. NULL means use default style. + * @return void + * @throws \Box\Spout\Common\Exception\IOException If the data cannot be written + * @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported + */ + public function addRow($dataRow, $style) + { + $cellNumber = 0; + $rowIndex = $this->lastWrittenRowIndex + 1; + $numCells = count($dataRow); + + $rowXML = ''; + + foreach($dataRow as $cellValue) { + $columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber); + $cellXML = 'getId() . '"'; + + if (CellHelper::isNonEmptyString($cellValue)) { + if ($this->shouldUseInlineStrings) { + $cellXML .= ' t="inlineStr">' . $this->stringsEscaper->escape($cellValue) . ''; + } else { + $sharedStringId = $this->sharedStringsHelper->writeString($cellValue); + $cellXML .= ' t="s">' . $sharedStringId . ''; + } + } else if (CellHelper::isBoolean($cellValue)) { + $cellXML .= ' t="b">' . intval($cellValue) . ''; + } else if (CellHelper::isNumeric($cellValue)) { + $cellXML .= '>' . $cellValue . ''; + } else if (empty($cellValue)) { + // don't write empty cells (not appending to $cellXML is the right behavior!) + $cellXML = ''; + } else { + throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cellValue)); + } + + $rowXML .= $cellXML; + $cellNumber++; + } + + $rowXML .= ''; + + $wasWriteSuccessful = fwrite($this->sheetFilePointer, $rowXML); + if ($wasWriteSuccessful === false) { + throw new IOException("Unable to write data in {$this->worksheetFilePath}"); + } + + // only update the count if the write worked + $this->lastWrittenRowIndex++; + } + + /** + * Closes the worksheet + * + * @return void + */ + public function close() + { + fwrite($this->sheetFilePointer, ''); + fwrite($this->sheetFilePointer, ''); + fclose($this->sheetFilePointer); + } +} diff --git a/lib/spout/src/Spout/Writer/XLSX/Writer.php b/lib/spout/src/Spout/Writer/XLSX/Writer.php new file mode 100644 index 00000000000..965955a384f --- /dev/null +++ b/lib/spout/src/Spout/Writer/XLSX/Writer.php @@ -0,0 +1,132 @@ +throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); + + $this->tempFolder = $tempFolder; + return $this; + } + + /** + * Use inline string to be more memory efficient. If set to false, it will use shared strings. + * This must be set before opening the writer. + * + * @api + * @param bool $shouldUseInlineStrings Whether inline or shared strings should be used + * @return Writer + * @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened + */ + public function setShouldUseInlineStrings($shouldUseInlineStrings) + { + $this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); + + $this->shouldUseInlineStrings = $shouldUseInlineStrings; + return $this; + } + + /** + * Configures the write and sets the current sheet pointer to a new sheet. + * + * @return void + * @throws \Box\Spout\Common\Exception\IOException If unable to open the file for writing + */ + protected function openWriter() + { + if (!$this->book) { + $tempFolder = ($this->tempFolder) ? : sys_get_temp_dir(); + $this->book = new Workbook($tempFolder, $this->shouldUseInlineStrings, $this->shouldCreateNewSheetsAutomatically, $this->defaultRowStyle); + $this->book->addNewSheetAndMakeItCurrent(); + } + } + + /** + * @return Internal\Workbook The workbook representing the file to be written + */ + protected function getWorkbook() + { + return $this->book; + } + + /** + * Adds data to the currently opened writer. + * If shouldCreateNewSheetsAutomatically option is set to true, it will handle pagination + * with the creation of new worksheets if one worksheet has reached its maximum capicity. + * + * @param array $dataRow Array containing data to be written. + * Example $dataRow = ['data1', 1234, null, '', 'data5']; + * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. + * @return void + * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the book is not created yet + * @throws \Box\Spout\Common\Exception\IOException If unable to write data + */ + protected function addRowToWriter(array $dataRow, $style) + { + $this->throwIfBookIsNotAvailable(); + $this->book->addRowToCurrentWorksheet($dataRow, $style); + } + + /** + * Returns the default style to be applied to rows. + * + * @return \Box\Spout\Writer\Style\Style + */ + protected function getDefaultRowStyle() + { + return (new StyleBuilder()) + ->setFontSize(self::DEFAULT_FONT_SIZE) + ->setFontName(self::DEFAULT_FONT_NAME) + ->build(); + } + + /** + * Closes the writer, preventing any additional writing. + * + * @return void + */ + protected function closeWriter() + { + if ($this->book) { + $this->book->close($this->filePointer); + } + } +} From bff1edbe44f6546df73fb60b90f334b3c3fa8668 Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Sat, 2 Apr 2016 16:10:29 +1100 Subject: [PATCH 2/5] MDL-51603 dataformat: Implement streaming dataformat plugin --- admin/dataformats.php | 82 +++++++++ admin/environment.xml | 2 + admin/settings/plugins.php | 5 + dataformat/csv/classes/writer.php | 50 ++++++ dataformat/csv/lang/en/dataformat_csv.php | 27 +++ dataformat/csv/version.php | 30 ++++ dataformat/excel/classes/writer.php | 50 ++++++ dataformat/excel/lang/en/dataformat_excel.php | 27 +++ dataformat/excel/version.php | 30 ++++ dataformat/html/classes/writer.php | 111 ++++++++++++ dataformat/html/lang/en/dataformat_html.php | 27 +++ dataformat/html/version.php | 29 +++ dataformat/json/classes/writer.php | 75 ++++++++ dataformat/json/lang/en/dataformat_json.php | 27 +++ dataformat/json/version.php | 29 +++ dataformat/ods/classes/writer.php | 50 ++++++ dataformat/ods/lang/en/dataformat_ods.php | 27 +++ dataformat/ods/version.php | 30 ++++ dataformat/upgrade.txt | 7 + lang/en/moodle.php | 2 + lang/en/plugin.php | 2 + lib/adminlib.php | 159 +++++++++++++++++ lib/classes/component.php | 2 + lib/classes/dataformat/base.php | 126 +++++++++++++ lib/classes/dataformat/spout_base.php | 103 +++++++++++ lib/classes/plugin_manager.php | 4 + lib/classes/plugininfo/dataformat.php | 166 ++++++++++++++++++ lib/dataformatlib.php | 72 ++++++++ lib/outputrenderers.php | 42 +++++ lib/templates/dataformat_selector.mustache | 44 +++++ lib/tests/component_test.php | 10 +- lib/thirdpartylibs.xml | 6 + theme/bootstrapbase/less/moodle/core.less | 11 ++ theme/bootstrapbase/style/moodle.css | 2 +- 34 files changed, 1461 insertions(+), 5 deletions(-) create mode 100644 admin/dataformats.php create mode 100644 dataformat/csv/classes/writer.php create mode 100644 dataformat/csv/lang/en/dataformat_csv.php create mode 100644 dataformat/csv/version.php create mode 100644 dataformat/excel/classes/writer.php create mode 100644 dataformat/excel/lang/en/dataformat_excel.php create mode 100644 dataformat/excel/version.php create mode 100644 dataformat/html/classes/writer.php create mode 100644 dataformat/html/lang/en/dataformat_html.php create mode 100644 dataformat/html/version.php create mode 100644 dataformat/json/classes/writer.php create mode 100644 dataformat/json/lang/en/dataformat_json.php create mode 100644 dataformat/json/version.php create mode 100644 dataformat/ods/classes/writer.php create mode 100644 dataformat/ods/lang/en/dataformat_ods.php create mode 100644 dataformat/ods/version.php create mode 100644 dataformat/upgrade.txt create mode 100644 lib/classes/dataformat/base.php create mode 100644 lib/classes/dataformat/spout_base.php create mode 100644 lib/classes/plugininfo/dataformat.php create mode 100644 lib/dataformatlib.php create mode 100644 lib/templates/dataformat_selector.mustache diff --git a/admin/dataformats.php b/admin/dataformats.php new file mode 100644 index 00000000000..f531e9b4ed5 --- /dev/null +++ b/admin/dataformats.php @@ -0,0 +1,82 @@ +. + +/** + * Lets users manage data formats + * + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @package core + * @subpackage dataformat + */ + +require_once('../config.php'); +require_once($CFG->libdir.'/adminlib.php'); + +$action = required_param('action', PARAM_ALPHANUMEXT); +$name = required_param('name', PARAM_PLUGIN); + +$syscontext = context_system::instance(); +$PAGE->set_url('/admin/dataformats.php'); +$PAGE->set_context($syscontext); + +require_login(); +require_capability('moodle/site:config', $syscontext); +require_sesskey(); + +$return = new moodle_url('/admin/settings.php', array('section' => 'managedataformats')); + +$plugins = core_plugin_manager::instance()->get_plugins_of_type('dataformat'); +$sortorder = array_flip(array_keys($plugins)); + +if (!isset($plugins[$name])) { + print_error('courseformatnotfound', 'error', $return, $name); +} + +switch ($action) { + case 'disable': + if ($plugins[$name]->is_enabled()) { + set_config('disabled', 1, 'dataformat_'. $name); + core_plugin_manager::reset_caches(); + } + break; + case 'enable': + if (!$plugins[$name]->is_enabled()) { + unset_config('disabled', 'dataformat_'. $name); + core_plugin_manager::reset_caches(); + } + break; + case 'up': + if ($sortorder[$name]) { + $currentindex = $sortorder[$name]; + $seq = array_keys($plugins); + $seq[$currentindex] = $seq[$currentindex - 1]; + $seq[$currentindex - 1] = $name; + set_config('dataformat_plugins_sortorder', implode(',', $seq)); + } + break; + case 'down': + if ($sortorder[$name] < count($sortorder) - 1) { + $currentindex = $sortorder[$name]; + $seq = array_keys($plugins); + $seq[$currentindex] = $seq[$currentindex + 1]; + $seq[$currentindex + 1] = $name; + set_config('dataformat_plugins_sortorder', implode(',', $seq)); + } + break; +} +redirect($return); + diff --git a/admin/environment.xml b/admin/environment.xml index 43bc5e48a85..1fc8ce1f4e3 100644 --- a/admin/environment.xml +++ b/admin/environment.xml @@ -1647,6 +1647,8 @@ + + diff --git a/admin/settings/plugins.php b/admin/settings/plugins.php index 1bea1b20c6b..1beaca5858f 100644 --- a/admin/settings/plugins.php +++ b/admin/settings/plugins.php @@ -187,6 +187,11 @@ if ($hassiteconfig) { $plugin->load_settings($ADMIN, 'filtersettings', $hassiteconfig); } + // Data format settings. + $ADMIN->add('modules', new admin_category('dataformatsettings', new lang_string('dataformats'))); + $temp = new admin_settingpage('managedataformats', new lang_string('managedataformats')); + $temp->add(new admin_setting_managedataformats()); + $ADMIN->add('dataformatsettings', $temp); //== Portfolio settings == require_once($CFG->libdir. '/portfoliolib.php'); diff --git a/dataformat/csv/classes/writer.php b/dataformat/csv/classes/writer.php new file mode 100644 index 00000000000..8ccea006a58 --- /dev/null +++ b/dataformat/csv/classes/writer.php @@ -0,0 +1,50 @@ +. + +/** + * CSV data format writer + * + * @package dataformat_csv + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace dataformat_csv; + +require_once("$CFG->libdir/spout/src/Spout/Autoloader/autoload.php"); + +defined('MOODLE_INTERNAL') || die(); + +/** + * CSV data format writer + * + * @package dataformat_csv + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class writer extends \core\dataformat\spout_base { + + /** @var $mimetype */ + protected $mimetype = "text/csv"; + + /** @var $extension */ + protected $extension = ".csv"; + + /** @var $spouttype */ + protected $spouttype = \Box\Spout\Common\Type::CSV; + +} + diff --git a/dataformat/csv/lang/en/dataformat_csv.php b/dataformat/csv/lang/en/dataformat_csv.php new file mode 100644 index 00000000000..033e32d6bb2 --- /dev/null +++ b/dataformat/csv/lang/en/dataformat_csv.php @@ -0,0 +1,27 @@ +. + +/** + * CSV dataformat lang strings. + * + * @package dataformat_csv + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['dataformat'] = 'Comma separated values (.csv)'; +$string['shortname'] = 'CSV'; + diff --git a/dataformat/csv/version.php b/dataformat/csv/version.php new file mode 100644 index 00000000000..fad7203fb90 --- /dev/null +++ b/dataformat/csv/version.php @@ -0,0 +1,30 @@ +. + +/** + * Data activity filter version information + * + * @package dataformat_csv + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2016031700; +$plugin->requires = 2016031700; // Requires this Moodle version. +$plugin->component = 'dataformat_csv'; + diff --git a/dataformat/excel/classes/writer.php b/dataformat/excel/classes/writer.php new file mode 100644 index 00000000000..e6454a357c9 --- /dev/null +++ b/dataformat/excel/classes/writer.php @@ -0,0 +1,50 @@ +. + +/** + * Excel data format writer + * + * @package dataformat_excel + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace dataformat_excel; + +require_once("$CFG->libdir/spout/src/Spout/Autoloader/autoload.php"); + +defined('MOODLE_INTERNAL') || die(); + +/** + * Excel data format writer + * + * @package dataformat_excel + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class writer extends \core\dataformat\spout_base { + + /** @var $mimetype */ + protected $mimetype = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + + /** @var $extension */ + protected $extension = ".xlsx"; + + /** @var $spouttype */ + protected $spouttype = \Box\Spout\Common\Type::XLSX; + +} + diff --git a/dataformat/excel/lang/en/dataformat_excel.php b/dataformat/excel/lang/en/dataformat_excel.php new file mode 100644 index 00000000000..b286875281f --- /dev/null +++ b/dataformat/excel/lang/en/dataformat_excel.php @@ -0,0 +1,27 @@ +. + +/** + * Excel dataformat lang strings. + * + * @package dataformat_excel + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['dataformat'] = 'Microsoft Excel (.xlsx)'; +$string['shortname'] = 'Excel'; + diff --git a/dataformat/excel/version.php b/dataformat/excel/version.php new file mode 100644 index 00000000000..b6b5ffcf351 --- /dev/null +++ b/dataformat/excel/version.php @@ -0,0 +1,30 @@ +. + +/** + * Data activity filter version information + * + * @package dataformat_excel + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2016031700; +$plugin->requires = 2016031700; // Requires this Moodle version. +$plugin->component = 'dataformat_excel'; + diff --git a/dataformat/html/classes/writer.php b/dataformat/html/classes/writer.php new file mode 100644 index 00000000000..90ca3688a77 --- /dev/null +++ b/dataformat/html/classes/writer.php @@ -0,0 +1,111 @@ +. + +/** + * html data format writer + * + * @package dataformat_html + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace dataformat_html; + +defined('MOODLE_INTERNAL') || die(); + +/** + * html data format writer + * + * @package dataformat_html + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class writer extends \core\dataformat\base { + + /** @var $mimetype */ + public $mimetype = "text/html"; + + /** @var $extension */ + public $extension = ".html"; + + /** + * Write the start of the format + * + * @param array $columns + */ + public function write_header($columns) { + echo ""; + echo \html_writer::tag('title', $this->filename); + echo " + +
+"; + echo \html_writer::start_tag('tr'); + foreach ($columns as $k => $v) { + echo \html_writer::tag('th', $v); + } + echo \html_writer::end_tag('tr'); + } + + /** + * Write a single record + * + * @param array $record + * @param int $rownum + */ + public function write_record($record, $rownum) { + echo \html_writer::start_tag('tr'); + foreach ($record as $cell) { + echo \html_writer::tag('td', $cell); + } + echo \html_writer::end_tag('tr'); + } + + /** + * Write the end of the format + * + * @param array $columns + */ + public function write_footer($columns) { + echo "
"; + } + +} diff --git a/dataformat/html/lang/en/dataformat_html.php b/dataformat/html/lang/en/dataformat_html.php new file mode 100644 index 00000000000..df87409bbb1 --- /dev/null +++ b/dataformat/html/lang/en/dataformat_html.php @@ -0,0 +1,27 @@ +. + +/** + * html dataformat lang strings. + * + * @package dataformat_html + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['dataformat'] = 'HTML table'; +$string['shortname'] = 'HTML'; + diff --git a/dataformat/html/version.php b/dataformat/html/version.php new file mode 100644 index 00000000000..ac903a1f81f --- /dev/null +++ b/dataformat/html/version.php @@ -0,0 +1,29 @@ +. + +/** + * Data activity filter version information + * + * @package dataformat_html + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2016031700; +$plugin->requires = 2016031700; // Requires this Moodle version. +$plugin->component = 'dataformat_html'; diff --git a/dataformat/json/classes/writer.php b/dataformat/json/classes/writer.php new file mode 100644 index 00000000000..84f3cc67ccc --- /dev/null +++ b/dataformat/json/classes/writer.php @@ -0,0 +1,75 @@ +. + +/** + * JSON data format writer + * + * @package dataformat_json + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace dataformat_json; + +defined('MOODLE_INTERNAL') || die(); + +/** + * JSON data format writer + * + * @package dataformat_json + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class writer extends \core\dataformat\base { + + /** @var $mimetype */ + public $mimetype = "application/json"; + + /** @var $extension */ + public $extension = ".json"; + + /** + * Write the start of the format + * + * @param array $columns + */ + public function write_header($columns) { + echo "["; + } + + /** + * Write a single record + * + * @param array $record + * @param int $rownum + */ + public function write_record($record, $rownum) { + if ($rownum) { + echo ","; + } + echo json_encode($record); + } + + /** + * Write the end of the format + * + * @param array $columns + */ + public function write_footer($columns) { + echo "]"; + } + +} diff --git a/dataformat/json/lang/en/dataformat_json.php b/dataformat/json/lang/en/dataformat_json.php new file mode 100644 index 00000000000..60e77bb3587 --- /dev/null +++ b/dataformat/json/lang/en/dataformat_json.php @@ -0,0 +1,27 @@ +. + +/** + * JSON dataformat lang strings. + * + * @package dataformat_json + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['dataformat'] = 'Javascript Object Notation (.json)'; +$string['shortname'] = 'JSON'; + diff --git a/dataformat/json/version.php b/dataformat/json/version.php new file mode 100644 index 00000000000..41ef9090891 --- /dev/null +++ b/dataformat/json/version.php @@ -0,0 +1,29 @@ +. + +/** + * Data activity filter version information + * + * @package dataformat_json + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2016031700; +$plugin->requires = 2016031700; // Requires this Moodle version. +$plugin->component = 'dataformat_json'; diff --git a/dataformat/ods/classes/writer.php b/dataformat/ods/classes/writer.php new file mode 100644 index 00000000000..359aaf3e233 --- /dev/null +++ b/dataformat/ods/classes/writer.php @@ -0,0 +1,50 @@ +. + +/** + * ODS data format writer + * + * @package dataformat_ods + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace dataformat_ods; + +require_once("$CFG->libdir/spout/src/Spout/Autoloader/autoload.php"); + +defined('MOODLE_INTERNAL') || die(); + +/** + * ODS data format writer + * + * @package dataformat_ods + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class writer extends \core\dataformat\spout_base { + + /** @var $mimetype */ + protected $mimetype = "application/vnd.oasis.opendocument.spreadsheet"; + + /** @var $extension */ + protected $extension = ".ods"; + + /** @var $spouttype */ + protected $spouttype = \Box\Spout\Common\Type::ODS; + +} + diff --git a/dataformat/ods/lang/en/dataformat_ods.php b/dataformat/ods/lang/en/dataformat_ods.php new file mode 100644 index 00000000000..9c1c44e3885 --- /dev/null +++ b/dataformat/ods/lang/en/dataformat_ods.php @@ -0,0 +1,27 @@ +. + +/** + * ODS dataformat lang strings. + * + * @package dataformat_ods + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['dataformat'] = 'OpenDocument (.ods)'; +$string['shortname'] = 'OpenDoc'; + diff --git a/dataformat/ods/version.php b/dataformat/ods/version.php new file mode 100644 index 00000000000..41452d6554d --- /dev/null +++ b/dataformat/ods/version.php @@ -0,0 +1,30 @@ +. + +/** + * Data activity filter version information + * + * @package dataformat_ods + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2016031700; +$plugin->requires = 2016031700; // Requires this Moodle version. +$plugin->component = 'dataformat_ods'; + diff --git a/dataformat/upgrade.txt b/dataformat/upgrade.txt new file mode 100644 index 00000000000..bfd29d1c945 --- /dev/null +++ b/dataformat/upgrade.txt @@ -0,0 +1,7 @@ +This files describes API changes in /dataformat/ download system, +information provided here is intended especially for developers. + +=== 3.1 === +* Added new plugin system with low memory support for csv, ods, xls and json + + diff --git a/lang/en/moodle.php b/lang/en/moodle.php index a5407741470..bc8fba5f5b6 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -431,6 +431,7 @@ $string['databaseupgradeblocks'] = 'Blocks version is now {$a}'; $string['databaseupgradegroups'] = 'Groups version is now {$a}'; $string['databaseupgradelocal'] = 'Local database customisations version is now {$a}'; $string['databaseupgrades'] = 'Upgrading database'; +$string['dataformats'] = 'Data formats'; $string['date'] = 'Date'; $string['datechanged'] = 'Date changed'; $string['datemostrecentfirst'] = 'Date - most recent first'; @@ -1088,6 +1089,7 @@ $string['makethismyhome'] = 'Make this my default home page'; $string['manageblocks'] = 'Blocks'; $string['managecategorythis'] = 'Manage this category'; $string['managecourses'] = 'Manage courses'; +$string['managedataformats'] = 'Manage data formats'; $string['managedatabase'] = 'Database'; $string['manageeditorfiles'] = 'Manage files used by editor'; $string['managefilters'] = 'Filters'; diff --git a/lang/en/plugin.php b/lang/en/plugin.php index b3d0a5d9b1d..ff64fd338f0 100644 --- a/lang/en/plugin.php +++ b/lang/en/plugin.php @@ -121,6 +121,8 @@ $string['type_calendartype'] = 'Calendar type'; $string['type_calendartype_plural'] = 'Calendar types'; $string['type_coursereport'] = 'Course report'; $string['type_coursereport_plural'] = 'Course reports'; +$string['type_dataformat'] = 'Data format'; +$string['type_dataformat_plural'] = 'Data formats'; $string['type_editor'] = 'Editor'; $string['type_editor_plural'] = 'Editors'; $string['type_enrol'] = 'Enrolment method'; diff --git a/lib/adminlib.php b/lib/adminlib.php index 3df2cc3d6ad..c12e5457f25 100644 --- a/lib/adminlib.php +++ b/lib/adminlib.php @@ -6916,6 +6916,165 @@ class admin_setting_manageformats extends admin_setting { } } +/** + * Data formats manager. Allow reorder and to enable/disable data formats and jump to settings + * + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class admin_setting_managedataformats extends admin_setting { + + /** + * Calls parent::__construct with specific arguments + */ + public function __construct() { + $this->nosave = true; + parent::__construct('managedataformats', new lang_string('managedataformats'), '', ''); + } + + /** + * Always returns true + * + * @return true + */ + public function get_setting() { + return true; + } + + /** + * Always returns true + * + * @return true + */ + public function get_defaultsetting() { + return true; + } + + /** + * Always returns '' and doesn't write anything + * + * @param mixed $data string or array, must not be NULL + * @return string Always returns '' + */ + public function write_setting($data) { + // Do not write any setting. + return ''; + } + + /** + * Search to find if Query is related to format plugin + * + * @param string $query The string to search for + * @return bool true for related false for not + */ + public function is_related($query) { + if (parent::is_related($query)) { + return true; + } + $formats = core_plugin_manager::instance()->get_plugins_of_type('dataformat'); + foreach ($formats as $format) { + if (strpos($format->component, $query) !== false || + strpos(core_text::strtolower($format->displayname), $query) !== false) { + return true; + } + } + return false; + } + + /** + * Return XHTML to display control + * + * @param mixed $data Unused + * @param string $query + * @return string highlight + */ + public function output_html($data, $query='') { + global $CFG, $OUTPUT; + $return = ''; + + $formats = core_plugin_manager::instance()->get_plugins_of_type('dataformat'); + + $txt = get_strings(array('settings', 'name', 'enable', 'disable', 'up', 'down', 'default')); + $txt->uninstall = get_string('uninstallplugin', 'core_admin'); + $txt->updown = "$txt->up/$txt->down"; + + $table = new html_table(); + $table->head = array($txt->name, $txt->enable, $txt->updown, $txt->uninstall, $txt->settings); + $table->align = array('left', 'center', 'center', 'center', 'center'); + $table->attributes['class'] = 'manageformattable generaltable admintable'; + $table->data = array(); + + $cnt = 0; + $spacer = $OUTPUT->pix_icon('spacer', '', 'moodle', array('class' => 'iconsmall')); + $totalenabled = 0; + foreach ($formats as $format) { + if ($format->is_enabled() && $format->is_installed_and_upgraded()) { + $totalenabled++; + } + } + foreach ($formats as $format) { + $status = $format->get_status(); + $url = new moodle_url('/admin/dataformats.php', + array('sesskey' => sesskey(), 'name' => $format->name)); + + $class = ''; + if ($format->is_enabled()) { + $strformatname = $format->displayname; + if ($totalenabled == 1&& $format->is_enabled()) { + $hideshow = ''; + } else { + $hideshow = html_writer::link($url->out(false, array('action' => 'disable')), + $OUTPUT->pix_icon('t/hide', $txt->disable, 'moodle', array('class' => 'iconsmall'))); + } + } else { + $class = 'dimmed_text'; + $strformatname = $format->displayname; + $hideshow = html_writer::link($url->out(false, array('action' => 'enable')), + $OUTPUT->pix_icon('t/show', $txt->enable, 'moodle', array('class' => 'iconsmall'))); + } + + $updown = ''; + if ($cnt) { + $updown .= html_writer::link($url->out(false, array('action' => 'up')), + $OUTPUT->pix_icon('t/up', $txt->up, 'moodle', array('class' => 'iconsmall'))). ''; + } else { + $updown .= $spacer; + } + if ($cnt < count($formats) - 1) { + $updown .= ' '.html_writer::link($url->out(false, array('action' => 'down')), + $OUTPUT->pix_icon('t/down', $txt->down, 'moodle', array('class' => 'iconsmall'))); + } else { + $updown .= $spacer; + } + + $uninstall = ''; + if ($status === core_plugin_manager::PLUGIN_STATUS_MISSING) { + $uninstall = get_string('status_missing', 'core_plugin'); + } else if ($status === core_plugin_manager::PLUGIN_STATUS_NEW) { + $uninstall = get_string('status_new', 'core_plugin'); + } else if ($uninstallurl = core_plugin_manager::instance()->get_uninstall_url('dataformat_'.$format->name, 'manage')) { + if ($totalenabled != 1 || !$format->is_enabled()) { + $uninstall = html_writer::link($uninstallurl, $txt->uninstall); + } + } + + $settings = ''; + if ($format->get_settings_url()) { + $settings = html_writer::link($format->get_settings_url(), $txt->settings); + } + + $row = new html_table_row(array($strformatname, $hideshow, $updown, $uninstall, $settings)); + if ($class) { + $row->attributes['class'] = $class; + } + $table->data[] = $row; + $cnt++; + } + $return .= html_writer::table($table); + return highlight($query, $return); + } +} + /** * Special class for filter administration. * diff --git a/lib/classes/component.php b/lib/classes/component.php index 8bd659ddffd..584efd1888a 100644 --- a/lib/classes/component.php +++ b/lib/classes/component.php @@ -357,6 +357,7 @@ $cache = '.var_export($cache, true).'; 'countries' => null, 'course' => $CFG->dirroot.'/course', 'currencies' => null, + 'dataformat' => $CFG->dirroot.'/dataformat', 'dbtransfer' => null, 'debug' => null, 'editor' => $CFG->dirroot.'/lib/editor', @@ -430,6 +431,7 @@ $cache = '.var_export($cache, true).'; 'filter' => $CFG->dirroot.'/filter', 'editor' => $CFG->dirroot.'/lib/editor', 'format' => $CFG->dirroot.'/course/format', + 'dataformat' => $CFG->dirroot.'/dataformat', 'profilefield' => $CFG->dirroot.'/user/profile/field', 'report' => $CFG->dirroot.'/report', 'coursereport' => $CFG->dirroot.'/course/report', // Must be after system reports. diff --git a/lib/classes/dataformat/base.php b/lib/classes/dataformat/base.php new file mode 100644 index 00000000000..d9fe32da04d --- /dev/null +++ b/lib/classes/dataformat/base.php @@ -0,0 +1,126 @@ +. + +/** + * Base class for dataformat. + * + * @package core + * @subpackage dataformat + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\dataformat; + +/** + * Base class for dataformat. + * + * @package core + * @subpackage dataformat + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class base { + + /** @var $mimetype */ + protected $mimetype = "text/plain"; + + /** @var $extension */ + protected $extension = ".txt"; + + /** @var $filename */ + protected $filename = ''; + + /** + * Get the file extension + * + * @return string file extension + */ + public function get_extension() { + return $this->extension; + } + + /** + * Set download filename base + * + * @param string $filename + */ + public function set_filename($filename) { + $this->filename = $filename; + } + + /** + * Set the title of the worksheet inside a spreadsheet + * + * For some formats this will be ignored. + * + * @param string $title + */ + public function set_sheettitle($title) { + } + + /** + * Output file headers to initialise the download of the file. + */ + public function send_http_headers() { + global $CFG; + + if (defined('BEHAT_SITE_RUNNING')) { + // For text based formats - we cannot test the output with behat if we force a file download. + return; + } + if (is_https()) { + // HTTPS sites - watch out for IE! KB812935 and KB316431. + header('Cache-Control: max-age=10'); + header('Pragma: '); + } else { + // Normal http - prevent caching at all cost. + header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0'); + header('Pragma: no-cache'); + } + header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT'); + header("Content-Type: $this->mimetype\n"); + $filename = $this->filename . $this->get_extension(); + header("Content-Disposition: attachment; filename=\"$filename\""); + } + + /** + * Write the start of the format + * + * @param array $columns + */ + public function write_header($columns) { + // Override me if needed. + } + + /** + * Write a single record + * + * @param array $record + * @param int $rownum + */ + abstract public function write_record($record, $rownum); + + /** + * Write the end of the format + * + * @param array $columns + */ + public function write_footer($columns) { + // Override me if needed. + } + +} diff --git a/lib/classes/dataformat/spout_base.php b/lib/classes/dataformat/spout_base.php new file mode 100644 index 00000000000..ef085f7b0a0 --- /dev/null +++ b/lib/classes/dataformat/spout_base.php @@ -0,0 +1,103 @@ +. + +/** + * Common Spout class for dataformat. + * + * @package core + * @subpackage dataformat + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\dataformat; + +/** + * Common Spout class for dataformat. + * + * @package core + * @subpackage dataformat + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class spout_base extends \core\dataformat\base { + + /** @var $spouttype */ + protected $spouttype = ''; + + /** @var $writer */ + protected $writer; + + /** @var $sheettitle */ + protected $sheettitle; + + /** + * Output file headers to initialise the download of the file. + */ + public function send_http_headers() { + $this->writer = \Box\Spout\Writer\WriterFactory::create($this->spouttype); + $filename = $this->filename . $this->get_extension(); + $this->writer->openToBrowser($filename); + } + + /** + * Set the title of the worksheet inside a spreadsheet + * + * For some formats this will be ignored. + * + * @param string $title + */ + public function set_sheettitle($title) { + if (!$title) { + return; + } + $title = preg_replace('/[\\\\\/\\?\\*\\[\\]]/', '', $title); + $title = substr($title, 0, 31); + $this->sheettitle = $title; + $sheet = $this->writer->getCurrentSheet(); + $sheet->setName($title); + } + + /** + * Write the start of the format + * + * @param array $columns + */ + public function write_header($columns) { + $this->writer->addRow(array_values((array)$columns)); + } + + /** + * Write a single record + * + * @param object $record + * @param int $rownum + */ + public function write_record($record, $rownum) { + $this->writer->addRow(array_values((array)$record)); + } + + /** + * Write the end of the format + * + * @param array $columns + */ + public function write_footer($columns) { + $this->writer->close(); + $this->writer = null; + } + +} diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index d454849be47..27d5088ceaa 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -1758,6 +1758,10 @@ class core_plugin_manager { 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url' ), + 'dataformat' => array( + 'html', 'csv', 'json', 'excel', 'ods', + ), + 'datapreset' => array( 'imagegallery' ), diff --git a/lib/classes/plugininfo/dataformat.php b/lib/classes/plugininfo/dataformat.php new file mode 100644 index 00000000000..6bd48ec881e --- /dev/null +++ b/lib/classes/plugininfo/dataformat.php @@ -0,0 +1,166 @@ +. + +/** + * Defines classes used for plugin info. + * + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @package core + */ +namespace core\plugininfo; + +use moodle_url, part_of_admin_tree, admin_settingpage, admin_externalpage, core_plugin_manager; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class for dataformats + * + * @package core + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + */ +class dataformat extends base { + + /** + * Display name + */ + public function init_display_name() { + if (!get_string_manager()->string_exists('dataformat', $this->component)) { + $this->displayname = '[dataformat,' . $this->component . ']'; + } else { + $this->displayname = get_string('dataformat', $this->component); + } + } + + /** + * Gathers and returns the information about all plugins of the given type + * + * @param string $type the name of the plugintype, eg. mod, auth or workshopform + * @param string $typerootdir full path to the location of the plugin dir + * @param string $typeclass the name of the actually called class + * @param core_plugin_manager $pluginman the plugin manager calling this method + * @return array of plugintype classes, indexed by the plugin name + */ + public static function get_plugins($type, $typerootdir, $typeclass, $pluginman) { + global $CFG; + $formats = parent::get_plugins($type, $typerootdir, $typeclass, $pluginman); + + if (!empty($CFG->dataformat_plugins_sortorder)) { + $order = explode(',', $CFG->dataformat_plugins_sortorder); + $order = array_merge(array_intersect($order, array_keys($formats)), + array_diff(array_keys($formats), $order)); + } else { + $order = array_keys($formats); + } + $sortedformats = array(); + foreach ($order as $formatname) { + $sortedformats[$formatname] = $formats[$formatname]; + } + return $sortedformats; + } + + /** + * Finds all enabled plugins, the result may include missing plugins. + * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown + */ + public static function get_enabled_plugins() { + $enabled = array(); + $plugins = core_plugin_manager::instance()->get_installed_plugins('dataformat'); + + if (!$plugins) { + return array(); + } + + $enabled = array(); + foreach ($plugins as $plugin => $version) { + $disabled = get_config('dataformat_' . $plugin, 'disabled'); + if (empty($disabled)) { + $enabled[$plugin] = $plugin; + } + } + return $enabled; + } + + /** + * Returns the node name used in admin settings menu for this plugin settings (if applicable) + * + * @return null|string node name or null if plugin does not create settings node (default) + */ + public function get_settings_section_name() { + return 'dataformatsetting' . $this->name; + } + + /** + * Loads plugin settings to the settings tree + * + * This function usually includes settings.php file in plugins folder. + * Alternatively it can create a link to some settings page (instance of admin_externalpage) + * + * @param \part_of_admin_tree $adminroot + * @param string $parentnodename + * @param bool $hassiteconfig whether the current user has moodle/site:config capability + */ + public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) { + global $CFG, $USER, $DB, $OUTPUT, $PAGE; // In case settings.php wants to refer to them. + $ADMIN = $adminroot; // May be used in settings.php. + $plugininfo = $this; // Also can be used inside settings.php. + $dataformat = $this; // Also can be used inside settings.php. + + if (!$this->is_installed_and_upgraded()) { + return; + } + + if (!$hassiteconfig) { + return; + } + if (file_exists($this->full_path('settings.php'))) { + $fullpath = $this->full_path('settings.php'); + } else if (file_exists($this->full_path('dataformatsettings.php'))) { + $fullpath = $this->full_path('dataformatsettings.php'); + } else { + return; + } + + $section = $this->get_settings_section_name(); + $settings = new admin_settingpage($section, $this->displayname, 'moodle/site:config', $this->is_enabled() === false); + include($fullpath); // This may also set $settings to null. + + if ($settings) { + $ADMIN->add($parentnodename, $settings); + } + } + + /** + * dataformats can be uninstalled + * + * @return bool + */ + public function is_uninstall_allowed() { + return true; + } + + /** + * Return URL used for management of plugins of this type. + * @return moodle_url + */ + public static function get_manage_url() { + return new moodle_url('/admin/settings.php?section=managedataformats'); + } + +} + diff --git a/lib/dataformatlib.php b/lib/dataformatlib.php new file mode 100644 index 00000000000..f192ef82d44 --- /dev/null +++ b/lib/dataformatlib.php @@ -0,0 +1,72 @@ +. + +/** + * dataformatlib.php - Contains core dataformat related functions. + * + * @package core + * @subpackage dataformat + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Sends a formated data file to the browser + * + * @package core + * @subpackage dataformat + * + * @param string $filename The base filename without an extension + * @param string $dataformat A dataformat name + * @param array $columns An ordered map of column keys and labels + * @param Iterator $iterator An iterator over the records, usually a RecordSet + * @param function $callback An option function applied to each record before writing + * @param mixed $extra An optional value which is passed into the callback function + */ +function download_as_dataformat($filename, $dataformat, $columns, $iterator, $callback = null) { + + if (!NO_OUTPUT_BUFFERING) { + throw new coding_exception("NO_OUTPUT_BUFFERING must be set to true before calling download_as_dataformat"); + } + + $classname = 'dataformat_' . $dataformat . '\writer'; + if (!class_exists($classname)) { + throw new coding_exception("Unable to locate dataformat/$type/classes/writer.php"); + } + $format = new $classname; + + // The data format export could take a while to generate... + set_time_limit(0); + + // Close the session so that the users other tabs in the same session are not blocked. + \core\session\manager::write_close(); + + $format->set_filename($filename); + $format->send_http_headers(); + $format->write_header($columns); + $c = 0; + foreach ($iterator as $row) { + if ($callback) { + $row = $callback($row); + } + if ($row === null) { + continue; + } + $format->write_record($row, $c++); + } + $format->write_footer($columns); +} + diff --git a/lib/outputrenderers.php b/lib/outputrenderers.php index c8e75b53495..7495155c8d9 100644 --- a/lib/outputrenderers.php +++ b/lib/outputrenderers.php @@ -1876,6 +1876,48 @@ class core_renderer extends renderer_base { return $this->render($select); } + /** + * Returns a dataformat selection and download form + * + * @param string $label A text label + * @param moodle_url|string $base The download page url + * @param string $name The query param which will hold the type of the download + * @param array $params Extra params sent to the download page + * @return string HTML fragment + */ + public function download_dataformat_selector($label, $base, $name = 'dataformat', $params = array()) { + + $formats = core_plugin_manager::instance()->get_plugins_of_type('dataformat'); + $options = array(); + foreach ($formats as $format) { + if ($format->is_enabled()) { + $options[] = array( + 'value' => $format->name, + 'label' => get_string('shortname', $format->component), + ); + } + } + $hiddenparams = array(); + foreach ($params as $key => $value) { + $hiddenparams[] = array( + 'name' => $key, + 'value' => $value, + ); + } + $data = array( + 'label' => $label, + 'base' => $base, + 'name' => $name, + 'params' => $hiddenparams, + 'options' => $options, + 'sesskey' => sesskey(), + 'submit' => get_string('download'), + ); + + return $this->render_from_template('core/dataformat_selector', $data); + } + + /** * Internal implementation of single_select rendering * diff --git a/lib/templates/dataformat_selector.mustache b/lib/templates/dataformat_selector.mustache new file mode 100644 index 00000000000..96c713a3a22 --- /dev/null +++ b/lib/templates/dataformat_selector.mustache @@ -0,0 +1,44 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core/dataformat_selector + + Template for all html emails. Note that it may wrap content formatted + elsewhere in another a module template. + + Context variables required for this template: + * label + * base + * name + * params + * options + * sesskey + * submit +}} +
+ + + + + {{#params}} + + {{/params}} +
diff --git a/lib/tests/component_test.php b/lib/tests/component_test.php index d570e27f887..193cb637047 100644 --- a/lib/tests/component_test.php +++ b/lib/tests/component_test.php @@ -31,10 +31,12 @@ defined('MOODLE_INTERNAL') || die(); */ class core_component_testcase extends advanced_testcase { - // To be changed if number of subsystems increases/decreases, - // this is defined here to annoy devs that try to add more without any thinking, - // always verify that it does not collide with any existing add-on modules and subplugins!!! - const SUBSYSTEMCOUNT = 64; + /** + * To be changed if number of subsystems increases/decreases, + * this is defined here to annoy devs that try to add more without any thinking, + * always verify that it does not collide with any existing add-on modules and subplugins!!! + */ + const SUBSYSTEMCOUNT = 65; public function test_get_core_subsystems() { global $CFG; diff --git a/lib/thirdpartylibs.xml b/lib/thirdpartylibs.xml index 2f208ee44d8..6440a734fea 100644 --- a/lib/thirdpartylibs.xml +++ b/lib/thirdpartylibs.xml @@ -269,4 +269,10 @@ GPL 1.6.3 + + spout + Spout + GPL + 2.3.0 + diff --git a/theme/bootstrapbase/less/moodle/core.less b/theme/bootstrapbase/less/moodle/core.less index bec8e380860..8795f6be1c9 100644 --- a/theme/bootstrapbase/less/moodle/core.less +++ b/theme/bootstrapbase/less/moodle/core.less @@ -501,6 +501,17 @@ a.skip:active { .groupselector label { display: inline-block; } +// Data format selector +.dataformatselector { + margin: 1em 0; +} +.dataformatselector label { + display: inline-block; + margin: 0 5px 10px 0; + line-height: 30px; + vertical-align: top; +} + // Login .loginbox { margin: 15px; diff --git a/theme/bootstrapbase/style/moodle.css b/theme/bootstrapbase/style/moodle.css index 7a6777eb81f..c6c2f1a3331 100644 --- a/theme/bootstrapbase/style/moodle.css +++ b/theme/bootstrapbase/style/moodle.css @@ -1,4 +1,4 @@ -.layout-option-noheader #page-header,.layout-option-nonavbar #page-navbar,.layout-option-nofooter #page-footer,.layout-option-nocourseheader .course-content-header,.layout-option-nocoursefooter .course-content-footer{display:none}.empty-region-side-pre #block-region-side-pre,.empty-region-side-post #block-region-side-post,.jsenabled.docked-region-side-post #block-region-side-post,.jsenabled.docked-region-side-pre #block-region-side-pre{display:none}.content-only #region-main.span9,.empty-region-side-post #region-bs-main-and-pre.span9,.empty-region-side-pre #region-bs-main-and-post.span9,.empty-region-side-post #region-bs-main-and-post.span9 #region-main.span8,.jsenabled.docked-region-side-post #region-bs-main-and-pre.span9,.jsenabled.docked-region-side-post #region-bs-main-and-post.span9 #region-main.span8,.jsenabled.docked-region-side-pre #region-bs-main-and-post.span9{width:100%}.empty-region-side-pre #region-bs-main-and-pre.span9 #region-main,.jsenabled.docked-region-side-pre #region-bs-main-and-pre.span9 #region-main{float:none;width:100%}.empty-region-side-pre #region-bs-main-and-post.span9 #region-main.span8,.jsenabled.docked-region-side-pre #region-bs-main-and-post.span9 #region-main.span8{float:right}.content-only #region-main-box,.content-only #region-main{width:100%}.empty-region-side-pre.used-region-side-post #region-main{width:100%}.empty-region-side-post.used-region-side-pre #region-main-box{width:100%}.jsenabled.docked-region-side-pre.empty-region-side-pre.used-region-side-post #region-main{width:100%}.jsenabled.docked-region-side-post.empty-region-side-post.used-region-side-pre #region-main-box{width:100%}.empty-region-side-post.used-region-side-pre #region-main.span8,.jsenabled.docked-region-side-post.used-region-side-pre #region-main.span8{width:74.46808511%;*width:74.41489362%}.empty-region-side-post.used-region-side-pre #block-region-side-pre.span4,.jsenabled.docked-region-side-post.used-region-side-pre #block-region-side-pre.span4{width:23.40425532%;*width:23.35106383%}.dir-ltr,.mdl-left,.dir-rtl .mdl-right{text-align:left}.dir-rtl,.mdl-right,.dir-rtl .mdl-left{text-align:right}#add,#remove,.centerpara,.mdl-align{text-align:center}a.dimmed,a.dimmed:link,a.dimmed:visited,a.dimmed_text,a.dimmed_text:link,a.dimmed_text:visited,.dimmed_text,.dimmed_text a,.dimmed_text a:link,.dimmed_text a:visited,.usersuspended,.usersuspended a,.usersuspended a:link,.usersuspended a:visited,.dimmed_category,.dimmed_category a{color:#999}.activity.label .dimmed_text{opacity:.5;filter:alpha(opacity=50)}.unlist,.unlist li,.inline-list,.inline-list li,.block .list,.block .list li,.section li.activity,.section li.movehere,.tabtree li{list-style:none;margin:0;padding:0}.inline,.inline-list li{display:inline}.notifytiny{font-size:10.5px}.notifytiny li,.notifytiny td{font-size:100%}.red,.notifyproblem{color:#b94a48}.green,.notifysuccess{color:#468847}.highlight{background:#d9edf7}.reportlink{text-align:right}a.autolink.glossary:hover{cursor:help}.collapsibleregioncaption{white-space:nowrap}.pagelayout-mydashboard.jsenabled .collapsibleregioncaption{cursor:pointer}.collapsibleregioncaption img{vertical-align:middle}.jsenabled .hiddenifjs{display:none}.visibleifjs{display:none}.jsenabled .visibleifjs{display:inline}.jsenabled .collapsibleregion{overflow:hidden}.jsenabled .collapsed .collapsibleregioninner{visibility:hidden}.collapsible-actions{display:none;text-align:right}.dir-rtl .collapsible-actions{text-align:left}.jsenabled .collapsible-actions{display:block}.collapsible-actions .collapseexpand{padding-left:20px;background:url([[pix:t/collapsed]]) 2px center no-repeat}.dir-rtl .collapsible-actions .collapseexpand{padding-right:20px;padding-left:0;background:url([[pix:t/collapsed_rtl]]) right center no-repeat}.collapsible-actions .collapse-all,.dir-rtl .collapsible-actions .collapse-all{background-image:url([[pix:t/expanded]])}.yui-overlay .yui-widget-bd{background-color:#FFEE69;border:1px solid #A6982B;border-top-color:#D4C237;color:#000000;left:0;padding:2px 5px;position:relative;top:0;z-index:1}.clearer{background:transparent;border-width:0;clear:both;display:block;height:1px;margin:0;padding:0}.bold,.warning,.errorbox .title,.pagingbar .title,.pagingbar .thispage{font-weight:bold}img.resize{height:1em;width:1em}.block img.resize,.breadcrumb img.resize{height:.9em;width:.8em}img.icon{height:16px;vertical-align:text-bottom;width:16px;padding-right:6px}.dir-rtl img.icon{padding-left:6px;padding-right:0}img.iconsmall{height:12px;margin-right:3px;vertical-align:middle;width:12px}img.iconhelp,.helplink img{height:16px;padding-left:3px;vertical-align:text-bottom;width:16px}h1 img.iconhelp,h1 img.icon,h2 img.iconhelp,h2 img.icon,h3 img.iconhelp,h3 img.icon,h4 img.iconhelp,h4 img.icon,h5 img.iconhelp,h5 img.icon,h6 img.iconhelp,h6 img.icon{vertical-align:middle;padding:4px}.dir-rtl img.iconhelp,.dir-rtl .helplink img{padding-right:3px;padding-left:0}img.iconlarge{height:24px;width:24px;vertical-align:middle}img.iconsort{vertical-align:text-bottom;padding-left:.3em;margin-bottom:.15em}.dir-rtl img.iconsort{padding-right:.3em;padding-left:0}img.icontoggle{height:17px;vertical-align:middle;width:50px}img.iconkbhelp{height:17px;width:49px}img.icon-pre,.dir-rtl img.icon-post{padding-right:3px;padding-left:0}img.icon-post,.dir-rtl img.icon-pre{padding-left:3px;padding-right:0}.boxaligncenter{margin-left:auto;margin-right:auto}.boxalignright{margin-left:auto;margin-right:0}.boxalignleft{margin-left:0;margin-right:auto}.boxwidthnarrow{width:30%}.boxwidthnormal{width:50%}.boxwidthwide{width:80%}.headermain{font-weight:bold}#maincontent{display:block;height:1px;overflow:hidden}img.uihint{cursor:help}#addmembersform table{margin-left:auto;margin-right:auto}table.flexible .emptyrow{display:none}img.emoticon{vertical-align:middle;width:15px;height:15px}form.popupform,form.popupform div{display:inline}.arrow_button input{overflow:hidden}.action-icon img.smallicon{vertical-align:text-bottom;margin:0 .3em}.no-overflow{overflow:auto;padding-bottom:1px}.pagelayout-report .no-overflow{overflow:visible}.no-overflow>.generaltable{margin-bottom:0}.accesshide{position:absolute;left:-10000px;font-weight:normal;font-size:1em}.dir-rtl .accesshide{top:-30000px;left:auto}span.hide,div.hide{display:none}a.skip-block,a.skip{position:absolute;top:-1000em;font-size:.85em;text-decoration:none}a.skip-block:focus,a.skip-block:active,a.skip:focus,a.skip:active{position:static;display:block}.skip-block-to{display:block;height:1px;overflow:hidden}.addbloglink{text-align:center}.blog_entry .audience{text-align:right;padding-right:4px}.blog_entry .tags{margin-top:15px}.blog_entry .tags .action-icon img.smallicon{height:16px;width:16px}.blog_entry .content{margin-left:43px}#page-group-index #groupeditform{text-align:center}#doc-contents h1{margin:1em 0 0 0}#doc-contents ul{margin:0;padding:0;width:90%}#doc-contents ul li{list-style-type:none}.groupmanagementtable td{vertical-align:top}.groupmanagementtable #existingcell,.groupmanagementtable #potentialcell{width:42%}.groupmanagementtable #buttonscell{width:16%}.groupmanagementtable #buttonscell p.arrow_button input{width:auto;min-width:80%;margin:0 auto}.groupmanagementtable #removeselect_wrapper,.groupmanagementtable #addselect_wrapper{width:100%}.groupmanagementtable #removeselect_wrapper label,.groupmanagementtable #addselect_wrapper label{font-weight:normal}.dir-rtl .groupmanagementtable p{text-align:right}#group-usersummary{width:14em}.groupselector{margin-top:3px;margin-bottom:3px;display:inline-block}.groupselector label{display:inline-block}.loginbox{margin:15px;overflow:visible}.loginbox.twocolumns{margin:15px}.loginbox h2,.loginbox .subcontent{margin:5px;padding:10px;text-align:center}.loginbox .loginpanel .desc{margin:0;padding:0;margin-bottom:5px;margin-top:15px}.loginbox .signuppanel .subcontent{text-align:left}.dir-rtl .loginbox .signuppanel .subcontent{text-align:right}.loginbox .loginsub{margin-left:0;margin-right:0}.loginbox .guestsub,.loginbox .forgotsub,.loginbox .potentialidps{margin:5px 12%}.loginbox .potentialidps .potentialidplist{margin-left:40%}.loginbox .potentialidps .potentialidplist div{text-align:left}.loginbox .loginform{margin-top:1em;text-align:left}.loginbox .loginform .form-label{float:left;text-align:right;width:49%;white-space:nowrap}.loginbox .loginform .form-input{float:right;width:50%}.loginbox .loginform .form-input input{width:6em}.loginbox .signupform{margin-top:1em;text-align:center}.loginbox.twocolumns .loginpanel,.loginbox.twocolumns .signuppanel{width:48%;border:0;margin:0;padding:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;float:left;margin-left:2.76243%;min-height:30px;margin-bottom:-2000px;padding-bottom:2000px}.dir-rtl .loginbox.twocolumns .loginpanel,.dir-rtl .loginbox.twocolumns .signuppanel{float:right}.loginbox .potentialidp .smallicon{vertical-align:text-bottom;margin:0 .3em}.notepost{margin-bottom:1em}.notepost .userpicture{float:left;margin-right:5px}.notepost .content,.notepost .footer{clear:both}.notesgroup{margin-left:20px}.path-my .coursebox .overview{margin:15px 30px 10px 30px}.path-my .coursebox .info{float:none;margin:0}.mod_introbox{padding:10px}table.mod_index{width:100%}.comment-ctrl{font-size:12px;display:none;margin:0;padding:0}.comment-ctrl h5{margin:0;padding:5px}.comment-area{max-width:400px;padding:5px}.comment-area textarea{width:100%;overflow:auto}.comment-area textarea.fullwidth{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.comment-area .fd{text-align:right}.comment-meta span{color:gray}.comment-link img{vertical-align:text-bottom}.comment-list{font-size:11px;overflow:auto;list-style:none;padding:0;margin:0}.comment-list li{margin:2px;list-style:none;margin-bottom:5px;clear:both;padding:.3em;position:relative}.comment-list li.first{display:none}.comment-paging{text-align:center}.comment-paging .pageno{padding:2px}.comment-paging .curpage{border:1px solid #CCC}.comment-message .picture{width:20px;float:left}.dir-rtl .comment-message .picture{float:right}.comment-message .text{margin:0;padding:0}.comment-message .text p{padding:0;margin:0 18px 0 0}.comment-delete{position:absolute;top:0;right:0;margin:.3em}.dir-rtl .comment-delete{position:absolute;left:0;right:auto;margin:.3em}.comment-report-selectall{display:none}.comment-link{display:none}.jsenabled .comment-link{display:block}.jsenabled .showcommentsnonjs{display:none}.jsenabled .comment-report-selectall{display:inline}.completion-expired{background:#f2dede}.completion-expected{font-size:10.5px}.completion-sortchoice,.completion-identifyfield{font-size:10.5px;vertical-align:bottom}.completion-progresscell{text-align:right}.completion-expired .completion-expected{font-weight:bold}img.user-image{height:100px;width:100px}#tag-search-box{text-align:center;margin:10px auto}.path-tag .tag-index-items .tagarea{border:1px solid #E3E3E3;border-radius:4px;padding:10px;margin-top:10px}.path-tag .tag-index-items .tagarea h3{display:block;padding:3px 0 10px 0;margin:0;font-size:1.1em;font-weight:bold;line-height:20px;color:#999;text-shadow:0 1px 0 rgba(255,255,255,0.5);text-transform:uppercase;word-wrap:break-word;border-bottom:solid 1px #E3E3E3;margin-bottom:10px}.path-tag .tagarea .controls,.path-tag .tagarea .taggeditems{*zoom:1}.path-tag .tagarea .controls:before,.path-tag .tagarea .taggeditems:before,.path-tag .tagarea .controls:after,.path-tag .tagarea .taggeditems:after{display:table;content:"";line-height:0}.path-tag .tagarea .controls:after,.path-tag .tagarea .taggeditems:after{clear:both}.path-tag .tagarea .controls,.path-tag .tag-backtoallitems{text-align:center}.path-tag .tagarea .controls .gotopage.nextpage{float:right}.path-tag .tagarea .controls .gotopage.prevpage{float:left}.path-tag .tagarea .controls .exclusivemode{display:inline-block}.dir-rtl.path-tag .tagarea .controls .gotopage.nextpage{float:left}.dir-rtl.path-tag .tagarea .controls .gotopage.prevpage{float:right}.path-tag .tagarea .controls.controls-bottom{margin-top:5px}.path-tag .tagarea .controls .gotopage.nextpage::after{padding-right:5px;padding-left:5px;content:"»"}.path-tag .tagarea .controls .gotopage.prevpage::before{padding-right:5px;padding-left:5px;content:"«"}span.flagged-tag,tr.flagged-tag,span.flagged-tag a,tr.flagged-tag a{color:#b94a48}.tag-management-table td,.tag-management-table th{vertical-align:middle;padding:4px}.tag-management-table .inplaceeditable.inplaceeditingon input{width:150px}.path-admin-tag .addstandardtags{float:right}.path-admin-tag .addstandardtags img{margin:0 5px}.dir-rtl.path-admin-tag .addstandardtags{float:left}.path-tag .tag-relatedtags{padding-top:10px}.path-tag .tag-management-box{text-align:right}.path-tag .tag-index-toc{padding:10px;text-align:center}.path-tag .tag-index-toc li,.path-tag .tag-management-box li{margin-left:5px;margin-right:5px}.path-tag .tag-management-box li a.edittag{background-image:url([[pix:moodle|i/settings]])}.path-tag .tag-management-box li a.flagasinappropriate{background-image:url([[pix:moodle|i/flagged]])}.path-tag .tag-management-box li a.removefrommyinterests{background-image:url([[pix:moodle|t/delete]])}.path-tag .tag-management-box li a.addtomyinterests{background-image:url([[pix:moodle|t/add]])}.path-tag .tag-management-box li a{background-repeat:no-repeat;background-position:left;padding-left:17px}.tag_feed.media-list .media .itemimage{float:left}.dir-rtl .tag_feed.media-list .media .itemimage{float:right}.tag_feed.media-list .media .itemimage img{height:35px;width:35px}.tag_feed.media-list .media .media-body{padding-right:10px;padding-left:10px}.tag_feed .media .muted a{color:#999}.tag_cloud{text-align:center}.tag_cloud .inline-list li{padding:0 .2em}.tag_cloud .tag_overflow{margin-top:1em;font-style:italic}.tag_cloud .s20{font-size:2.7em}.tag_cloud .s19{font-size:2.6em}.tag_cloud .s18{font-size:2.5em}.tag_cloud .s17{font-size:2.4em}.tag_cloud .s16{font-size:2.3em}.tag_cloud .s15{font-size:2.2em}.tag_cloud .s14{font-size:2.1em}.tag_cloud .s13{font-size:2em}.tag_cloud .s12{font-size:1.9em}.tag_cloud .s11{font-size:1.8em}.tag_cloud .s10{font-size:1.7em}.tag_cloud .s9{font-size:1.6em}.tag_cloud .s8{font-size:1.5em}.tag_cloud .s7{font-size:1.4em}.tag_cloud .s6{font-size:1.3em}.tag_cloud .s5{font-size:1.2em}.tag_cloud .s4{font-size:1.1em}.tag_cloud .s3{font-size:1em}.tag_cloud .s2{font-size:.9em}.tag_cloud .s1{font-size:.8em}.tag_cloud .s0{font-size:.7em}.tag_list ul{display:inline}.tag_list.hideoverlimit .overlimit{display:none}.tag_list .tagmorelink{display:none}.tag_list.hideoverlimit .tagmorelink{display:inline}.tag_list.hideoverlimit .taglesslink{display:none}#webservice-doc-generator td{text-align:left;border:0 solid black}.smartselect{position:absolute}.smartselect .smartselect_mask{background-color:#fff}.smartselect ul{padding:0;margin:0}.smartselect ul li{list-style:none}.smartselect .smartselect_menu{margin-right:5px}.safari .smartselect .smartselect_menu{margin-left:2px}.smartselect .smartselect_menu,.smartselect .smartselect_submenu{border:1px solid #000;background-color:#FFF;display:none}.smartselect .smartselect_menu.visible,.smartselect .smartselect_submenu.visible{display:block}.smartselect .smartselect_menu_content ul li{position:relative;padding:2px 5px}.smartselect .smartselect_menu_content ul li a{color:#333;text-decoration:none}.smartselect .smartselect_menu_content ul li a.selectable{color:inherit}.smartselect .smartselect_submenuitem{background-image:url([[pix:moodle|t/collapsed]]);background-repeat:no-repeat;background-position:100%}.smartselect.spanningmenu .smartselect_submenu{position:absolute;top:-1px;left:100%}.smartselect.spanningmenu .smartselect_submenu a{white-space:nowrap;padding-right:16px}.smartselect.spanningmenu .smartselect_menu_content ul li a.selectable:hover{text-decoration:underline}.smartselect.compactmenu .smartselect_submenu{position:relative;margin:2px -3px;margin-left:10px;display:none;border-width:0;z-index:1010}.smartselect.compactmenu .smartselect_submenu.visible{display:block}.smartselect.compactmenu .smartselect_menu{z-index:1000;overflow:hidden}.smartselect.compactmenu .smartselect_submenu .smartselect_submenu{z-index:1020}.smartselect.compactmenu .smartselect_submenuitem:hover>.smartselect_menuitem_label{font-weight:bold}#page-admin-registration-register .registration_textfield{width:300px}.userenrolment{width:100%;border-collapse:collapse}.userenrolment tr{vertical-align:top}.userenrolment td{padding:0;height:41px}.userenrolment .subfield{margin-right:5px}.userenrolment .col_userdetails .subfield_picture{float:left}.userenrolment .col_lastseen{width:150px}.userenrolment .col_role{width:262px}.userenrolment .col_role .roles,.userenrolment .col_group .groups{margin-right:30px}.userenrolment .col_role .role,.userenrolment .col_group .group{float:left;padding:3px;margin:3px;white-space:nowrap}.userenrolment .col_role .role a,.userenrolment .col_group .group a{margin-left:3px;cursor:pointer}.userenrolment .col_role .addrole,.userenrolment .col_group .addgroup{float:right;padding:3px;margin:3px}.userenrolment .col_role .addrole>a:hover,.userenrolment .col_group .addgroup>a:hover{border-bottom:1px solid #666}.userenrolment .col_role .addrole img,.userenrolment .col_group .addgroup img{vertical-align:baseline}.dir-rtl .userenrolment .col_role .role{float:right}.userenrolment .hasAllRoles .col_role .addrole{display:none}.userenrolment .col_enrol .enrolment{float:left;padding:3px;margin:3px}.userenrolment .col_enrol .enrolment a{float:right;margin-left:3px}#page-enrol-users .enrol_user_buttons{float:right}#page-enrol-users .enrol_user_buttons .enrolusersbutton{display:inline}#page-enrol-users .enrol_user_buttons .enrolusersbutton div,#page-enrol-users .enrol_user_buttons .enrolusersbutton form{display:inline;margin-right:0}#page-enrol-users #filterform{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);border-color:#e3e3e3;padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;display:inline-block}#page-enrol-users #filterform blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}#page-enrol-users #filterform .fitem{display:inline-block;line-height:40px;margin-right:.3em;white-space:nowrap}#page-enrol-users #filterform .fitem label{display:inline;line-height:20px;padding-right:.3em}#page-enrol-users #filterform .fitem :before,#page-enrol-users #filterform .fitem :after{display:inline}#page-enrol-users #filterform div,#page-enrol-users #filterform fieldset{display:inline;float:none;clear:none;width:auto;margin:0}#page-enrol-users #filterform select,#page-enrol-users #filterform .ftext input{width:7em}#page-enrol-users #filterform input,#page-enrol-users #filterform select{margin-bottom:0}#page-enrol-users .user-enroller-panel .uep-search-results .user .details{width:237px}#page-enrol-users .user-enroller-panel .uep-search-results .cohort .details{width:237px}.dir-rtl#page-enrol-users .col_userdetails .subfield_picture{float:right}.dir-rtl#page-enrol-users .enrol_user_buttons{float:left}.dir-rtl#page-enrol-users .enrol_user_buttons .enrolusersbutton{margin-left:0;margin-right:1em}.dir-rtl#page-enrol-users .enrol_user_buttons .enrolusersbutton div{margin-left:0}.dir-rtl#page-enrol-users #filterform .fitem{margin-right:0;margin-left:.3em}.dir-rtl#page-enrol-users #filterform .fitem label{padding-right:0;padding-left:.3em}#page-enrol-users .enrol-users-page-action input{margin-left:0}.dir-rtl .headermain{float:right}.dir-rtl .headermenu{float:left}.dir-rtl .loginbox .loginform .form-label{float:right;text-align:left}.dir-rtl .loginbox .loginform .form-input{text-align:right;margin-right:1%}.dir-rtl .yui3-menu-hidden{left:0}#page-admin-roles-define.dir-rtl #rolesform .felement{margin-right:180px}#page-message-edit.dir-rtl table.generaltable th.c0{text-align:right}.corelightbox{background-color:#CCC;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center}.corelightbox img{position:fixed;top:50%;left:50%}.mod-indent-outer{display:table}.mod-indent{display:table-cell}.label .mod-indent{float:left;padding-top:20px}.mod-indent-1{width:30px}.mod-indent-2{width:60px}.mod-indent-3{width:90px}.mod-indent-4{width:120px}.mod-indent-5{width:150px}.mod-indent-6{width:180px}.mod-indent-7{width:210px}.mod-indent-8{width:240px}.mod-indent-9{width:270px}.mod-indent-10{width:300px}.mod-indent-11{width:330px}.mod-indent-12{width:360px}.mod-indent-13{width:390px}.mod-indent-14{width:420px}.mod-indent-15{width:450px}.mod-indent-16{width:480px}.mod-indent-huge{width:480px}.resourcecontent .mediaplugin_mp3 object{height:25px;width:600px}.resourcecontent audio.mediaplugin_html5audio{width:600px}.resourceimage{max-width:100%}.mediaplugin_mp3 object{height:15px;width:300px}audio.mediaplugin_html5audio{width:300px}.core_media_preview.pagelayout-embedded #content{padding:0}.core_media_preview.pagelayout-embedded #maincontent{height:0}body#page-lib-editor-tinymce-plugins-moodlemedia-preview{padding:0;margin:0;min-width:0;background:none}.dir-rtl .ygtvtn,.dir-rtl .ygtvtm,.dir-rtl .ygtvtmh,.dir-rtl .ygtvtmhh,.dir-rtl .ygtvtp,.dir-rtl .ygtvtph,.dir-rtl .ygtvtphh,.dir-rtl .ygtvln,.dir-rtl .ygtvlm,.dir-rtl .ygtvlmh,.dir-rtl .ygtvlmhh,.dir-rtl .ygtvlp,.dir-rtl .ygtvlph,.dir-rtl .ygtvlphh,.dir-rtl .ygtvdepthcell,.dir-rtl .ygtvok,.dir-rtl .ygtvok:hover,.dir-rtl .ygtvcancel,.dir-rtl .ygtvcancel:hover{width:18px;height:22px;background-image:url([[pix:theme|yui2-treeview-sprite-rtl]]);background-repeat:no-repeat;cursor:pointer}.dir-rtl .ygtvtn{background-position:0 -5600px}.dir-rtl .ygtvtm{background-position:0 -4000px}.dir-rtl .ygtvtmh,.dir-rtl .ygtvtmhh{background-position:0 -4800px}.dir-rtl .ygtvtp{background-position:0 -6400px}.dir-rtl .ygtvtph,.dir-rtl .ygtvtphh{background-position:0 -7200px}.dir-rtl .ygtvln{background-position:0 -1600px}.dir-rtl .ygtvlm{background-position:0 0}.dir-rtl .ygtvlmh,.dir-rtl .ygtvlmhh{background-position:0 -800px}.dir-rtl .ygtvlp{background-position:0 -2400px}.dir-rtl .ygtvlph,.dir-rtl .ygtvlphh{background-position:0 -3200px}.dir-rtl .ygtvdepthcell{background-position:0 -8000px}.dir-rtl .ygtvok{background-position:0 -8800px}.dir-rtl .ygtvok:hover{background-position:0 -8844px}.dir-rtl .ygtvcancel{background-position:0 -8822px}.dir-rtl .ygtvcancel:hover{background-position:0 -8866px}.dir-rtl.yui-skin-sam .yui-panel .hd{text-align:right}.dir-rtl .yui-skin-sam .yui-layout .yui-layout-unit div.yui-layout-bd{text-align:right}.dir-rtl .clearlooks2.ie9 .mceAlert .mceMiddle span,.dir-rtl .clearlooks2 .mceConfirm .mceMiddle span{top:44px}.dir-rtl .o2k7Skin table,.dir-rtl .o2k7Skin tbody,.dir-rtl .o2k7Skin a,.dir-rtl .o2k7Skin img,.dir-rtl .o2k7Skin tr,.dir-rtl .o2k7Skin div,.dir-rtl .o2k7Skin td,.dir-rtl .o2k7Skin iframe,.dir-rtl .o2k7Skin span,.dir-rtl .o2k7Skin *,.dir-rtl .o2k7Skin .mceText,.dir-rtl .o2k7Skin .mceListBox .mceText{text-align:right}.path-rating .ratingtable{width:100%;margin-bottom:1em}.path-rating .ratingtable th.rating{width:100%}.path-rating .ratingtable td.rating,.path-rating .ratingtable td.time{white-space:nowrap;text-align:center}.initialbar a,.initialbar strong{padding-left:3px;padding-right:3px}.moodle-dialogue-base .moodle-dialogue-lightbox{background-color:#AAA}.moodle-dialogue-base .hidden,.moodle-dialogue-base .moodle-dialogue-hidden{display:none}.no-scrolling{overflow:hidden}.moodle-dialogue-base .moodle-dialogue-fullscreen{left:0;top:0;right:0;bottom:-50px;position:fixed}.moodle-dialogue-base .moodle-dialogue-fullscreen .moodle-dialogue-content{overflow:auto}.moodle-dialogue-base .moodle-dialogue-fullscreen .closebutton{width:28px;height:16px;background-size:100%}.moodle-dialogue-base .moodle-dialogue{padding:0;margin:0;background:none;border:none;z-index:600;outline:#000 dotted 0}.moodle-dialogue-base .moodle-dialogue-wrap{margin-top:-3px;margin-left:-3px;background-color:#fff;border:1px solid #ccc;-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;-webkit-box-shadow:5px 5px 20px 0 #666;-moz-box-shadow:5px 5px 20px 0 #666;box-shadow:5px 5px 20px 0 #666}.moodle-dialogue-base .moodle-dialogue-wrap .moodle-dialogue-hd,.moodle-dialogue-base .moodle-dialogue-wrap .moodle-dialogue-hd.yui3-widget-hd{margin:0;padding:5px;font-size:12px;font-weight:normal;letter-spacing:1px;color:#333;text-align:center;text-shadow:1px 1px 1px #fff;-webkit-border-radius:10px 10px 0 0;-moz-border-radius:10px 10px 0 0;border-radius:10px 10px 0 0;border-bottom:1px solid #bbb;background:#ccc;background-color:#ebebeb;background-image:-moz-linear-gradient(top, #fff, #ccc);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#ccc));background-image:-webkit-linear-gradient(top, #fff, #ccc);background-image:-o-linear-gradient(top, #fff, #ccc);background-image:linear-gradient(to bottom, #fff, #ccc);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffcccccc', GradientType=0);filter:0}.moodle-dialogue-base .moodle-dialogue-wrap .moodle-dialogue-hd h1{margin:0;padding:0;display:inline;font-size:100%;font-weight:bold}.moodle-dialogue-base .moodle-dialogue-wrap .moodle-dialogue-hd .yui3-widget-buttons{padding:5px}.moodle-dialogue-base .closebutton{width:25px;height:15px;float:right;vertical-align:middle;display:inline-block;cursor:pointer;padding:0;background-image:url([[pix:theme|sprite]]);background-repeat:no-repeat;border-style:none}.dir-rtl .moodle-dialogue-base .moodle-dialogue-wrap .moodle-dialogue-hd .yui3-widget-buttons{left:0;right:auto}.moodle-dialogue-base .moodle-dialogue .moodle-dialogue-bd{padding:1em;line-height:2em;color:#555;font-size:12px}.moodle-dialogue-base .moodle-dialogue-wrap .moodle-dialogue-content{padding:0;background:#FFF}.moodle-dialogue-base .moodle-dialogue-fullscreen .moodle-dialogue-hd{padding:10px;font-size:16px}.moodle-dialogue-base .moodle-dialogue-fullscreen .moodle-dialogue-content{overflow:auto;position:absolute;top:0;bottom:50px;left:0;right:0;margin:0;border:0}.moodle-dialogue-base .moodle-dialogue-fullscreen .moodle-dialogue-hd,.moodle-dialogue-base .moodle-dialogue-fullscreen .moodle-dialogue-wrap{border-radius:0}.moodle-dialogue-confirm .confirmation-dialogue{text-align:center}.moodle-dialogue-confirm .confirmation-dialogue input{text-align:center}.moodle-dialogue-exception .moodle-exception-message{text-align:center}.moodle-dialogue-exception .moodle-exception-param label{font-weight:bold}.moodle-dialogue-exception .param-stacktrace label{background-color:#EEE;border:1px solid #ccc;border-bottom-width:0}.moodle-dialogue-exception .param-stacktrace pre{border:1px solid #ccc;background-color:#fff}.moodle-dialogue-exception .param-stacktrace .stacktrace-file{color:navy;font-size:11.9px}.moodle-dialogue-exception .param-stacktrace .stacktrace-line{color:#b94a48;font-size:11.9px}.moodle-dialogue-exception .param-stacktrace .stacktrace-call{color:#333;font-size:90%;border-bottom:1px solid #eee}.moodle-dialogue-base .moodle-dialogue .moodle-dialogue-content .moodle-dialogue-ft{padding:0;margin:.7em 1em;text-align:right;background-color:#FFF;font-size:12px}.moodle-dialogue-confirm .confirmation-message{margin:.5em 1em}.moodle-dialogue-confirm .confirmation-dialogue input{min-width:80px}.moodle-dialogue-exception .moodle-exception-message{margin:1em}.moodle-dialogue-exception .moodle-exception-param{margin-bottom:.5em}.moodle-dialogue-exception .moodle-exception-param label{width:150px}.moodle-dialogue-exception .param-stacktrace label{display:block;margin:0;padding:4px 1em}.moodle-dialogue-exception .param-stacktrace pre{display:block;height:200px;overflow:auto}.moodle-dialogue-exception .param-stacktrace .stacktrace-file{display:inline-block;margin:4px 0}.moodle-dialogue-exception .param-stacktrace .stacktrace-line{display:inline-block;width:50px;margin:4px 1em}.moodle-dialogue-exception .param-stacktrace .stacktrace-call{padding-left:25px;margin-bottom:4px;padding-bottom:4px}.moodle-dialogue .moodle-dialogue-bd .content-lightbox{opacity:.75;filter:alpha(opacity=75);width:100%;height:100%;top:0;left:0;background-color:white;text-align:center;padding:10% 0}.moodle-dialogue .tooltiptext{max-height:300px}.moodle-dialogue-base .moodle-dialogue.moodle-dialogue-tooltip{z-index:3001}.moodle-dialogue-base .moodle-dialogue.moodle-dialogue-tooltip .moodle-dialogue-bd{overflow:auto}#page-question-edit.dir-rtl a.container-close{right:auto;left:6px}.chooserdialoguebody,.choosertitle{display:none}.moodle-dialogue.chooserdialogue .moodle-dialogue-content .moodle-dialogue-ft{margin:0}.chooserdialogue .moodle-dialogue-wrap .moodle-dialogue-bd{padding:0;background:#F2F2F2;-webkit-border-bottom-right-radius:10px;-moz-border-radius-bottomright:10px;border-bottom-right-radius:10px;-webkit-border-bottom-left-radius:10px;-moz-border-radius-bottomleft:10px;border-bottom-left-radius:10px}.choosercontainer #chooseform .submitbuttons{padding:.7em 0;text-align:center}@media (max-height:639px){.ios.safari .choosercontainer #chooseform .submitbuttons{padding:45px 0}}.choosercontainer #chooseform .submitbuttons input{min-width:100px;margin:0 .5em}.choosercontainer #chooseform .options{position:relative;border-bottom:1px solid #BBBBBB}.jschooser .choosercontainer #chooseform .alloptions{overflow-x:hidden;overflow-y:auto;max-width:20.3em;-webkit-box-shadow:inset 0 0 30px 0 #ccc;-moz-box-shadow:inset 0 0 30px 0 #ccc;box-shadow:inset 0 0 30px 0 #ccc}.jschooser .choosercontainer #chooseform .alloptions .option input[type=radio]{display:inline-block}.jschooser .choosercontainer #chooseform .alloptions .option .modicon{display:inline-block}.jschooser .choosercontainer #chooseform .alloptions .option .typename{display:inline-block;width:65%}.dir-rtl.jschooser .choosercontainer #chooseform .alloptions{max-width:18.3em}.choosercontainer #chooseform .moduletypetitle,.choosercontainer #chooseform .option,.choosercontainer #chooseform .nonoption{margin-bottom:0;padding:0 1.6em 0 1.6em}.choosercontainer #chooseform .moduletypetitle{text-transform:uppercase;padding-top:1.2em;padding-bottom:.4em}.choosercontainer #chooseform .option .typename,.choosercontainer #chooseform .option span.modicon img.icon,.choosercontainer #chooseform .nonoption .typename,.choosercontainer #chooseform .nonoption span.modicon img.icon{padding:0 0 0 .5em}.dir-rtl .choosercontainer #chooseform .option .typename,.dir-rtl .choosercontainer #chooseform .option span.modicon img.icon,.dir-rtl .choosercontainer #chooseform .nonoption .typename,.dir-rtl .choosercontainer #chooseform .nonoption span.modicon img.icon{padding:0 .5em 0 0}.chooserdialogue-course-modchooser .choosercontainer #chooseform .option span.modicon img.icon,.chooserdialogue-course-modchooser .choosercontainer #chooseform .nonoption span.modicon img.icon{height:24px;width:24px}.choosercontainer #chooseform .option input[type=radio],.choosercontainer #chooseform .option span.typename,.choosercontainer #chooseform .option span.modicon{vertical-align:middle}.choosercontainer #chooseform .option label{display:block;padding:.3em 0 .1em 0;border-bottom:1px solid #FFFFFF}.choosercontainer #chooseform .nonoption{padding-left:2.7em;padding-top:.3em;padding-bottom:.1em}.dir-rtl .choosercontainer #chooseform .nonoption{padding-right:2.7em;padding-left:0}.choosercontainer #chooseform .subtype{margin-bottom:0;padding:0 1.6em 0 3.2em}.dir-rtl .choosercontainer #chooseform .subtype{padding:0 3.2em 0 1.6em}.choosercontainer #chooseform .subtype .typename{margin:0 0 0 .2em}.dir-rtl .choosercontainer #chooseform .subtype .typename{margin:0 .2em 0 0}.jschooser .choosercontainer #chooseform .instruction,.jschooser .choosercontainer #chooseform .typesummary{display:none;position:absolute;top:0;right:0;bottom:0;left:20.3em;margin:0;padding:1.6em;background-color:#fff;overflow-x:hidden;overflow-y:auto;line-height:2em}.dir-rtl.jschooser .choosercontainer #chooseform .instruction,.dir-rtl.jschooser .choosercontainer #chooseform .typesummary{left:0;right:18.5em;border-right:1px solid grey}.jschooser .choosercontainer #chooseform .instruction,.choosercontainer #chooseform .selected .typesummary{display:block}.choosercontainer #chooseform .selected{background-color:#fff;-webkit-box-shadow:0 0 10px 0 #ccc;-moz-box-shadow:0 0 10px 0 #ccc;box-shadow:0 0 10px 0 #ccc}.section-modchooser-link img.smallicon{padding:3px}.formlistingradio{padding-bottom:25px;padding-right:10px}.formlistinginputradio{float:left}.formlistingmain{min-height:225px}.formlisting{position:relative;margin:15px 0;padding:1px 19px 14px;background-color:white;border:1px solid #DDD;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.formlistingmore{position:absolute;cursor:pointer;bottom:-1px;right:-1px;padding:3px 7px;font-size:12px;font-weight:bold;background-color:whiteSmoke;border:1px solid #ddd;color:#9DA0A4;-webkit-border-radius:4px 0 4px 0;-moz-border-radius:4px 0 4px 0;border-radius:4px 0 4px 0}.formlistingall{margin:15px 0;padding:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.formlistingrow{cursor:pointer;border-bottom:1px solid;border-color:#E1E1E8;border-left:1px solid #E1E1E8;border-right:1px solid #E1E1E8;background-color:#F7F7F9;-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;padding:6px;top:50%;left:50%;min-height:34px;float:left;width:150px}body.jsenabled .formlistingradio{display:none}body.jsenabled .formlisting{display:block}table.collection{width:100%;margin-bottom:20px;border:1px solid #ddd;border-collapse:separate;*border-collapse:collapse;border-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}table.collection th,table.collection td{padding:8px;line-height:20px;text-align:left;vertical-align:top;border-top:1px solid #ddd}table.collection th{font-weight:bold}table.collection thead th{vertical-align:bottom}table.collection caption+thead tr:first-child th,table.collection caption+thead tr:first-child td,table.collection colgroup+thead tr:first-child th,table.collection colgroup+thead tr:first-child td,table.collection thead:first-child tr:first-child th,table.collection thead:first-child tr:first-child td{border-top:0}table.collection tbody+tbody{border-top:2px solid #ddd}table.collection .table{background-color:#fff}table.collection th,table.collection td{border-left:1px solid #ddd}table.collection caption+thead tr:first-child th,table.collection caption+tbody tr:first-child th,table.collection caption+tbody tr:first-child td,table.collection colgroup+thead tr:first-child th,table.collection colgroup+tbody tr:first-child th,table.collection colgroup+tbody tr:first-child td,table.collection thead:first-child tr:first-child th,table.collection tbody:first-child tr:first-child th,table.collection tbody:first-child tr:first-child td{border-top:0}table.collection thead:first-child tr:first-child>th:first-child,table.collection tbody:first-child tr:first-child>td:first-child,table.collection tbody:first-child tr:first-child>th:first-child{-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px}table.collection thead:first-child tr:first-child>th:last-child,table.collection tbody:first-child tr:first-child>td:last-child,table.collection tbody:first-child tr:first-child>th:last-child{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px}table.collection thead:last-child tr:last-child>th:first-child,table.collection tbody:last-child tr:last-child>td:first-child,table.collection tbody:last-child tr:last-child>th:first-child,table.collection tfoot:last-child tr:last-child>td:first-child,table.collection tfoot:last-child tr:last-child>th:first-child{-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px}table.collection thead:last-child tr:last-child>th:last-child,table.collection tbody:last-child tr:last-child>td:last-child,table.collection tbody:last-child tr:last-child>th:last-child,table.collection tfoot:last-child tr:last-child>td:last-child,table.collection tfoot:last-child tr:last-child>th:last-child{-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px}table.collection tfoot+tbody:last-child tr:last-child td:first-child{-webkit-border-bottom-left-radius:0;-moz-border-radius-bottomleft:0;border-bottom-left-radius:0}table.collection tfoot+tbody:last-child tr:last-child td:last-child{-webkit-border-bottom-right-radius:0;-moz-border-radius-bottomright:0;border-bottom-right-radius:0}table.collection caption+thead tr:first-child th:first-child,table.collection caption+tbody tr:first-child td:first-child,table.collection colgroup+thead tr:first-child th:first-child,table.collection colgroup+tbody tr:first-child td:first-child{-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px}table.collection caption+thead tr:first-child th:last-child,table.collection caption+tbody tr:first-child td:last-child,table.collection colgroup+thead tr:first-child th:last-child,table.collection colgroup+tbody tr:first-child td:last-child{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px}table.collection tbody>tr:nth-child(odd)>td,table.collection tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}table.collection .name{text-align:left;vertical-align:middle}table.collection .awards{width:10%;text-align:center;vertical-align:middle}table.collection .criteria{width:40%;text-align:left;vertical-align:top}table.collection .badgeimage,table.collection .status{width:15%;text-align:center;vertical-align:middle}table.collection .description{width:25%;text-align:left}.dir-rtl table.collection .name,.dir-rtl table.collection .criteria,.dir-rtl table.collection .description{text-align:right}table.collection .actions{width:11em;text-align:center;vertical-align:middle}a.criteria-action{padding:0 3px;float:right}div.criteria-description{padding:10px 15px;margin:5px 0;background:none repeat scroll 0 0 #f9f9f9;border:1px solid #EEE}ul.badges{margin:0;list-style:none}.badges li{position:relative;display:inline-block;padding-top:1em;text-align:center;vertical-align:top;width:150px}.badges li .badge-name{display:block;padding:5px}.badges li>img{position:absolute}.badges li .badge-image{width:100px;height:100px;left:10px;top:0;z-index:1}.badges li .badge-actions{position:relative}.badges li .expireimage{width:100px;height:100px;left:25px;top:0;position:absolute;z-index:10;opacity:.85}#badge-image{background-color:transparent;padding:0;position:relative;min-width:100px;width:20%;display:inline-block;vertical-align:top;margin-top:17px}#badge-image .expireimage{width:100px;height:100px;left:0;top:0;opacity:.85;filter:alpha(opacity=85);position:absolute;z-index:10}#badge-image .singlebutton{padding-top:5px}#badge-image .singlebutton input{margin-left:0}.dir-rtl #badge-image{float:right}.dir-rtl #badge-image .expireimage{left:41px}#badge-details{display:inline-block;width:79%}#badge-overview dl,#badge-details dl{margin:0}#badge-overview dl dt,#badge-details dl dt,#badge-overview dl dd,#badge-details dl dd{vertical-align:top;padding:3px 0}#badge-overview dl dt,#badge-details dl dt{clear:both;display:inline-block;width:20%;min-width:100px}#badge-overview dl dd,#badge-details dl dd{display:inline-block;width:79%;margin-left:1%}.badge-profile{vertical-align:top}.connected{color:#468847}.notconnected{color:#b94a48}.connecting{color:#8a6d3b}#page-badges-award .recipienttable tr td{vertical-align:top}#page-badges-award .recipienttable tr td.actions .actionbutton{margin:.3em 0;padding:.5em 0;width:100%}#page-badges-award .recipienttable tr td.existing,#page-badges-award .recipienttable tr td.potential{width:42%}#issued-badge-table .activatebadge{display:inline-block}.statusbox.active{background-color:#dff0d8}.statusbox.inactive{background-color:#fcf8e3}.statusbox{text-align:center;margin-bottom:5px;padding:5px}.statusbox .activatebadge{display:inline-block}.statusbox .activatebadge input[type=submit]{margin:3px}.activatebadge{margin:0;text-align:left;vertical-align:middle}.dir-rtl .activatebadge{text-align:right}img#persona_signin{cursor:pointer}.addcourse{float:right}.invisiblefieldset{display:inline;margin:0;padding:0;border-width:0}.breadcrumb-nav{float:left;margin-bottom:10px}.dir-rtl .breadcrumb-nav{float:right}.breadcrumb-button .singlebutton div{margin-right:0}.breadcrumb-nav .breadcrumb{margin:0}.page-context-header{overflow:hidden}.page-context-header .page-header-image,.page-context-header .page-header-headings{display:block;position:relative}.page-context-header .page-header-image{margin-bottom:1em}.page-context-header .page-header-headings{margin-top:30px;margin-bottom:10px}.page-context-header .page-header-headings h1{display:block}.page-context-header .page-header-headings,.page-context-header .header-button-group{position:relative;line-height:24px;vertical-align:middle}.page-context-header .header-button-group{display:block}.page-context-header .header-button-group a{position:relative;top:-0.4em}.dir-ltr .page-context-header .page-header-image{float:left;margin-right:1em}.dir-ltr .page-context-header .header-button-group{float:left}.dir-rtl .page-context-header .page-header-image{float:right;margin-left:1em}.dir-rtl .page-context-header .header-button-group{float:right}.moodle-actionmenu,.moodle-actionmenu>ul,.moodle-actionmenu>ul>li{display:inline-block}.moodle-actionmenu ul{padding:0;margin:0;list-style-type:none}.section_action_menu .moodle-actionmenu ul.menubar{margin:0}.section_action_menu .moodle-actionmenu ul.menu{margin:0 10px 10px 0}.moodle-actionmenu .toggle-display,.moodle-actionmenu .menu-action-text{display:none}.jsenabled .moodle-actionmenu[data-enhance]{display:block}.jsenabled .moodle-actionmenu[data-enhance] .menu{display:none}.jsenabled .moodle-actionmenu[data-enhance] .toggle-display{display:inline;opacity:.5;filter:alpha(opacity=50)}.jsenabled .moodle-actionmenu[data-enhance] .toggle-display.textmenu{display:block;margin-left:4px;padding-left:4px;padding-right:4px}.jsenabled .moodle-actionmenu[data-enhance] .toggle-display.textmenu .iconsmall,.jsenabled .moodle-actionmenu[data-enhance] .toggle-display.textmenu .smallicon{margin:4px 4px 4px 0;padding:8px 4px 0 2px;vertical-align:text-bottom}.jsenabled .moodle-actionmenu[data-enhance] .toggle-display.textmenu .caret{margin-top:8px;margin-left:2px;border-top-color:#777}.jsenabled .moodle-actionmenu[data-enhance] .toggle-display.textmenu .caret:hover,.jsenabled .moodle-actionmenu[data-enhance] .toggle-display.textmenu .caret:active{border-top-color:#555}.jsenabled .moodle-actionmenu[data-enhanced] .toggle-display{opacity:1;filter:alpha(opacity=100)}.jsenabled .moodle-actionmenu[data-enhanced] .menu-action-text{display:inline}.jsenabled.dir-rtl .moodle-actionmenu[data-enhance] .toggle-display.textmenu{margin-left:initial;margin-right:4px}.jsenabled.dir-rtl .moodle-actionmenu[data-enhance] .toggle-display.textmenu .caret{margin-left:initial;margin-right:2px}.moodle-actionmenu[data-enhanced].show{position:relative}.moodle-actionmenu[data-enhanced].show .menu{display:block;position:absolute;text-align:left;background-color:#fff;border:1px solid rgba(0,0,0,0.2);z-index:1000;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;-webkit-box-shadow:5px 5px 20px 0 #666;-moz-box-shadow:5px 5px 20px 0 #666;box-shadow:5px 5px 20px 0 #666}.moodle-actionmenu[data-enhanced].show .menu a{display:block;color:#333;padding:2px 1em 2px 28px}.moodle-actionmenu[data-enhanced].show .menu a:hover{color:#fff;background-color:#0070a8}.moodle-actionmenu[data-enhanced].show .menu a:first-child{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px}.moodle-actionmenu[data-enhanced].show .menu a:last-child{-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px}.moodle-actionmenu[data-enhanced].show .menu a.hidden{display:none}.moodle-actionmenu[data-enhanced].show .menu img{vertical-align:middle}.moodle-actionmenu[data-enhanced].show .menu .iconsmall,.moodle-actionmenu[data-enhanced].show .menu .smallicon{margin:4px 4px 4px -24px;padding:4px}.moodle-actionmenu[data-enhanced].show .menu>li{display:block}.moodle-actionmenu[data-enhanced].show .menu.align-tl-bl{top:100%;left:0;margin-top:4px}.moodle-actionmenu[data-enhanced].show .menu.align-tr-bl{top:100%;right:100%}.moodle-actionmenu[data-enhanced].show .menu.align-bl-bl{bottom:100%;left:0}.moodle-actionmenu[data-enhanced].show .menu.align-br-bl{bottom:100%;right:100%}.moodle-actionmenu[data-enhanced].show .menu.align-tl-br{top:100%;left:100%}.moodle-actionmenu[data-enhanced].show .menu.align-tr-br{top:100%;right:0;margin-top:4px}.moodle-actionmenu[data-enhanced].show .menu.align-bl-br{bottom:100%;left:100%}.moodle-actionmenu[data-enhanced].show .menu.align-br-br{bottom:100%;right:0}.moodle-actionmenu[data-enhanced].show .menu.align-tl-tl{top:0;left:0}.moodle-actionmenu[data-enhanced].show .menu.align-tr-tl{top:0;right:100%;margin-right:4px}.moodle-actionmenu[data-enhanced].show .menu.align-bl-tl{bottom:100%;left:0;margin-bottom:4px}.moodle-actionmenu[data-enhanced].show .menu.align-br-tl{bottom:100%;right:100%}.moodle-actionmenu[data-enhanced].show .menu.align-tl-tr{top:0;left:100%;margin-left:4px}.moodle-actionmenu[data-enhanced].show .menu.align-tr-tr{top:0;right:0}.moodle-actionmenu[data-enhanced].show .menu.align-bl-tr{bottom:100%;left:100%}.moodle-actionmenu[data-enhanced].show .menu.align-br-tr{bottom:100%;right:0;margin-bottom:4px}.moodle-actionmenu[data-enhanced].show.nowrap-items .menu>li{white-space:nowrap}.block .moodle-actionmenu{text-align:right}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu{text-align:right;left:0;right:auto}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu a{padding:2px 28px 2px 1em}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu .iconsmall,.dir-rtl .moodle-actionmenu[data-enhanced].show .menu .smallicon{margin-right:-24px;margin-left:4px}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-tl-bl{left:auto;right:0}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-tr-bl{right:auto;left:100%}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-bl-bl{left:auto;right:0}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-br-bl{right:auto;left:100%}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-tl-br{left:auto;right:100%}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-tr-br{right:auto;left:0}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-bl-br{left:auto;right:100%}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-br-br{right:auto;left:0}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-tl-tl{left:auto;right:0}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-tr-tl{right:auto;left:100%}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-bl-tl{left:auto;right:0}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-br-tl{right:auto;left:100%}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-tl-tr{left:auto;right:100%}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-tr-tr{right:auto;left:0}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-bl-tr{left:auto;right:100%}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-br-tr{right:auto;left:0}.dir-rtl .block .moodle-actionmenu{text-align:right}ul.dragdrop-keyboard-drag li{list-style-type:none}.block-control-actions .moodle-core-dragdrop-draghandle img{width:12px;height:12px}a.disabled:hover,a.disabled{text-decoration:none;cursor:default;font-style:italic;color:#808080}body.lockscroll{height:100%;overflow:hidden}.dir-rtl ul{margin-left:0;margin-right:25px}.progressbar_container{max-width:500px;margin:0 auto}.ie10 .yui3-calendar-header-label{display:inline-block}dd:before,dd:after{display:block;content:" "}dd:after{clear:both}.nav-tabs>.active>a[href],.nav-tabs>.active>a[href]:hover,.nav-tabs>.active>a[href]:focus{cursor:pointer}span.inplaceeditable.inplaceeditingon{position:relative}span.inplaceeditable.inplaceeditingon span.editinstructions{margin-top:-30px;font-weight:normal;margin-right:-300px;margin-left:0}.dir-rtl span.inplaceeditable.inplaceeditingon span.editinstructions{margin-left:-300px;margin-right:0}.inplaceeditable.inplaceeditingon{position:relative}.inplaceeditable.inplaceeditingon .editinstructions{margin-top:-30px;font-weight:normal;margin-right:-300px;margin-left:0}.inplaceeditable.inplaceeditingon input{width:330px;height:16px;vertical-align:text-bottom;margin-bottom:0}.inplaceeditable.inplaceeditingon select{margin-bottom:0}.inplaceeditable .quickediticon img{opacity:.2}.inplaceeditable .quickeditlink{color:inherit;text-decoration:inherit}.inplaceeditable:hover .quickeditlink .quickediticon img,.inplaceeditable .quickeditlink:focus .quickediticon img{opacity:1}.inplaceeditable.inplaceeditable-toggle .quickediticon{display:none}.dir-rtl .inplaceeditable.inplaceeditingon .editinstructions{margin-left:-300px;margin-right:0}h3.sectionname .inplaceeditable.inplaceeditingon .editinstructions{margin-top:-20px}.formtable tbody th{font-weight:normal;text-align:right}.path-admin #assignrole{width:60%;margin-left:auto;margin-right:auto}.path-admin .admintable .leftalign{text-align:left}.dir-rtl.path-admin .admintable .leftalign{text-align:right}.environmenttable p.warn{background-color:#fcf8e3;color:#8a6d3b}.environmenttable .error,.environmenttable span.warn,.environmenttable .ok{display:inline-block;padding:2px 4px;font-size:11.844px;font-weight:bold;line-height:14px;color:#fff;vertical-align:baseline;white-space:nowrap;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.environmenttable .error:empty,.environmenttable span.warn:empty,.environmenttable .ok:empty{display:none}.environmenttable .error-important,.environmenttable span.warn-important,.environmenttable .ok-important{background-color:#b94a48}.environmenttable .error-important[href],.environmenttable span.warn-important[href],.environmenttable .ok-important[href]{background-color:#953b39}.environmenttable .error-warning,.environmenttable span.warn-warning,.environmenttable .ok-warning{background-color:#f89406}.environmenttable .error-warning[href],.environmenttable span.warn-warning[href],.environmenttable .ok-warning[href]{background-color:#c67605}.environmenttable .error-success,.environmenttable span.warn-success,.environmenttable .ok-success{background-color:#468847}.environmenttable .error-success[href],.environmenttable span.warn-success[href],.environmenttable .ok-success[href]{background-color:#356635}.environmenttable .error-info,.environmenttable span.warn-info,.environmenttable .ok-info{background-color:#3a87ad}.environmenttable .error-info[href],.environmenttable span.warn-info[href],.environmenttable .ok-info[href]{background-color:#2d6987}.environmenttable .error-inverse,.environmenttable span.warn-inverse,.environmenttable .ok-inverse{background-color:#333}.environmenttable .error-inverse[href],.environmenttable span.warn-inverse[href],.environmenttable .ok-inverse[href]{background-color:#1a1a1a}.environmenttable .error{background-color:#b94a48}.environmenttable span.warn{background-color:#f89406}.environmenttable .ok{background-color:#468847}.path-admin .admintable.environmenttable .name,.path-admin .admintable.environmenttable .info,.path-admin #assignrole .admintable .role,.path-admin #assignrole .admintable .userrole,.path-admin #assignrole .admintable .roleholder{white-space:nowrap}.path-admin .incompatibleblockstable td.c0{font-weight:bold}#page-admin-course-category .addcategory{padding:10px}#page-admin-course-index .editcourse{margin:20px auto}#page-admin-course-index .editcourse th,#page-admin-course-index .editcourse td{padding-left:10px;padding-right:10px}.timewarninghidden{display:none}.statusok,.statuswarning,.statusserious,.statuscritical{display:inline-block;padding:2px 4px;font-size:11.844px;font-weight:bold;line-height:14px;color:#fff;vertical-align:baseline;white-space:nowrap;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.statusok:empty,.statuswarning:empty,.statusserious:empty,.statuscritical:empty{display:none}.statusok-important,.statuswarning-important,.statusserious-important,.statuscritical-important{background-color:#b94a48}.statusok-important[href],.statuswarning-important[href],.statusserious-important[href],.statuscritical-important[href]{background-color:#953b39}.statusok-warning,.statuswarning-warning,.statusserious-warning,.statuscritical-warning{background-color:#f89406}.statusok-warning[href],.statuswarning-warning[href],.statusserious-warning[href],.statuscritical-warning[href]{background-color:#c67605}.statusok-success,.statuswarning-success,.statusserious-success,.statuscritical-success{background-color:#468847}.statusok-success[href],.statuswarning-success[href],.statusserious-success[href],.statuscritical-success[href]{background-color:#356635}.statusok-info,.statuswarning-info,.statusserious-info,.statuscritical-info{background-color:#3a87ad}.statusok-info[href],.statuswarning-info[href],.statusserious-info[href],.statuscritical-info[href]{background-color:#2d6987}.statusok-inverse,.statuswarning-inverse,.statusserious-inverse,.statuscritical-inverse{background-color:#333}.statusok-inverse[href],.statuswarning-inverse[href],.statusserious-inverse[href],.statuscritical-inverse[href]{background-color:#1a1a1a}.statusok{background-color:#468847}.statuswarning{background-color:#8a6d3b}.statusserious{background-color:#f89406}.statuscritical{background-color:#b94a48}#page-admin-report-capability-index #capabilitysearch{width:30em}#page-admin-report-backups-index .backup-error,#page-admin-report-backups-index .backup-unfinished{color:#b94a48}#page-admin-report-backups-index .backup-skipped,#page-admin-report-backups-index .backup-ok,#page-admin-report-backups-index .backup-notyetrun{color:#468847}#page-admin-report-backups-index .backup-warning{color:#8a6d3b}#page-admin-qtypes .disabled,#page-admin-qbehaviours .disabled{color:#999}#page-admin-qtypes #qtypes div,#page-admin-qtypes #qtypes form,#page-admin-qbehaviours #qbehaviours div,#page-admin-qbehaviours #qbehaviours form{display:inline}#page-admin-qtypes #qtypes img.spacer,#page-admin-qbehaviours #qbehaviours img.spacer{width:16px}img.iconsmall{margin:0;padding:.3em}#page-admin-qbehaviours .cell.c3,#page-admin-qtypes .cell.c3{font-size:10.5px}#page-admin-lang .generalbox,#page-admin-course-index .singlebutton,#page-admin-course-index .addcategory,#page-course-index .buttons,#page-course-index-category .buttons,#page-admin-course-category .addcategory,#page-admin-stickyblocks .generalbox,#page-admin-maintenance .buttons,#page-admin-course-index .buttons,#page-admin-course-category .buttons,#page-admin-index .copyright,#page-admin-index .copyrightnotice,#page-admin-index .adminerror .singlebutton,#page-admin-index .adminwarning .singlebutton,#page-admin-index #layout-table .singlebutton{text-align:center;margin-bottom:1em}.path-admin-roles .capabilitysearchui{text-align:left;margin-left:auto;margin-right:auto}#page-admin-roles-define .topfields{margin:1em 0 2em}#page-admin-roles-define .capdefault{background-color:#f5f5f5;border:1px solid #ddd}#page-filter-manage .backlink,.path-admin-roles .backlink{margin-top:1em}#page-admin-roles-explain #chooseuser h3,#page-admin-roles-usersroles .contextname{margin-top:0}#page-admin-roles-explain #chooseusersubmit{margin-top:0;text-align:center}#page-admin-roles-usersroles p{margin:0}#page-admin-roles-override .cell.c1,#page-admin-roles-assign .cell.c3,#page-admin-roles-assign .cell.c1{padding-top:.75em}#page-admin-roles-override .overridenotice,#page-admin-roles-define .definenotice{margin:1em 10% 2em 10%;text-align:left}#notice{width:60%;min-width:220px;margin:auto}#page-admin-index .releasenoteslink,#page-admin-index .adminwarning,#page-admin-index .adminerror{margin:auto;padding:8px 35px 8px 14px;margin-bottom:20px;text-shadow:0 1px 0 rgba(255,255,255,0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;color:#8a6d3b;width:60%;min-width:220px}#page-admin-index .adminerror{background-color:#f2dede;border-color:#eed3d7;color:#b94a48}#page-admin-index .releasenoteslink{background-color:#d9edf7;border-color:#bce8f1;color:#3a87ad}#page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo span{display:block}#page-admin-index .updateplugin div{margin-bottom:.5em}#page-admin-index .updateplugin .updatepluginconfirmexternal{padding:1em;background-color:#f2dede;border:1px solid #eed3d7}#page-admin-user-user_bulk #users .fgroup{white-space:nowrap}#page-admin-report-stats-index .graph{text-align:center;margin-bottom:1em}#page-admin-report-courseoverview-index .graph{text-align:center;margin-bottom:1em}#page-admin-lang .translator{border-width:1px;border-style:solid}.path-admin .roleassigntable{width:100%}.path-admin .roleassigntable td{vertical-align:top;padding:.2em .3em}.path-admin .roleassigntable p{text-align:left;margin:.2em 0}.path-admin .roleassigntable #existingcell,.path-admin .roleassigntable #potentialcell{width:42%}.path-admin .roleassigntable #existingcell p>label:first-child,.path-admin .roleassigntable #potentialcell p>label:first-child{font-weight:bold}.path-admin .roleassigntable #buttonscell{width:16%}.path-admin .roleassigntable #buttonscell #assignoptions{font-size:10.5px}.path-admin .roleassigntable #removeselect_wrapper,.path-admin .roleassigntable #addselect_wrapper{width:100%}.path-admin table.rolecap tr.rolecap th{text-align:left;font-weight:normal}.path-admin.dir-rtl table.rolecap tr.rolecap th{text-align:right}.path-admin .rolecap .hiddenrow{display:none}.path-admin #defineroletable .rolecap .inherit,.path-admin #defineroletable .rolecap .allow,.path-admin #defineroletable .rolecap .prevent,.path-admin #defineroletable .rolecap .prohibit{text-align:center;padding:0;min-width:3.5em}.path-admin .rolecap .cap-name,.path-admin .rolecap .note{display:block;font-size:10.5px;white-space:nowrap;font-weight:normal}.path-admin .rolecap label{display:block;text-align:center;padding:.5em;margin:0}.plugincheckwrapper{width:100%}.environmentbox{margin-top:1em}#mnetconfig table{margin-left:auto;margin-right:auto}.environmenttable .cell{padding:.15em .5em}.environmenttable img.iconhelp{padding-right:.3em}.dir-rtl .environmenttable img.iconhelp{padding-left:.3em;padding-right:0}#trustedhosts .generaltable{margin-left:auto;margin-right:auto;width:500px}#trustedhosts .standard{width:auto}#adminsettings legend{display:none}#adminsettings fieldset.error{margin:.2em 0 .5em 0}#adminsettings fieldset.error legend{display:block}.dir-rtl #admin-spelllanguagelist textarea,#page-admin-setting-editorsettingstinymce.dir-rtl .form-textarea textarea{text-align:left;direction:ltr}.adminsettingsflags{float:right}.dir-rtl .adminsettingsflags{float:left}.adminsettingsflags label{margin-right:7px}.dir-rtl .adminsettingsflags label{margin-left:7px}.form-description{clear:right}.dir-rtl .form-description{clear:left}.form-item .form-setting .form-htmlarea{width:640px;display:inline}.form-item .form-setting .form-htmlarea .htmlarea{width:640px;display:block}.form-item .form-setting .form-multicheckbox ul{list-style:none;padding:0;margin:7px 0 0 0}.form-item .form-setting .defaultsnext{margin-right:.5em;display:inline}.dir-rtl .form-item .form-setting .defaultsnext{margin-left:.5em;margin-right:0}.form-item .form-setting .locked-checkbox{margin-right:.2em;margin-left:.5em;display:inline}.dir-rtl .form-item .form-setting .locked-checkbox{margin-right:.5em;margin-left:.2em;display:inline}.form-item .form-setting .form-password .unmask,.form-item .form-setting .form-defaultinfo{display:inline-block}.form-item .pathok,.form-item .patherror{margin-left:.5em}#admin-emoticons td input{width:8em}#admin-emoticons td.c0 input{width:4em}#adminthemeselector .selectedtheme td.c0{border:1px solid #000;border-right-width:0}#adminthemeselector .selectedtheme td.c1{border:1px solid #000;border-left-width:0}.admin_colourpicker,.admin_colourpicker_preview{display:none}.jsenabled .admin_colourpicker_preview{display:inline}.jsenabled .admin_colourpicker{display:block;height:102px;width:410px;margin-bottom:10px}.admin_colourpicker .loadingicon{vertical-align:middle;margin-left:auto}.admin_colourpicker .colourdialogue{float:left;border:1px solid #000}.admin_colourpicker .previewcolour{border:1px solid #000;margin-left:301px}.admin_colourpicker .currentcolour{border:1px solid #000;margin-left:301px;border-top-width:0}.dir-rtl .form-item .form-setting,.dir-rtl .form-item .form-label,.dir-rtl .form-item .form-description,.dir-rtl.path-admin .roleassigntable p{text-align:right}#page-admin-index #notice .checkforupdates{text-align:center}#page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo.maturity200 .info.release{background-color:#d9edf7}#page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo.maturity100 .info.release,#page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo.maturity150 .info.release{background-color:#fcf8e3}#page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo.maturity50 .info.release{background-color:#f2dede}#page-admin-plugins #plugins-overview-panel .info{display:inline-block;margin-right:1em}#page-admin-plugins .checkforupdates{margin:10px 0}#page-admin-plugins .checkforupdates .singlebutton{margin:5px 0;padding:0}#page-admin-plugins .checkforupdates .singlebutton div,#page-admin-plugins .checkforupdates .singlebutton input{margin:0 3px 0 0}#page-admin-plugins .updateavailableinstallall{margin:5px 0;padding:0}#page-admin-plugins .updateavailableinstallall div,#page-admin-plugins .updateavailableinstallall input{margin:0 3px 5px 0}#page-admin-plugins #plugins-control-panel .status-missing td{background-color:#f2dede}#page-admin-plugins #plugins-control-panel .pluginname .displayname img.icon{padding-top:0;padding-bottom:0}#page-admin-plugins #plugins-control-panel .pluginname .componentname{font-size:11.9px;color:#999;margin-left:22px}#page-admin-plugins #plugins-control-panel .version .versionnumber{font-size:11.9px;color:#999}#page-admin-plugins #plugins-control-panel .uninstall a{color:#b94a48}#page-admin-plugins #plugins-control-panel .notes .label{margin-right:3px}#page-admin-plugins #plugins-control-panel .notes .requiredby{font-size:11.9px;color:#999}#plugins-check-page .page-description{color:#999}#plugins-check-page .checkforupdates .singlebutton{margin:5px 0;padding:0}#plugins-check-page .checkforupdates .singlebutton div,#plugins-check-page .checkforupdates .singlebutton input{margin:0 3px 0 0}#plugins-check-page #plugins-check-info .actions>div{display:inline-block;margin-right:1em}#plugins-check-page #plugins-check-info .actions .singlebutton{margin:5px 0;padding:0}#plugins-check-page #plugins-check-info .actions .singlebutton div,#plugins-check-page #plugins-check-info .actions .singlebutton input{margin:0 3px 0 0}#plugins-check-page #plugins-check .requires-ok{color:#999}#plugins-check-page #plugins-check .status-missing td,#plugins-check-page #plugins-check .status-downgrade td{background-color:#f2dede}#plugins-check-page #plugins-check .displayname .pluginicon{margin-right:5px;width:16px}#plugins-check-page #plugins-check .displayname .plugindir{color:#999;font-size:11.9px}#plugins-check-page #plugins-check .requires ul{margin-left:13px}#plugins-check-page #plugins-check .status .actionbutton{margin:5px 0;padding:0}#plugins-check-page #plugins-check .status .actionbutton input{margin:0}#plugins-check-page .plugins-check-dependencies-actions>div{display:inline-block;margin-right:1em}#plugins-check-page .plugins-check-dependencies-actions .singlebutton{margin:5px 0;padding:0}#plugins-check-page .plugins-check-dependencies-actions .singlebutton div,#plugins-check-page .plugins-check-dependencies-actions .singlebutton input{margin:0 3px 0 0}#plugins-check-page #plugins-check-available-dependencies .displayname .component{font-size:11.9px;color:#999}#plugins-check-page #plugins-check-available-dependencies .info .actions>div{display:inline-block;margin-right:1em}#plugins-check-page #plugins-check-available-dependencies .info .actions .dependencyinstall{display:block;margin:5px 0;padding:0}#plugins-check-page #plugins-check-available-dependencies .info .actions .dependencyinstall input{margin:0}#plugins-check-page .pluginupdateinfo,#plugins-control-panel .pluginupdateinfo{background-color:#d9edf7;padding:5px;margin:10px 0;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}#plugins-check-page .pluginupdateinfo.maturity50,#plugins-control-panel .pluginupdateinfo.maturity50{background-color:#f2dede}#plugins-check-page .pluginupdateinfo.maturity100,#plugins-control-panel .pluginupdateinfo.maturity100,#plugins-check-page .pluginupdateinfo.maturity150,#plugins-control-panel .pluginupdateinfo.maturity150{background-color:#fcf8e3}#plugins-check-page .pluginupdateinfo .info,#plugins-control-panel .pluginupdateinfo .info{display:inline-block}#plugins-check-page .pluginupdateinfo .separator:after,#plugins-control-panel .pluginupdateinfo .separator:after{content:" | "}#plugins-check-page .pluginupdateinfo .singlebutton,#plugins-control-panel .pluginupdateinfo .singlebutton{margin:5px 0;padding:0}#plugins-check-page .pluginupdateinfo .singlebutton div,#plugins-control-panel .pluginupdateinfo .singlebutton div,#plugins-check-page .pluginupdateinfo .singlebutton input,#plugins-control-panel .pluginupdateinfo .singlebutton input{margin:0 3px 0 0}.plugins-management-confirm-buttons>div{display:inline-block;margin:1em 1em 1em 0}.plugins-management-confirm-buttons .continue{padding:0}.plugins-management-confirm-buttons .continue div,.plugins-management-confirm-buttons .continue input{margin:0}.uninstalldeleteconfirmexternal{background-color:#fcf8e3;padding:.5em 1em;margin:5px 0 10px 0}#page-admin-index .upgradepluginsinfo{text-align:center}#page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo .separator:after{content:" | "}.dir-rtl #plugins-check .pluginupdateinfo{text-align:center;direction:ltr}.dir-rtl #plugins-check .requires-ok{text-align:left;direction:ltr}#page-admin-mnet-peers .box.deletedhosts{margin-bottom:1em;font-size:11.9px}#page-admin-mnet-peers .mform .deletedhostinfo{background-color:#f2dede;border:2px solid #eed3d7;padding:4px;margin-bottom:5px}#core-cache-plugin-summaries table,#core-cache-store-summaries table{width:100%}#core-cache-lock-summary table,#core-cache-definition-summaries table,#core-cache-mode-mappings table{margin:0 auto}#core-cache-store-summaries .default-store td{font-style:italic}#core-cache-rescan-definitions,#core-cache-mode-mappings .edit-link,#core-cache-lock-summary .new-instance{margin-top:.5em;text-align:center}.tinymcesubplugins img.icon{padding-top:0;padding-bottom:0}.maintenancewarning{padding:3px 1em;text-align:center;position:fixed;bottom:0;right:0;overflow:hidden;z-index:1}.maintenancewarning.error{color:#b94a48;background-color:#f2dede;border:2px solid #eed3d7;font-weight:bold}.maintenancewarning.warning{color:#8a6d3b;background-color:#fcf8e3;border:2px solid #fbeed5}#adminsettings .form-overridden{color:#3a87ad;background-color:#d9edf7}.calendar_event_course{background-color:#ffd3bd}.calendar_event_global{background-color:#d6f8cd}.calendar_event_group{background-color:#fee7ae}.calendar_event_user{background-color:#dce7ec}.path-calendar .calendartable{width:100%}.path-calendar .calendartable th,.path-calendar .calendartable td{width:14%;vertical-align:top;text-align:center;border:0}.path-calendar .calendar-controls .previous,.path-calendar .calendar-controls .next,.path-calendar .calendar-controls .current{display:block;float:left;width:12%}.path-calendar .calendar-controls .previous{text-align:left}.path-calendar .calendar-controls .current{text-align:center;width:76%}.path-calendar .calendar-controls .next{text-align:right}.path-calendar .filters table{border-collapse:separate;border-spacing:2px;width:100%}.path-calendar .cal_courses_flt{float:left}.path-calendar .cal_courses_flt label{margin-right:.45em}.path-calendar .maincalendar{vertical-align:top;padding:0}.path-calendar .maincalendar .bottom{text-align:center;padding:5px 0 0 0}.path-calendar .maincalendar .heightcontainer{height:100%;position:relative}.path-calendar .maincalendar .calendarmonth{width:98%;margin:10px auto}.path-calendar .maincalendar .calendarmonth ul{margin:0}.path-calendar .maincalendar .calendarmonth ul li{list-style-type:none;margin-top:4px}.path-calendar .maincalendar .calendarmonth td{height:5em}.path-calendar .maincalendar .calendar-controls .previous,.path-calendar .maincalendar .calendar-controls .next{width:30%}.path-calendar .maincalendar .calendar-controls .current{width:39.95%}.path-calendar .maincalendar .controls{width:98%;margin:10px auto}.path-calendar .maincalendar .calendar_event_course,.path-calendar .maincalendar .calendar_event_global,.path-calendar .maincalendar .calendar_event_group,.path-calendar .maincalendar .calendar_event_user{border-width:1px 1px 1px 12px;border-style:solid}.path-calendar .maincalendar .calendar_event_course{border-color:#ffd3bd}.path-calendar .maincalendar .calendar_event_global{border-color:#d6f8cd}.path-calendar .maincalendar .calendar_event_group{border-color:#fee7ae}.path-calendar .maincalendar .calendar_event_user{border-color:#dce7ec}.path-calendar .maincalendar .calendar-event-panel{background-color:#eee;border:2px solid #eee}.path-calendar .maincalendar .calendar-event-panel .yui3-overlay-content{padding:19px;background-color:#fdfdfd;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.path-calendar .maincalendar .calendar-controls .current{font-family:inherit;font-weight:bold;color:inherit;font-size:25px;line-height:1.2}.path-calendar .maincalendar .calendartable td,.path-calendar .maincalendar .calendartable li{padding:5px}.path-calendar .maincalendar .calendartable li{padding-left:10px;text-align:left}.path-calendar .maincalendar .header{overflow:hidden}.path-calendar .maincalendar .header .buttons{float:right}.path-calendar .maincalendar .eventlist{margin:0}.path-calendar .maincalendar .eventlist .event{width:92%;border-spacing:0;border-collapse:separate;position:relative;padding:20px 4%;margin-bottom:20px;background-color:#fdfdfd;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);list-style-type:none}.path-calendar .maincalendar .eventlist .event>img{padding-top:3px;float:left}.path-calendar .maincalendar .eventlist .event .name{font-size:17.5px;font-weight:200;line-height:24px;float:left;margin:0}.path-calendar .maincalendar .eventlist .event .name,.path-calendar .maincalendar .eventlist .event .course{margin-bottom:5px}.path-calendar .maincalendar .eventlist .event .date{float:right}.path-calendar .maincalendar .eventlist .event .course,.path-calendar .maincalendar .eventlist .event .subscription{float:left;clear:left}.path-calendar .maincalendar .eventlist .event .side{width:22px}.path-calendar .maincalendar .eventlist .event .description{background-color:#fff;padding:5px;clear:both}.path-calendar .maincalendar .eventlist .event .description .commands{position:absolute;right:0;top:0;margin:3px}.path-calendar .maincalendar .eventlist .event .commands{position:absolute;top:2px;right:2px}.path-calendar .maincalendar .eventlist .event .commands a{margin:0 3px}.dir-rtl.path-calendar .cal_courses_flt{float:right}.dir-rtl.path-calendar .cal_courses_flt label{margin-left:.45em;margin-right:0}.dir-rtl.path-calendar .maincalendar .calendar_event_course,.dir-rtl.path-calendar .maincalendar .calendar_event_global,.dir-rtl.path-calendar .maincalendar .calendar_event_group,.dir-rtl.path-calendar .maincalendar .calendar_event_user{border-left-width:1px;border-right-width:12px}.dir-rtl.path-calendar .maincalendar .calendar-controls .next{text-align:left}.dir-rtl.path-calendar .maincalendar .calendar-controls .previous{text-align:right}.dir-rtl.path-calendar .maincalendar .calendartable td,.dir-rtl.path-calendar .maincalendar .calendartable li{text-align:right}.dir-rtl.path-calendar .maincalendar .calendartable li{padding-right:10px;padding-left:5px}.dir-rtl.path-calendar .maincalendar .header .buttons{float:left}.dir-rtl.path-calendar .maincalendar .eventlist .event>img{float:right}.dir-rtl.path-calendar .maincalendar .eventlist .event .name{float:right}.dir-rtl.path-calendar .maincalendar .eventlist .event .date{float:left}.dir-rtl.path-calendar .maincalendar .eventlist .event .description .commands{right:inherit;left:0}.dir-rtl.path-calendar .maincalendar .eventlist .event .course,.dir-rtl.path-calendar .maincalendar .eventlist .event .subscription{float:right;clear:right}.dir-rtl.path-calendar .maincalendar .eventlist .event .commands{left:2px;right:inherit}#page-calendar-export .indent{padding-left:20px}.block .minicalendar{max-width:280px;margin:0 auto;width:100%}.block .minicalendar th,.block .minicalendar td{padding:2px;font-size:.8em;text-align:center}.block .minicalendar td.weekend{color:#999}.block .minicalendar td a{width:100%;height:100%;display:block}.block .minicalendar td.duration_global{border-top:1px solid #d6f8cd;border-bottom:1px solid #d6f8cd}.block .minicalendar td.duration_global.duration_finish{background-color:#d6f8cd}.block .minicalendar td.duration_course{border-top:1px solid #ffd3bd;border-bottom:1px solid #ffd3bd}.block .minicalendar td.duration_course.duration_finish{background-color:#ffd3bd}.block .minicalendar td.duration_group{border-top:1px solid #fee7ae;border-bottom:1px solid #fee7ae}.block .minicalendar td.duration_group.duration_finish{background-color:#fee7ae}.block .minicalendar td.duration_user{border-top:1px solid #dce7ec;border-bottom:1px solid #dce7ec}.block .minicalendar td.duration_user.duration_finish{background-color:#dce7ec}.block .minicalendar caption{font-size:inherit;font-weight:inherit;line-height:inherit;text-align:center}.block .calendar-event-panel{background-color:#eee;border:1px solid #eee}.block .calendar-event-panel .yui3-overlay-content{padding:19px;background-color:#fdfdfd;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.block .calendar-event-panel .yui3-overlay-content h2.eventtitle{line-height:1.2;font-size:18px}.block .calendar-event-panel .yui3-overlay-content .eventcontent img{padding-right:5px}.block .calendar-controls .previous,.block .calendar-controls .current,.block .calendar-controls .next{display:block;float:left}.block .calendar-controls .previous{text-align:left;width:12%}.block .calendar-controls .current{text-align:center;width:76%}.block .calendar-controls .next{text-align:right;width:12%}.block .calendar_filters ul{list-style:none;margin:0}.block .calendar_filters li{margin-bottom:.2em}.block .calendar_filters li span img{padding:0 .2em}.block .calendar_filters .eventname{padding-left:.2em}.block .content h3.eventskey{margin-top:.5em}.dir-rtl .block .calendar_filters .eventname{padding-right:.2em;padding-left:0}.dir-rtl .block .calendar-event-panel .yui3-overlay-content .eventcontent img{padding-right:0;padding-left:5px}.ical-link{font-size:10px;font-weight:bold;background-color:#f60;padding:0 5px;color:#fff;border-top:1px solid #f93;border-left:1px solid #f93;border-bottom:1px solid #013;border-right:1px solid #013}.ical-link:hover,.ical-link:active,.ical-link:focus,.ical-link:visited{color:#fff;text-decoration:none}@media (min-width:768px){#page-calender-view .container-fluid{min-width:1024px}}.section_add_menus{text-align:right;clear:both}.section-modchooser{clear:both}.dir-rtl .section_add_menus{text-align:left;clear:both}.section_add_menus .horizontal div,.section_add_menus .horizontal form{display:inline}.section_add_menus optgroup{font-weight:normal;font-style:italic}.section_add_menus .urlselect{margin-left:.4em}.dir-rtl .section_add_menus .urlselect{margin-right:.4em;margin-left:0}.section_add_menus .urlselect select{margin-left:.2em}.dir-rtl .section_add_menus .urlselect select{margin-right:.2em;margin-left:0}.section_add_menus .urlselect img.iconhelp{padding:0;margin:0;vertical-align:text-bottom}.sitetopic ul.section{margin:0}.course-content ul.section{margin:1em}.section .side.left{float:left}.section .side.right{float:right}.section .spinner{height:16px;width:16px}.section .activity .spinner{left:100%;position:absolute;vertical-align:text-bottom}.section .activity .editing_move{position:absolute;left:0;top:0}.section .activity .mod-indent-outer{padding-left:32px}.section .activity .actions{position:absolute;right:0;top:0}.section .activity .contentwithoutlink,.section .activity .activityinstance{min-width:40%;display:table-cell;padding-right:4px;min-height:2em}.section .activity .contentwithoutlink .dimmed img.activityicon,.section .activity .activityinstance .dimmed img.activityicon{opacity:.5;filter:alpha(opacity=50)}.section .label .contentwithoutlink,.section .label .activityinstance{padding-right:32px;display:block;height:inherit}.section .label .mod-indent-outer{padding-left:24px;display:block}.section .filler{width:16px;height:16px;padding:.3em;display:inline-block}.section .activity.editor_displayed a.editing_title,.section .activity.editor_displayed .moodle-actionmenu{display:none}.section .activity.editor_displayed div.activityinstance{padding-right:initial}.section .activity.editor_displayed div.activityinstance input{margin-bottom:initial;padding-top:initial;padding-bottom:initial;vertical-align:text-bottom}.dir-rtl .section .side.left{float:right}.dir-rtl .section .side.right{float:left}.dir-rtl .section .activity .spinner{left:auto;right:100%}.dir-rtl .section .activity .mod-indent-outer{padding-left:initial;padding-right:32px}.dir-rtl .section .activity .actions{left:0;right:auto}.dir-rtl .section .activity .contentwithoutlink,.dir-rtl .section .activity .activityinstance{padding-left:4px;padding-right:initial}.dir-rtl .section .activity .editing_move{left:auto;right:0}.dir-rtl .section .activity.editor_displayed div.activityinstance{padding-left:initial}.activity img.activityicon{margin-right:6px;vertical-align:text-bottom}.dir-rtl .section .activity img.activityicon{margin-left:6px;margin-right:0}.section .activity .activityinstance,.section .activity .activityinstance div{display:inline-block}.editing .section .activity .contentwithoutlink,.editing .section .activity .activityinstance{padding-right:200px}.dir-rtl.editing .section .activity .contentwithoutlink,.dir-rtl.editing .section .activity .activityinstance{padding-left:200px;padding-right:0}.editing_show+.editing_assign,.editing_hide+.editing_assign{margin-left:20px}.section .activity .commands{white-space:nowrap;display:inline}.section .activity.modtype_label.label{font-weight:normal;padding:.2em}.section li.activity{padding:.2em;clear:both}.section .activity .activityinstance .groupinglabel{padding-left:30px}.dir-rtl .section .activity .activityinstance .groupinglabel{padding-right:30px}.section .activity .availabilityinfo,.section .activity .contentafterlink{margin-top:.5em;margin-left:30px}.dir-rtl .section .activity .availabilityinfo,.dir-rtl .section .activity .contentafterlink{margin-left:0;margin-right:30px}.section .activity .contentafterlink p{margin:.5em 0}.editing .section .activity:hover,.editing .section .activity.action-menu-shown{background-color:#eee}.course-content .current{background-color:#d9edf7}.course-content .section-summary{border:1px solid #ddd;margin-top:5px;list-style:none}.course-content .section-summary .section-title{margin:2px 5px 10px 5px}.course-content .section-summary .summarytext{margin:2px 5px 2px 5px}.course-content .section-summary .section-summary-activities .activity-count{color:#999;font-size:11.9px;margin:3px;white-space:nowrap;display:inline-block}.course-content .section-summary .summary{margin-top:5px}.course-content .single-section{margin-top:1em}.course-content .single-section .section-navigation{display:block;padding:.5em;margin-bottom:-0.5em}.course-content .single-section .section-navigation .title{font-weight:bold;font-size:108%;clear:both}.course-content .single-section .section-navigation .mdl-left{font-weight:normal;float:left;margin-right:1em}.dir-rtl .course-content .single-section .section-navigation .mdl-left{float:right}.course-content .single-section .section-navigation .mdl-left .larrow{margin-right:.1em}.course-content .single-section .section-navigation .mdl-right{font-weight:normal;float:right;margin-left:1em}.dir-rtl .course-content .single-section .section-navigation .mdl-right{float:left}.course-content .single-section .section-navigation .mdl-right .rarrow{margin-left:.1em}.course-content .single-section .section-navigation .mdl-bottom{margin-top:0}.course-content ul li.section.main{border-bottom:2px solid #ddd;margin-top:0}.course-content ul li.section.hidden .sectionname>span,.course-content ul li.section.hidden .content>div,.course-content ul li.section.hidden .activity .activityinstance{opacity:.5}.course-content ul li.section.hidden .sectionname>span,.course-content ul li.section.hidden .activity .activityinstance{margin-left:10px;margin-right:10px}.course-content ul.topics li.section .content,.course-content ul.weeks li.section .content{margin-right:20px;margin-left:20px;padding:0}.course-content{margin-top:0}.course-content ul.topics li.section{padding-bottom:20px}.course-content ul.topics li.section .summary{margin-left:25px}.course-content li.section ul{list-style:disc}.course-content li.section ul ul{list-style:circle}.course-content li.section ul ul ul{list-style:square}.course-content li.section li.activity ul{list-style:disc}.course-content li.section li.activity ul ul{list-style:circle}.course-content li.section li.activity ul ul ul{list-style:square}.path-course-view .completionprogress{margin-left:25px}.path-course-view .completionprogress{display:block;float:right;height:20px;position:relative}#page-site-index .subscribelink{text-align:right}#site-news-forum h2,#frontpage-course-list h2,#frontpage-category-names h2,#frontpage-category-combo h2{margin-bottom:9px}.path-course-view a.reduce-sections{padding-left:.2em}.path-course-view .subscribelink{text-align:right}.path-course-view .unread{margin-left:30px}.dir-rtl.path-course-view .unread{margin-right:30px}.path-course-view .block.drag .header{cursor:move}.path-course-view .completionprogress{text-align:right}.dir-rtl.path-course-view .completionprogress{text-align:left}.path-course-view .single-section .completionprogress{margin-right:5px}.path-course-view .section .summary{line-height:normal}.path-site li.activity>div,.path-course-view li.activity>div{position:relative;padding:0 16px 0 0}.dir-rtl.path-site li.activity>div,.dir-rtl.path-course-view li.activity>div{position:relative;padding:0 0 0 16px}.path-course-view li.activity span.autocompletion img{vertical-align:text-bottom}.path-course-view li.activity form.togglecompletion img{max-width:none}.path-course-view li.activity form.togglecompletion .ajaxworking{width:16px;height:16px;position:absolute;right:22px;top:3px;background:url([[pix:i/ajaxloader]]) no-repeat}.dir-rtl.path-course-view .completionprogress{float:none}.dir-rtl.path-course-view li.activity form.togglecompletion .ajaxworking{right:-22px}li.section.hidden span.commands a.editing_hide,li.section.hidden span.commands a.editing_show{cursor:default}ul.weeks h3.sectionname{white-space:nowrap}.editing ul.weeks h3.sectionname{white-space:normal}.single-section h3.sectionname{text-align:center;clear:both}.section img.movetarget{height:16px;width:80px}input.titleeditor{width:330px;vertical-align:text-bottom}span.editinstructions{position:absolute;top:0;margin-top:-22px;margin-left:30px;line-height:16px;font-size:11.9px;padding:.1em .4em;background-color:#d9edf7;color:#3a87ad;text-decoration:none;z-index:9999;-webkit-box-shadow:2px 2px 5px 1px #ccc;-moz-box-shadow:2px 2px 5px 1px #ccc;box-shadow:2px 2px 5px 1px #ccc;border:1px solid #bce8f1}#dndupload-status{position:fixed;left:0;width:40%;margin:0 30%;padding:6px;border:1px solid #bce8f1;text-align:center;background:#d9edf7;color:#3a87ad;z-index:1;-webkit-box-shadow:2px 2px 5px 1px #ccc;-moz-box-shadow:2px 2px 5px 1px #ccc;box-shadow:2px 2px 5px 1px #ccc;-webkit-border-radius:8px;-moz-border-radius:8px;border-radius:8px}.dndupload-preview{color:#909090;border:1px dashed #909090;list-style:none;margin-top:.2em;padding:.3em}.dndupload-preview img.icon{vertical-align:text-bottom;padding:0}.dndupload-progress-outer{overflow:hidden;height:20px;margin-bottom:20px;background-color:#f7f7f7;background-image:-moz-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9));background-image:-webkit-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-o-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:linear-gradient(to bottom, #f5f5f5, #f9f9f9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.dndupload-progress-inner{width:0;height:100%;color:#fff;float:left;font-size:12px;text-align:center;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top, #149bdf, #0480be);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be));background-image:-webkit-linear-gradient(top, #149bdf, #0480be);background-image:-o-linear-gradient(top, #149bdf, #0480be);background-image:linear-gradient(to bottom, #149bdf, #0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width .6s ease;-moz-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.dndupload-hidden{display:none}#page-course-pending .singlebutton,#page-course-index .singlebutton,#page-course-index-category .singlebutton,#page-course-editsection .singlebutton{text-align:center}#page-admin-course-manage #movecourses td img{margin:0 .22em;vertical-align:text-bottom}#page-admin-course-manage #movecourses td img.icon{padding:0}#coursesearch{margin-top:1em;text-align:center}#page-course-pending .pendingcourserequests{margin-bottom:1em}#page-course-pending .pendingcourserequests .singlebutton{display:inline}#page-course-pending .pendingcourserequests .cell{padding:0 5px}#page-course-pending .pendingcourserequests .cell.c6{white-space:nowrap}.coursebox{margin-bottom:15px;border:1px dotted #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;padding:5px}.coursebox>.info>.coursename a{display:block;background-image:url([[pix:moodle|i/course]]);background-repeat:no-repeat;padding-left:21px;background-position:left .2em}.dir-rtl .coursebox>.info>.coursename a{padding-left:0;padding-right:21px;background-position:right .2em}.coursebox>.info>.coursename,.coursebox .content .teachers,.coursebox .content .courseimage,.coursebox .content .coursefile{float:left;clear:left}.coursebox .content .teachers,.coursebox .content .courseimage,.coursebox .content .coursefile{width:40%}.dir-rtl .coursebox>.info>.coursename,.dir-rtl .coursebox .teachers,.dir-rtl .coursebox .content .courseimage,.dir-rtl .coursebox .content .coursefile{float:right;clear:right}.coursebox>.info>h3.coursename{margin:5px;line-height:1}.coursebox>.info>.coursename{margin:5px;padding:0}.coursebox .content .teachers li{list-style-type:none;padding:0;margin:0}.coursebox .enrolmenticons{padding:3px 0;float:right}.coursebox .moreinfo{padding:3px 0;float:right}.coursebox .enrolmenticons img,.coursebox .moreinfo img{margin:0 .2em}.coursebox .content{clear:both}.coursebox .content .summary,.coursebox .content .coursecat{float:right;width:55%}.coursebox .content .coursecat{text-align:right;clear:right}.coursebox.remotecoursebox .remotecourseinfo{float:left;width:40%}.coursebox .content .courseimage img{max-width:100px;max-height:100px}.coursebox .content .coursecat,.coursebox .content .summary,.coursebox .content .courseimage,.coursebox .content .coursefile,.coursebox .content .teachers,.coursebox.remotecoursebox .remotecourseinfo{margin:3px 5px;padding:0}.coursebox.remotehost>.info>.categoryname a{background-image:url([[pix:moodle|i/mnethost]])}.dir-rtl .coursebox>.info>.categoryname a{padding-left:0;padding-right:21px;background-position:center right}.dir-rtl .coursebox>.info>.categoryname,.dir-rtl .coursebox .teachers,.dir-rtl .coursebox .content .courseimage,.dir-rtl .coursebox .content .coursefile{float:right;clear:right}.dir-rtl .coursebox .enrolmenticons,.dir-rtl .coursebox .moreinfo{float:left}.dir-rtl .coursebox .summary,.dir-rtl .coursebox .coursecat{float:left}.dir-rtl .coursebox .coursecat{text-align:left;clear:left}.coursebox.collapsed{margin-bottom:0}.coursebox.collapsed>.content{display:none}.courses .coursebox.collapsed{border:1px solid #ddd;padding:5px}.courses .coursebox.even{background-color:#f9f9f9}.courses .coursebox:hover,.course_category_tree .courses>.paging.paging-morelink:hover{background-color:#f5f5f5}.course_category_tree .category .numberofcourse{font-size:11.9px}.course_category_tree .controls{visibility:hidden}.course_category_tree .controls div{display:inline;cursor:pointer}.jsenabled .course_category_tree .controls{visibility:visible}.course_category_tree .controls{margin-bottom:5px;text-align:right;float:right}.course_category_tree .controls div{padding-right:2em;font-size:75%}.course_category_tree .category>.info>.categoryname{background-image:url([[pix:moodle|t/collapsed_empty]]);background-repeat:no-repeat;padding:2px 18px;margin:3px;background-position:center left}.dir-rtl .course_category_tree .category>.info>.categoryname{background-image:url([[pix:moodle|t/collapsed_empty_rtl]]);background-position:center right}.course_category_tree .category.with_children>.info>.categoryname{background-image:url([[pix:moodle|t/expanded]]);cursor:pointer}.course_category_tree .category.with_children.collapsed>.info>.categoryname{background-image:url([[pix:moodle|t/collapsed]])}.dir-rtl .course_category_tree .category.with_children.collapsed>.info>.categoryname{background-image:url([[pix:moodle|t/collapsed_rtl]])}.course_category_tree .category.collapsed>.content{display:none}.course_category_tree .category>.info{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);border-color:#e3e3e3;min-height:0;padding:0;margin:3px 0;margin-bottom:3px;clear:both}.course_category_tree .category>.info blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.course_category_tree.frontpage-category-names .category>.info{background:none;border:none;margin:0}.course_category_tree .category>.content{padding-left:16px}.dir-rtl .course_category_tree .category>.content{padding-left:0;padding-right:16px}.course_category_tree .subcategories>.paging,.courses>.paging{margin:0;padding:5px;text-align:center}.courses>.paging.paging-morelink,.course_category_tree .subcategories>.paging.paging-morelink{text-align:left}.course_category_tree .paging.paging-morelink a{font-size:11.9px}.dir-rtl .courses>.paging.paging-morelink,.dir-rtl .course_category_tree .paging.paging-morelink{text-align:right}#page-course-index-category .generalbox.info{margin-bottom:15px;border:1px dotted #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;padding:5px}#page-course-index-category .categorypicker{text-align:center;margin:10px 0 20px}.section .summary .iconsmall,.section .activity .iconsmall{width:16px;height:16px}.section .editing_title .iconsmall{width:12px;height:12px;margin:8px 8px 0 0;padding:4px 8px 0 0;vertical-align:text-bottom}.section .moodle-actionmenu .iconsmall{max-width:none !important;width:16px;height:16px;padding:4px;vertical-align:text-bottom}.section .moodle-actionmenu[data-enhanced] .menu img{width:12px;height:12px}.dir-rtl .section .editing_title .iconsmall{margin:8px 0 0 8px;padding:4px 0 0 8px}#course-category-listings{background-color:transparent;margin-bottom:200px}#course-category-listings.columns-2>#course-listing>div{position:relative;left:-1px}#course-category-listings.columns-3>#course-listing>div{height:100%}#course-category-listings>div>div{min-height:300px}#course-category-listings>div>div>ul.ml>li:first-child>div{border-top:0}#course-category-listings h3{margin:0;padding:.4rem .6rem .3rem}#course-category-listings h4{margin:1rem 0 0;padding:.6rem 1rem .5rem}#course-category-listings .moodle-actionmenu{white-space:nowrap}#course-category-listings .moodle-actionmenu[data-enhance] .toggle-display img{width:auto}#course-category-listings .moodle-actionmenu[data-enhance] .toggle-display.textmenu{padding-right:4px}#course-category-listings .moodle-actionmenu[data-enhance] .toggle-display.textmenu .caret{margin-top:12px}#course-category-listings .listing-actions{text-align:center;padding:.4rem .3rem .3rem;line-height:2.2em}#course-category-listings .listing-actions>a,#course-category-listings .listing-actions>.moodle-actionmenu{display:inline-block}#course-category-listings .listing-actions>.moodle-actionmenu .menu a{padding-left:1rem}#course-category-listings .listing-actions .moodle-actionmenu:not([data-enhanced]) li{line-height:normal}#course-category-listings .listing-actions .moodle-actionmenu:not([data-enhanced])>.menubar a{color:inherit;display:inline-block}#course-category-listings .listing-actions .moodle-actionmenu:not([data-enhanced])>.menubar a>img{display:none}#course-category-listings .listing-actions .moodle-actionmenu:not([data-enhanced])>.menubar a .caret{display:none}#course-category-listings .listing-actions .moodle-actionmenu:not([data-enhanced])>.menu .menu-action-text{display:inline-block}#course-category-listings ul.ml{list-style:none;margin:1rem 0}#course-category-listings ul.ml ul.ml{margin:0}#course-category-listings li{line-height:2.2em}#course-category-listings li>div:hover{background-color:#f5f5f5}#course-category-listings li .tree-icon{margin:2px 6px 0 0;width:12px;vertical-align:inherit}#course-category-listings li[data-selected='1']>div{background-color:#f9f9f9}#course-category-listings li[data-selected='1']>div:hover{background-color:#f5f5f5}#course-category-listings li .tree-icon{margin-left:0}#course-category-listings li li .tree-icon{margin-left:1em}#course-category-listings li li li .tree-icon{margin-left:2em}#course-category-listings li li li li .tree-icon{margin-left:3em}#course-category-listings li li li li li .tree-icon{margin-left:4em}#course-category-listings li li li li li li .tree-icon{margin-left:4.5em}#course-category-listings li li li li li li li .tree-icon{margin-left:5em}#course-category-listings li li li li li li li li .tree-icon{margin-left:5.5em}#course-category-listings .item-actions{margin-right:1em;display:inline-block;display:initial}#course-category-listings .item-actions>a img,#course-category-listings .item-actions .menubar img{margin:0 4px;height:12px;padding:0;vertical-align:inherit}#course-category-listings .item-actions.show .menu li{line-height:20px}#course-category-listings .item-actions.show .menu img{width:12px;max-width:none}#course-category-listings .item-actions .menu-action-text{vertical-align:inherit}#course-category-listings .listitem>div>.float-left{float:left}#course-category-listings .listitem>div>.float-right{float:right;text-align:right}#course-category-listings .listitem>div .item-actions .action-show{display:none}#course-category-listings .listitem>div .item-actions .action-hide{display:inline}#course-category-listings .listitem>div .without-actions{color:#333}#course-category-listings .listitem>div .idnumber{color:#a1a1a8;margin-right:2em}#course-category-listings .listitem[data-visible="0"]{color:#999}#course-category-listings .listitem[data-visible="0"]>div>a{color:#999}#course-category-listings .listitem[data-visible="0"]>div .item-actions .action-show{display:inline}#course-category-listings .listitem[data-visible="0"]>div .item-actions .action-hide{display:none}#course-category-listings .listitem.highlight{background-color:transparent}#course-category-listings .listitem.highlight>div,#course-category-listings .listitem.highlight>div:hover,#course-category-listings .listitem.highlight[data-selected='1']>div{background-color:#f5f5f5}#course-category-listings #course-listing .listitem .categoryname{display:inline-block;margin-left:1em;color:#a1a1a8}#course-category-listings #course-listing .listitem .coursename{display:inline-block}#course-category-listings #course-listing .listitem>div{padding-left:1rem}#course-category-listings #course-listing>.firstpage .listitem:first-child>div .item-actions .action-moveup,#course-category-listings #course-listing>.lastpage .listitem:last-child>div .item-actions .action-movedown{display:none}#course-category-listings #course-listing .bulk-action-checkbox{margin:-2px 6px 0 0}#course-category-listings #category-listing .listitem.collapsed>ul.ml{display:none}#course-category-listings #category-listing .listitem>div>.ba-checkbox{width:2.2em;text-align:center;margin:-1px .5em 0 0;padding-top:2px}#course-category-listings #category-listing .listitem.highlight>div>.ba-checkbox{background-color:#f5f5f5}#course-category-listings #category-listing .listitem[data-selected='1']>div>.ba-checkbox{margin:0 .5em 0 0;padding:0;background-color:inherit}#course-category-listings #category-listing .listitem:first-child>div .item-actions .action-moveup,#course-category-listings #category-listing .listitem:last-child>div .item-actions .action-movedown{display:none}#course-category-listings #category-listing .course-count{color:#a1a1a8;margin-right:2rem;min-width:3.5em;display:inline-block}#course-category-listings #category-listing .course-count .smallicon{width:12px;margin-left:4px;vertical-align:inherit}#course-category-listings #category-listing .bulk-action-checkbox{margin-right:-3px}#course-category-listings #category-listing .category-listing>ul>.listitem:first-child{position:relative}#course-category-listings #category-listing .category-bulk-actions{margin:0 .5em .5em;position:relative}#course-category-listings .detail-pair{border-bottom:1px solid #ddd;margin:0 1rem}#course-category-listings .detail-pair>*{display:inline-block;line-height:2.2rem}#course-category-listings .detail-pair .pair-key{font-weight:bold;vertical-align:top}#course-category-listings .detail-pair .pair-key span{margin-right:1rem;display:block}#course-category-listings .detail-pair .pair-value select{max-width:100%}#course-category-listings .bulk-actions .detail-pair>*{display:block;width:100%}#course-category-listings .listing-pagination{text-align:center}#course-category-listings .listing-pagination .yui3-button{background-color:#fff;border:0;margin:.4rem .2rem .45rem;font-size:10.4px}#course-category-listings .listing-pagination .yui3-button.active-page{background-color:#e6e6e6}#course-category-listings .listing-pagination-totals{text-align:center}#course-category-listings .listing-pagination-totals.dimmed{color:#999;margin:.4rem 1rem .45rem}#course-category-listings .select-a-category .notifymessage,#course-category-listings .select-a-category .alert{margin:1em}#course-category-listings #course-listing .listitem .drag-handle{display:none}.jsenabled #course-category-listings #course-listing .listitem .drag-handle{display:inline-block;margin:0 6px 0 0;cursor:pointer}.dir-rtl #course-category-listings #category-listing,.dir-rtl #course-category-listings #course-listing{float:right;margin-left:0}.dir-rtl #course-category-listings .listitem>div>.float-left{float:right}.dir-rtl #course-category-listings .listitem>div>.float-right{float:left;text-align:left}.dir-rtl #course-category-listings li .tree-icon{margin:2px 0 0 6px}.dir-rtl #course-category-listings li .tree-icon{margin-right:0}.dir-rtl #course-category-listings li li .tree-icon{margin-right:1em}.dir-rtl #course-category-listings li li li .tree-icon{margin-right:2em}.dir-rtl #course-category-listings li li li li .tree-icon{margin-right:3em}.dir-rtl #course-category-listings li li li li li .tree-icon{margin-right:4em}.dir-rtl #course-category-listings li li li li li li .tree-icon{margin-right:4.5em}.dir-rtl #course-category-listings li li li li li li li .tree-icon{margin-right:5em}.dir-rtl #course-category-listings li li li li li li li li .tree-icon{margin-right:5.5em}.dir-rtl #course-category-listings #category-listing .listitem>div{margin-right:.5em;margin-left:0}.dir-rtl #course-category-listings #category-listing .listitem>div>.ba-checkbox{margin:-1px 0 0 .5em}.dir-rtl #course-category-listings #category-listing .listitem[data-selected='1']>div>.ba-checkbox{margin:0 0 0 .5em}.dir-rtl #course-category-listings #category-listing .course-count{margin-left:2rem}.dir-rtl #course-category-listings #category-listing .course-count .smallicon{margin-left:0;margin-right:4px}.dir-rtl #course-category-listings #category-listing .bulk-action-checkbox{margin-left:-3px;margin-right:0}.dir-rtl #course-category-listings #course-listing{padding-right:24px}.dir-rtl #course-category-listings #course-listing .listitem .idnumber{color:#a1a1a8;padding-right:2em}.dir-rtl #course-category-listings #course-listing .listitem .categoryname{display:inline-block;margin-right:1em;margin-left:0}.dir-rtl #course-category-listings #course-listing .listitem .drag-handle{margin:0 6px 0 6px}.dir-rtl #course-category-listings #course-listing .listitem>div{padding-left:1rem}.dir-rtl #course-category-listings #course-listing .bulk-action-checkbox{vertical-align:middle;margin:-2px 0 0 6px}.dir-rtl #course-category-listings .detail-pair>*{float:right;margin-right:0}.dir-rtl #course-category-listings .detail-pair .pair-key span{margin-right:0;margin-left:0}.dir-rtl #course-category-listings .detail-pair .pair-value{margin-right:.5em}.coursecat-management-header{vertical-align:middle}.coursecat-management-header h2{display:inline-block;text-align:left}.coursecat-management-header>div{display:inline-block;float:right;line-height:40px}.coursecat-management-header>div>div{margin-left:1em;margin:10px 0;display:inline-block}.coursecat-management-header select{max-width:300px;cursor:pointer;padding:.4em .5em .45em 1em;vertical-align:baseline;white-space:nowrap}.coursecat-management-header .view-mode-selector .moodle-actionmenu{white-space:nowrap;display:inline-block}.coursecat-management-header .view-mode-selector .moodle-actionmenu[data-enhanced].show .menu a{padding-left:1em}.dir-rtl .coursecat-management-header h2{text-align:right}.dir-rtl .coursecat-management-header>div{float:left;margin-right:1em;margin-left:0}.course-being-dragged-proxy{border:0;color:#0070a8;vertical-align:middle;padding:0 0 0 4em}.course-being-dragged{opacity:.5;filter:alpha(opacity=50)}@media (min-width:1200px) and (max-width:1600px){#course-category-listings.columns-3{background-color:transparent;border:0}#course-category-listings.columns-3 #category-listing,#course-category-listings.columns-3 #course-listing{width:50%}#course-category-listings.columns-3 #category-listing>div,#course-category-listings.columns-3 #course-listing>div,#course-category-listings.columns-3 #course-detail>div{background-color:transparent}#course-category-listings.columns-3 #course-detail{width:100%;margin-top:1em}}@media (max-width:1199px){#course-category-listings.columns-2,#course-category-listings.columns-3{background-color:transparent;border:0}#course-category-listings.columns-2 #category-listing,#course-category-listings.columns-3 #category-listing,#course-category-listings.columns-2 #course-listing,#course-category-listings.columns-3 #course-listing,#course-category-listings.columns-2 #course-detail,#course-category-listings.columns-3 #course-detail{width:100%;margin:0 0 1em}#course-category-listings.columns-2 #category-listing>div,#course-category-listings.columns-3 #category-listing>div,#course-category-listings.columns-2 #course-listing>div,#course-category-listings.columns-3 #course-listing>div,#course-category-listings.columns-2 #course-detail>div,#course-category-listings.columns-3 #course-detail>div{background-color:transparent}}.filemanager,.filepicker,.file-picker{font-size:11px}.filemanager a,.file-picker a,.filemanager a:hover,.file-picker a:hover{color:#555555;text-decoration:none}.filemanager input[type="text"],.file-picker input[type="text"]{width:265px}.filemanager .fp-license td,.file-picker .fp-setlicense td{max-width:265px}.filemanager .fp-license select,.file-picker .fp-setlicense select{max-width:100%}.fp-content-center{height:100%;width:100%;display:table-cell;vertical-align:middle}.fp-content-hidden{visibility:hidden}.yui3-panel-focused{outline:none}#filesskin .yui3-panel-content{padding-bottom:20px;background:#F2F2F2;-webkit-border-radius:8px;-moz-border-radius:8px;border-radius:8px;border:1px solid #fff;display:inline-block;*display:inline;*zoom:1;-webkit-box-shadow:5px 5px 20px 0 #666;-moz-box-shadow:5px 5px 20px 0 #666;box-shadow:5px 5px 20px 0 #666}#filesskin .yui3-widget-hd{-webkit-border-radius:10px 10px 0 0;-moz-border-radius:10px 10px 0 0;border-radius:10px 10px 0 0;border-bottom:1px solid #BBBBBB;padding:5px;text-align:center;font-size:12px;color:#333;letter-spacing:1px;text-shadow:1px 1px 1px #fff;filter:dropshadow(color=#FFFFFF, offx=1, offy=1);background-color:#ebebeb;background-image:-moz-linear-gradient(top, #fff, #ccc);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#ccc));background-image:-webkit-linear-gradient(top, #fff, #ccc);background-image:-o-linear-gradient(top, #fff, #ccc);background-image:linear-gradient(to bottom, #fff, #ccc);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffcccccc', GradientType=0)}.fp-panel-button{background:#fff;padding:3px 20px 2px 20px;text-align:center;margin:10px;-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;display:inline-block;*display:inline;*zoom:1;-webkit-box-shadow:2px 2px 3px .1px #999;-moz-box-shadow:2px 2px 3px .1px #999;box-shadow:2px 2px 3px .1px #999}.moodle-dialogue h3{font-size:14px;margin:0;line-height:20px}.moodle-dialogue-base .filepicker .moodle-dialogue-wrap .moodle-dialogue-bd{padding:0}#filesskin .file-picker.fp-generallayout{width:859px;background:#FFFFFF;-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;border:1px solid #CCCCCC;position:relative}.file-picker .fp-repo-area{width:180px;overflow:auto;display:inline-block;*display:inline;*zoom:1;float:left;height:525px;border-right:1px solid #BBBBBB}.dir-rtl .file-picker .fp-repo-area{border-left:1px solid #BBBBBB;border-right:none;float:right}.file-picker .fp-repo-items{float:none;width:auto;margin-left:181px}.moodle-dialogue-fullscreen .file-picker .fp-repo-items{margin-left:0;margin-right:0;float:left}.dir-rtl .file-picker .fp-repo-items{margin-left:0;margin-right:181px}.dir-rtl .moodle-dialogue-fullscreen .file-picker .fp-repo-items{margin-left:0;margin-right:0;float:right}.file-picker .fp-navbar{background:#F2F2F2;border-bottom:1px solid #BBBBBB;min-height:40px;overflow:hidden}.file-picker .fp-navbar .fp-viewbar{margin:4px}.file-picker .fp-content{background:#FFFFFF;clear:none;overflow:auto;height:452px}.filepicker.moodle-dialogue-fullscreen .file-picker .fp-content{width:100%}.file-picker .fp-content-loading{height:100%;width:100%;display:table;text-align:center}.file-picker .fp-content .fp-object-container{width:98%;height:98%}.dir-rtl .file-picker .fp-list{text-align:right}.dir-rtl .file-picker .fp-toolbar{padding:4px}.dir-rtl .file-picker .fp-list{text-align:right}.dir-rtl .file-picker .fp-repo-name{display:inline}.dir-rtl .file-picker .fp-pathbar{text-align:right;display:block;border-top:none}.dir-rtl .file-picker div.bd{text-align:right}.dir-rtl #filemenu .yuimenuitemlabel{text-align:right}.dir-rtl .filepicker .yui-layout-unit-left{left:500px}.dir-rtl .filepicker .yui-layout-unit-center{left:0}.dir-rtl .filemanager-toolbar a{padding:0}.file-picker .fp-list{list-style-type:none;padding:0;float:left;width:100%;margin:0}.dir-rtl .file-picker .fp-list{text-align:right;float:left}.file-picker .fp-list .fp-repo a{display:block;padding:.5em .7em}.file-picker .fp-list .fp-repo.active{background:#F2F2F2}.file-picker .fp-list .fp-repo-icon{padding:0 7px 0 5px;width:16px;height:16px}.fp-toolbar{float:left}.dir-rtl .fp-toolbar{float:right}.fp-toolbar.empty{display:none}.dir-rtl .fp-toolbar div.disabled,.fp-toolbar .disabled{display:none}.fp-toolbar div{display:block;float:left;margin-right:4px}.dir-rtl .fp-toolbar div{display:block;float:right;margin-left:4px;margin-right:0}.fp-toolbar img{vertical-align:-15%;margin-right:5px}.fp-toolbar .fp-tb-search{width:235px;height:27px}.fp-toolbar .fp-tb-search input{background:#FFFFFF url('[[pix:a/search]]') no-repeat 7px 7px;padding:2px 6px 1px 27px;width:200px;height:27px;border:1px solid #BBBBBB}.fp-viewbar{float:right;height:30px;border:1px solid #CCC;border-bottom:1px solid #B3B3B3;border-radius:4px;background:white}.fp-repo-items fp-viewbar{margin:4px}.dir-rtl .fp-toolbar img{vertical-align:-35%}.dir-rtl .fp-viewbar{float:left}.fp-viewbar a{width:30px;height:30px;border-right:1px solid #CCC;display:block;float:left}.fp-viewbar a.checked:hover,.fp-viewbar a:hover{background-image:radial-gradient(ellipse at center, #ffffff 60%, #dfdfdf 100%);background-color:#ebebeb}.fp-viewbar a.checked,.fp-viewbar a:active{background-image:radial-gradient(ellipse at center, #ffffff 40%, #dfdfdf 100%);background-color:#dfdfdf}.fp-viewbar a.fp-vb-icons{border-radius:4px 0 0 4px}.fp-viewbar a.fp-vb-tree{border-right:0;border-radius:0 4px 4px 0}.fp-viewbar a img{margin:7px}.fp-viewbar.disabled a{opacity:.45;background:none;cursor:default}.file-picker .fp-clear-left{clear:left}.dir-rtl .fp-vb-details a:hover{background:none;border:20px solid black}.dir-rtl .fp-vb-details.checked a:hover{background:none;border:40px solid black}.dir-rtl .fp-vb-tree a:hover{background:none;border:30px solid black}.dir-rtl .fp-vb-tree.checked a:hover{background:none;border:50px solid black}.file-picker .fp-pathbar{display:table-row}.fp-pathbar.empty{display:none}.fp-pathbar .fp-path-folder{background:url('[[pix:theme|fp/path_folder]]') no-repeat 0 0;width:27px;height:12px;margin-left:4px}.dir-rtl .fp-pathbar .fp-path-folder{background:url('[[pix:theme|fp/path_folder_rtl]]') no-repeat right top;width:auto;height:12px;margin-left:4px}.dir-rtl .fp-pathbar span{display:inline-block;*display:inline;*zoom:1;float:right;margin-left:32px}.fp-pathbar .fp-path-folder-name{margin-left:32px;line-height:20px}.dir-rtl .fp-pathbar .fp-path-folder-name{margin-right:32px;line-height:20px}.fp-iconview .fp-file{float:left;text-align:center;position:relative;margin:10px 10px 35px}.fp-iconview .fp-thumbnail{min-width:110px;min-height:110px;line-height:110px;text-align:center;border:1px solid #FFFFFF;display:block}.fp-iconview .fp-thumbnail img{border:1px solid #ddd;padding:3px;vertical-align:middle;-webkit-box-shadow:1px 1px 2px 0 #ccc;-moz-box-shadow:1px 1px 2px 0 #ccc;box-shadow:1px 1px 2px 0 #ccc}.fp-iconview .fp-thumbnail:hover{background:#fff;border:1px solid #ddd;-webkit-box-shadow:inset 0 0 10px 0 #ccc;-moz-box-shadow:inset 0 0 10px 0 #ccc;box-shadow:inset 0 0 10px 0 #ccc}.fp-iconview .fp-filename-field{height:33px;word-wrap:break-word;overflow:hidden;position:absolute}.fp-iconview .fp-filename-field:hover{overflow:visible;z-index:1000}.fp-iconview .fp-filename-field .fp-filename{background:#FFFFFF;padding-top:5px;padding-bottom:12px;min-width:112px}.dir-rtl .fp-iconview .fp-file{float:right}.file-picker .yui3-datatable table{border:0 solid #BBBBBB;width:100%}#filesskin .file-picker .yui3-datatable-header{background:#FFFFFF;border-bottom:1px solid #CCCCCC;border-left:0 solid #FFFFFF;color:#555555}#filesskin .file-picker .yui3-datatable-odd .yui3-datatable-cell{background-color:#F6F6F6;border-left:0 solid #F6F6F6}#filesskin .file-picker .yui3-datatable-even .yui3-datatable-cell{background-color:#FFFFFF;border-left:0 solid #FFFFFF}.dir-rtl .file-picker .yui3-datatable-header{text-align:right}.file-picker .ygtvtn,.filemanager .ygtvtn{background:url('[[pix:moodle|y/tn]]') 0 0 no-repeat;width:17px;height:22px}.dir-rtl .filemanager .ygtvtn,.dir-rtl .file-picker .ygtvtn{background:url('[[pix:moodle|y/tn_rtl]]') 0 0 no-repeat;width:17px;height:22px}.file-picker .ygtvtm,.filemanager .ygtvtm{background:url('[[pix:moodle|y/tm]]') 0 10px no-repeat;width:13px;height:12px;cursor:pointer}.file-picker .ygtvtmh,.filemanager .ygtvtmh{background:url('[[pix:moodle|y/tm]]') 0 10px no-repeat;width:13px;height:12px;cursor:pointer}.file-picker .ygtvtp,.filemanager .ygtvtp{background:url('[[pix:moodle|y/tp]]') 0 10px no-repeat;width:13px;height:12px;cursor:pointer}.dir-rtl .file-picker .ygtvtp,.dir-rtl .filemanager .ygtvtp{background:url('[[pix:moodle|y/tp_rtl]]') 0 10px no-repeat}.file-picker .ygtvtph,.filemanager .ygtvtph{background:url('[[pix:moodle|y/tp]]') 0 10px no-repeat;width:13px;height:22px;cursor:pointer}.dir-rtl .file-picker .ygtvtph,.dir-rtl .filemanager .ygtvtph{background:url('[[pix:moodle|y/tp_rtl]]') 0 10px no-repeat}.file-picker .ygtvln,.filemanager .ygtvln{background:url('[[pix:moodle|y/ln]]') 0 0 no-repeat;width:17px;height:22px}.dir-rtl .file-picker .ygtvln,.dir-rtl .filemanager .ygtvln{background:url('[[pix:moodle|y/ln_rtl]]') 0 0 no-repeat}.file-picker .ygtvlm,.filemanager .ygtvlm{background:url('[[pix:moodle|y/lm]]') 0 10px no-repeat;width:13px;height:12px;cursor:pointer}.file-picker .ygtvlmh,.filemanager .ygtvlmh{background:url('[[pix:moodle|y/lm]]') 0 10px no-repeat;width:13px;height:12px;cursor:pointer}.file-picker .ygtvlp,.filemanager .ygtvlp{background:url('[[pix:moodle|y/lp]]') 0 10px no-repeat;width:13px;height:12px;cursor:pointer}.dir-rtl .file-picker .ygtvlp,.dir-rtl .filemanager .ygtvlp{background:url('[[pix:moodle|y/lp_rtl]]') 0 10px no-repeat}.file-picker .ygtvlph,.filemanager .ygtvlph{background:url('[[pix:moodle|y/lp]]') 0 10px no-repeat;width:13px;height:12px;cursor:pointer}.dir-rtl .file-picker .ygtvlph,.dir-rtl .filemanager .ygtvlph{background:url('[[pix:moodle|y/lp_rtl]]') 0 10px no-repeat}.file-picker .ygtvloading,.filemanager .ygtvloading{background:transparent url('[[pix:moodle|y/loading]]') 0 0 no-repeat;width:16px;height:22px}.file-picker .ygtvdepthcell,.filemanager .ygtvdepthcell{background:url('[[pix:moodle|y/vline]]') 0 0 no-repeat;width:17px;height:32px}.file-picker .ygtvblankdepthcell,.filemanager .ygtvblankdepthcell{width:17px;height:22px}a.ygtvspacer:hover{color:transparent;text-decoration:none}.ygtvlabel,.ygtvlabel:link,.ygtvlabel:visited,.ygtvlabel:hover{background-color:transparent;cursor:pointer;margin-left:2px;text-decoration:none}.file-picker .ygtvfocus,.filemanager .ygtvfocus{background-color:#EEEEEE}.fp-filename-icon{margin-top:10px;display:block;position:relative}.fp-icon{float:left;margin-top:-7px;width:24px;height:24px;margin-right:10px;text-align:center;line-height:24px}.dir-rtl .fp-icon{float:right;margin-left:10px;margin-right:0}.fp-icon img{max-height:24px;max-width:24px;vertical-align:middle}.fp-filename{padding-right:10px}.dir-rtl .fp-filename{padding-left:10px;padding-right:0}.file-picker .fp-login-form{height:100%;width:100%;display:table}.file-picker .fp-login-form table{margin:0 auto}.file-picker .fp-login-form p{text-align:center;margin-top:3em}.file-picker .fp-login-form .fp-login-input label{text-align:right;display:block}.file-picker .fp-login-form .fp-login-input .input{text-align:left}.file-picker .fp-login-form input[type="checkbox"]{width:15px;height:15px}.file-picker .fp-upload-form{height:100%;width:100%;display:table}.file-picker .fp-upload-form table{margin:0 auto}.file-picker.fp-dlg{text-align:center}.file-picker.fp-dlg .fp-dlg-text{padding:30px 20px 10px;font-size:12px}.file-picker.fp-dlg .fp-dlg-buttons{margin:0 20px}.file-picker.fp-msg{text-align:center}.file-picker.fp-msg .fp-msg-text{padding:40px 20px 10px 20px;min-width:200px;max-width:500px;max-height:300px;overflow:auto;font-size:12px}.file-picker.fp-msg.fp-msg-error .fp-msg-text{padding:40px 20px 10px 20px;font-size:12px}.file-picker .fp-content-error{height:100%;width:100%;display:table;text-align:center}.file-picker .fp-content-error .fp-error{height:100%;width:100%;display:table-cell;vertical-align:middle;padding:40px 20px 10px 20px;font-size:12px}.file-picker .fp-nextpage{clear:both}.file-picker .fp-nextpage .fp-nextpage-loading{display:none}.file-picker .fp-nextpage.loading .fp-nextpage-link{display:none}.file-picker .fp-nextpage.loading .fp-nextpage-loading{display:block;text-align:center;height:100px;padding-top:50px}.fp-select form{padding:20px 20px 0}.fp-select .fp-select-loading{text-align:center;margin-top:20px}.fp-select .fp-hr{clear:both;height:1px;background-color:#FFFFFF;border-bottom:1px solid #BBBBBB;width:auto;margin:10px 0}.fp-select table{padding:0 0 10px}.fp-select table .mdl-right{min-width:84px}.fp-select .fp-reflist .mdl-right{vertical-align:top}.fp-select .fp-select-buttons{float:right}.fp-select .fp-info{display:block;clear:both;padding:1px 20px 0}.fp-select .fp-thumbnail{float:left;min-width:110px;min-height:110px;line-height:110px;text-align:center;margin:10px 20px 0 0;background:#fff;border:1px solid #ddd;-webkit-box-shadow:inset 0 0 10px 0 #ccc;-moz-box-shadow:inset 0 0 10px 0 #ccc;box-shadow:inset 0 0 10px 0 #ccc}.fp-select .fp-thumbnail img{border:1px solid #DDDDDD;padding:3px;vertical-align:middle;margin:10px}.fp-select .fp-fileinfo{display:inline-block;*display:inline;*zoom:1;margin-top:10px}.file-picker.fp-select .fp-fileinfo{max-width:240px}.fp-select .fp-fileinfo div{padding-bottom:5px}.file-picker.fp-select .uneditable{display:none}.file-picker.fp-select .fp-select-loading{display:none}.file-picker.fp-select.loading .fp-select-loading{display:block}.file-picker.fp-select.loading form{display:none}.fp-select .fp-dimensions.fp-unknown{display:none}.fp-select .fp-size.fp-unknown{display:none}.filemanager-loading{display:none}.jsenabled .filemanager-loading{display:block;margin-top:100px}.filemanager.fm-loading .filemanager-toolbar,.filemanager.fm-loading .fp-pathbar,.filemanager.fm-loading .filemanager-container,.filemanager.fm-loaded .filemanager-loading,.filemanager.fm-maxfiles .fp-btn-add,.filemanager.fm-maxfiles .dndupload-message,.filemanager.fm-noitems .fp-btn-download,.filemanager .fm-empty-container,.filemanager.fm-noitems .filemanager-container .fp-content{display:none}.filemanager .fp-img-downloading{display:none;padding-top:7px}.filemanager .filemanager-updating{display:none;text-align:center}.filemanager.fm-updating .filemanager-updating{display:block;margin-top:37px}.filemanager.fm-updating .fm-content-wrapper,.filemanager.fm-nomkdir .fp-btn-mkdir,.fitem.disabled .filemanager .filemanager-toolbar,.fitem.disabled .filemanager .fp-pathbar,.fitem.disabled .filemanager .fp-restrictions,.fitem.disabled .filemanager .fm-content-wrapper{display:none}.filemanager .fp-restrictions{text-align:right}.filemanager .fp-navbar{background:#F2F2F2;border:1px solid #BBBBBB;border-bottom:none}.filemanager-toolbar{padding:4px;overflow:hidden}.fp-pathbar{border-top:1px solid #BBBBBB;padding:5px 8px 1px;min-height:20px}.file-picker .fp-toolbar{padding:4px}.fp-toolbar .fp-btn-add,.fp-toolbar .fp-btn-download,.fp-toolbar .fp-btn-mkdir,.fp-toolbar .fp-tb-help,.fp-toolbar .fp-tb-manage,.fp-toolbar .fp-tb-logout,.fp-toolbar .fp-tb-refresh{border:1px solid #CCC;border-bottom:1px solid #B3B3B3;border-radius:4px;background:white;width:30px;height:30px}.fp-toolbar a:hover{background-image:radial-gradient(ellipse at center, #ffffff 60%, #dfdfdf 100%);background-color:#ebebeb}.fp-toolbar a:active{background-image:radial-gradient(ellipse at center, #ffffff 40%, #dfdfdf 100%);background-color:#dfdfdf}.fp-btn-add a,.fp-btn-download a,.fp-btn-mkdir a,.fp-tb-help a,.fp-tb-manage a,.fp-tb-logout a,.fp-tb-refresh a{display:block;width:30px;height:30px;border-radius:4px}.fp-btn-add img,.fp-btn-download img,.fp-btn-mkdir img,.fp-tb-help img,.fp-tb-manage img,.fp-tb-logout img,.fp-tb-refresh img{margin:7px}.filemanager .fp-pathbar.empty{display:none}.filepicker-filelist,.filemanager-container{background:#FFFFFF;clear:both;overflow:auto;border:1px solid #BBBBBB;min-height:140px;position:relative}.filemanager .fp-content{overflow:auto;max-height:472px;min-height:157px}.filemanager-container,.filepicker-filelist{overflow:hidden}.fitem.disabled .filepicker-filelist,.fitem.disabled .filemanager-container{background-color:#EBEBE4}.fitem.disabled .fp-btn-choose{color:#999}.fitem.disabled .filepicker-filelist .filepicker-filename{display:none}.fp-iconview .fp-reficons1{position:absolute;height:100%;width:100%;top:0;left:0}.fp-iconview .fp-reficons2{position:absolute;height:100%;width:100%;top:0;left:0}.fp-iconview .fp-file.fp-hasreferences .fp-reficons1{background:url('[[pix:theme|fp/link]]') no-repeat;background-position:bottom right}.fp-iconview .fp-file.fp-isreference .fp-reficons2{background:url('[[pix:theme|fp/alias]]') no-repeat;background-position:bottom left}.filemanager .fp-iconview .fp-file.fp-originalmissing .fp-thumbnail img{display:none}.filemanager .fp-iconview .fp-file.fp-originalmissing .fp-thumbnail{background:url([[pix:s/dead]]) no-repeat;background-position:center center}.filemanager .yui3-datatable table{border:0 solid #BBBBBB;width:100%}.filemanager .yui3-datatable-header{background:#FFFFFF !important;border-bottom:1px solid #CCCCCC !important;border-left:0 solid #FFFFFF !important;color:#555555 !important}.filemanager .yui3-datatable-odd .yui3-datatable-cell{background-color:#F6F6F6 !important;border-left:0 solid #F6F6F6}.filemanager .yui3-datatable-even .yui3-datatable-cell{background-color:#FFFFFF !important;border-left:0 solid #FFFFFF}.filemanager .fp-filename-icon.fp-hasreferences .fp-reficons1{background:url('[[pix:theme|fp/link_sm]]') no-repeat 0 0;height:100%;width:100%;position:absolute;top:8px;left:17px;z-index:1000}.filemanager .fp-filename-icon.fp-isreference .fp-reficons2{background:url('[[pix:theme|fp/alias_sm]]') no-repeat 0 0;height:100%;width:100%;position:absolute;top:9px;left:-6px;z-index:1001}.filemanager .fp-contextmenu{display:none}.filemanager .fp-iconview .fp-folder.fp-hascontextmenu .fp-contextmenu{display:block;position:absolute;right:7px;bottom:5px}.filemanager .fp-treeview .fp-folder.fp-hascontextmenu .fp-contextmenu,.filemanager .fp-tableview .fp-folder.fp-hascontextmenu .fp-contextmenu{display:inline;position:absolute;left:14px;margin-right:-20px;top:6px}.dir-rtl .filemanager .fp-iconview .fp-folder.fp-hascontextmenu .fp-contextmenu{left:7px;right:inherit}.dir-rtl .filemanager .fp-treeview .fp-folder.fp-hascontextmenu .fp-contextmenu,.dir-rtl .filemanager .fp-tableview .fp-folder.fp-hascontextmenu .fp-contextmenu{left:inherit;right:16px;margin-right:0}.filepicker-filelist .filepicker-container,.filemanager.fm-noitems .fm-empty-container{display:block;position:absolute;top:10px;bottom:10px;left:10px;right:10px;border:2px dashed #BBBBBB;padding-top:85px;text-align:center}.filepicker-filelist .dndupload-target,.filemanager-container .dndupload-target{background:#FFFFFF;position:absolute;top:10px;bottom:10px;left:10px;right:10px;border:2px dashed #fb7979;padding-top:85px;text-align:center;-webkit-box-shadow:0 0 0 10px #fff;-moz-box-shadow:0 0 0 10px #fff;box-shadow:0 0 0 10px #fff}.filepicker-filelist.dndupload-over .dndupload-target,.filemanager-container.dndupload-over .dndupload-target{background:#FFFFFF;position:absolute;top:10px;bottom:10px;left:10px;right:10px;border:2px dashed #6c8cd3;padding-top:85px;text-align:center}.dndupload-message{display:none}.dndsupported .dndupload-message{display:inline}.dnduploadnotsupported-message{display:none}.dndnotsupported .dnduploadnotsupported-message{display:inline}.dndupload-target{display:none}.dndsupported .dndupload-ready .dndupload-target{display:block}.dndupload-uploadinprogress{display:none;text-align:center}.dndupload-uploading .dndupload-uploadinprogress{display:block}.dndupload-arrow{background:url([[pix:theme|fp/dnd_arrow]]) center no-repeat;width:100%;height:80px;position:absolute;top:5px}.fitem.disabled .filepicker-container,.fitem.disabled .fm-empty-container{display:none}.dndupload-progressbars{padding:10px;display:none}.dndupload-inprogress .dndupload-progressbars{display:block}.dndupload-inprogress .fp-content{display:none}.filemanager.fm-noitems .dndupload-inprogress .fm-empty-container{display:none}.filepicker-filelist.dndupload-inprogress .filepicker-container{display:none}.filepicker-filelist.dndupload-inprogress a{display:none}.filemanager.fp-select .fp-select-loading{display:none}.filemanager.fp-select.loading .fp-select-loading{display:block}.filemanager.fp-select.loading form{display:none}.filemanager.fp-select.fp-folder .fp-license,.filemanager.fp-select.fp-folder .fp-author,.filemanager.fp-select.fp-file .fp-file-unzip,.filemanager.fp-select.fp-folder .fp-file-unzip,.filemanager.fp-select.fp-file .fp-file-zip,.filemanager.fp-select.fp-zip .fp-file-zip{display:none}.filemanager.fp-select .fp-file-setmain,.filemanager.fp-select .fp-file-setmain-help{display:none}.filemanager.fp-select.fp-cansetmain .fp-file-setmain,.filemanager.fp-select.fp-cansetmain .fp-file-setmain-help{display:inline-block;*display:inline;*zoom:1}.filemanager .fp-mainfile .fp-filename{font-weight:bold}.filemanager.fp-select.fp-folder .fp-file-download{display:none}.fm-operation{font-weight:bold}.filemanager.fp-select .fp-original.fp-unknown,.filemanager.fp-select .fp-original .fp-originloading{display:none}.filemanager.fp-select .fp-original.fp-loading .fp-originloading{display:inline}.filemanager.fp-select .fp-reflist.fp-unknown,.filemanager.fp-select .fp-reflist .fp-reflistloading{display:none}.filemanager.fp-select .fp-refcount{max-width:265px}.filemanager.fp-select .fp-reflist.fp-loading .fp-reflistloading{display:inline}.filemanager.fp-select .fp-reflist .fp-value{background:#F9F9F9;border:1px solid #BBBBBB;padding:8px 7px;margin:0;max-width:265px;max-height:75px;overflow:auto}.filemanager.fp-select .fp-reflist .fp-value li{padding-bottom:7px}.filemanager.fp-mkdir-dlg{text-align:center}.filemanager.fp-mkdir-dlg .fp-mkdir-dlg-text{text-align:left;margin:20px}.dir-rtl .filemanager .fp-mkdir-dlg p{text-align:right}.filemanager.fp-dlg{text-align:center}.filemanager.fp-dlg .fp-dlg-text{padding:0 10px;min-width:200px;max-width:340px;max-height:300px;overflow:auto;line-height:22px;margin:40px 20px 20px;font-size:12px}.file-picker div.bd{text-align:left}.dir-rtl .filemanager .fp-restrictions{text-align:left}.dir-rtl .file-picker div.bd,.dir-rtl .file-picker .fp-pathbar,.dir-rtl .file-picker .fp-list,.dir-rtl #filemenu .yuimenuitemlabel,.dir-rtl .filemanager-container .yui3-skin-sam .yui3-datatable-header{text-align:right}.dir-rtl .filepicker .yui-layout-unit-left{left:500px}.dir-rtl .filepicker .yui-layout-unit-center{left:0}.dir-rtl .file-picker .fp-toolbar .fp-tb-search input{background-position:208px 7px;padding:2px 30px 1px 3px}.dir-rtl .file-picker .fp-toolbar div{float:right;margin-left:4px}.fp-formset{max-width:500px;padding:10px}.fp-formset input[type="file"]{line-height:inherit}.fp-forminset{max-width:400px;padding:0 10px}.fp-forminset .control-group.control-radio{margin-bottom:0}.fp-forminset .control-group label.control-label{width:105px}.fp-forminset .control-group label.control-radio{float:right;text-align:left;width:215px}.fp-forminset .control-group .controls{margin-left:125px}.fp-forminset .control-group .controls select{width:100%}.fp-forminset .control-group .controls.control-radio input{margin-top:3px}.fp-forminset .fp-select-buttons{float:none}.fp-forminset input[type="text"]{width:228px}.fp-fileinfo .fp-value{display:inline-block;padding-left:5px}.dir-rtl .fp-forminset{max-width:400px}.dir-rtl .fp-forminset .control-group label.control-label{float:right;text-align:left}.dir-rtl .fp-forminset .control-group label.control-radio{float:left;text-align:right;width:215px}.dir-rtl .fp-forminset .control-group .controls{margin-left:0;margin-right:125px}.dir-rtl .fp-forminset .fp-select-buttons{float:left}.dir-rtl .fp-forminset input[type="text"]{width:228px}.dir-rtl .fp-fileinfo .fp-value{display:inline-block;padding-right:5px}.dir-rtl .fp-select .fp-thumbnail{margin:10px 0 0 0}.dir-rtl .filepicker .fp-formset label{float:right;text-align:left}.dir-rtl .filepicker .fp-formset .controls{margin-left:0;text-align:right}.message-discussion-noframes h1{font-size:1em}.message-discussion-noframes #userinfo .commands,.message .noframesjslink,.message .link{font-size:11.9px}.message .heading{font-size:1em;font-weight:bold;clear:both;word-wrap:break-word}.message .author{font-weight:bold}.message .time{font-style:italic}#page-message-user .commands span{font-size:.7em}#page-message-user .name{font-weight:bold;font-size:1.1em}.message .time{color:#999}#page-message-messages{padding:10px}#page-message-send .notifysuccess{padding:1px}#page-message-send td.fixeditor{text-align:center}.message{overflow:hidden}.message .note{padding:10px}table.message .searchresults td{padding:5px}.message .contactselector{max-width:380px;margin:0 auto;width:auto}@media screen and (min-width:1000px){.message .contactselector{float:left;padding:0 8px 0 0}}.message .contactselector .singleselect{width:240px}.message .contactselector .paging{z-index:1;position:relative}.message .contactselector #message_participants{max-width:240px}.message .contactselector .message-contacts{list-style-type:none;margin:0}.message .contactselector .message-contacts li{clear:both;position:relative;min-height:23px;padding:2px}.message .contactselector .message-contacts li:nth-child(odd){background:rgba(120,120,255,0.1)}.message .contactselector .message-contacts li>div{display:block;height:100%}.message .contactselector .message-contacts li>div>*{vertical-align:middle;display:inline-block}.message .contactselector .message-contacts li>div.pix{position:relative;float:left}.message .contactselector .message-contacts li>div.link{white-space:nowrap;width:60px;position:relative;float:right;text-align:right}.message .contactselector .message-contacts li>div.contact{position:relative;word-break:break-all}.message .contactselector .message-contacts li>div.contact a{line-height:22px}.dir-ltr .message .message-contacts li{text-align:left}.dir-ltr .message .message-contacts li>div.pix{float:left}.dir-ltr .message .message-contacts li>div.link{float:right;text-align:right}.dir-ltr .message .message-contacts li>div.contact{margin:0 60px 0 24px}.dir-ltr .message .message-contacts li>div.contact.nolinks{margin:0 0 0 24px}.dir-rtl .message .message-contacts li{text-align:right}.dir-rtl .message .message-contacts li>div.pix{float:right}.dir-rtl .message .message-contacts li>div.link{float:left;text-align:left}.dir-rtl .message .message-contacts li>div.contact{margin:0 24px 0 60px}.dir-rtl .message .message-contacts li>div.contact.nolinks{margin:0 24px 0 0}@media screen and (min-width:1000px){.dir-rtl .message .contactselector{float:right}}.message .messagearea{float:none;overflow:hidden;min-height:200px;min-width:300px}@media screen and (min-width:1000px){.message .messagearea{border-left:1px solid #ddd;padding:0 8px}}@media screen and (max-width:1000px){.message .messagearea{width:100%}}#message_user_pictures{text-align:center}.dir-rtl #message_user_pictures{direction:rtl}.message .messagearea .messagehistorytype{clear:both;padding-bottom:20px}.message .messagearea .messagehistory .user{vertical-align:top;width:45%;min-width:100px;display:inline-block}.message .messagearea .messagehistory .user>div{text-align:center}.message .messagearea .messagehistory .between{display:inline-block;width:16px;margin:0 1%;padding-top:40px}@media screen and (max-width:320px){.message .messagearea{min-width:0}.message .messagearea .messagehistory .user{max-width:70px;min-width:0}.message .messagearea .messagehistory .user .userpicture{width:50px;height:auto}}@media screen and (min-width:800px){.message .messagearea .messagehistory .between{margin:0 3%}.message .messagearea .messagehistory .user{width:32%}}@media screen and (min-width:1200px){.message .messagearea .messagehistory .user{width:25%}}.message .messagearea .messagehistory .heading{width:100%;clear:both}.message .messagearea .messagehistory .left{margin-bottom:10px;width:50%;float:left;position:relative;clear:both}.dir-rtl .message .messagearea .messagehistory .left{float:right}.message .messagearea .messagehistory .right{margin-bottom:10px;width:50%;float:right;position:relative;clear:both}.dir-rtl .message .messagearea .messagehistory .right{float:left}.message .messagearea .messagehistory .notification{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);border-color:#e3e3e3;padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;margin-bottom:0;margin-top:5px}.message .messagearea .messagehistory .notification blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.dir-ltr .message .messagearea .messagehistory .message{margin-right:20px}.dir-ltr .message .messagearea .messagehistory .right .message{margin-left:20px}.dir-rtl .message .messagearea .messagehistory .message{margin-left:20px}.dir-rtl .message .messagearea .messagehistory .right .message{margin-right:20px}.message .messagearea .messagehistory .messageactive{background-color:#f5f5f5}.message .messagearea .messagehistory .messagecontent .deleteicon{width:20px;position:absolute;top:-2px}.dir-ltr .message .messagearea .messagehistory .messagecontent .deleteicon{right:0}.dir-rtl .message .messagearea .messagehistory .messagecontent .deleteicon{left:0}.message .messagearea .messagesend{padding-top:20px;clear:both}.message .messagearea .messagesend .messagesendbox{width:100%;box-sizing:border-box}.message .messagearea .messagesend fieldset{padding:0;margin:0}.message .messagearea .messagerecent{text-align:left;width:100%}.message .messagearea .messagerecent .singlemessage{border-bottom:1px solid #ddd;padding:10px}.message .messagearea .messagerecent .singlemessage .otheruser span{padding:5px}.message .messagearea .messagerecent .singlemessage .messagedate{float:right}.message .hiddenelement{display:none}.message .visible{display:inline}.message #usergroupselector.fieldset,.message #viewing{width:100%}.messagesearchresults{margin-bottom:40px}.messagesearchresults td{padding:0 10px 0 20px}.messagesearchresults td span{white-space:nowrap}.messagesearchresults td img.userpicture{padding-right:.45em;vertical-align:text-bottom}.dir-rtl .messagesearchresults td img.userpicture{padding-left:.45em;padding-right:0}.messagesearchresults td span img{padding:0 0 0 .45em;vertical-align:text-bottom}.dir-rtl .messagesearchresults td span img{padding:0 .45em 0 0}#newmessageoverlay{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);border-color:#e3e3e3;margin:0 1em;position:fixed;bottom:0;right:0}#newmessageoverlay blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}#newmessageoverlay #usermessage{padding:10px}#page-user-action_redir #edit-messagebody{width:auto}.core_message-messenger-sendmessage-hidden{display:none}.core_message-messenger-sendmessage .message-actions{position:relative}.core_message-messenger-sendmessage .message-area{height:240px;max-height:100%;position:relative;margin-bottom:10px}.core_message-messenger-sendmessage .message-input{width:100%;height:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.core_message-messenger-sendmessage .message-send{margin:0;float:right}.core_message-messenger-sendmessage .message-notice-area{display:table;position:absolute;top:0;bottom:0;left:0;right:0;width:100%;height:100%}.core_message-messenger-sendmessage .message-notice{display:table-cell;vertical-align:middle;text-align:center}.core_message-messenger-sendmessage .message-notice>div{background:#eee;padding:5px;font-size:12px}.core_message-messenger-sendmessage .message-footer{margin-top:3px;line-height:20px}.core_message-messenger-sendmessage .message-history{position:absolute;bottom:0}.dir-rtl .core_message-messenger-sendmessage .message-send{float:left}.questionbank h2{margin-top:0}.questioncategories h3{margin-top:0}#chooseqtypebox{margin-top:1em}#chooseqtype h3{margin:0 0 .3em}#chooseqtype .instruction{display:none}#chooseqtype .fakeqtypes{border-top:1px solid silver}#chooseqtype .qtypeoption{margin-bottom:.5em}#chooseqtype label{display:block}#chooseqtype .qtypename img{padding:0 .3em}#chooseqtype .qtypename{display:inline-table;width:16em}#chooseqtype .qtypesummary{display:block;margin:0 2em}#chooseqtype .submitbuttons{margin:.7em 0;text-align:center}#qtypechoicecontainer{display:none}#qtypechoicecontainer_c.yui-panel-container.shadow .underlay{background:none}#qtypechoicecontainer.yui-panel .hd{color:#333;letter-spacing:1px;text-shadow:1px 1px 1px #fff;-webkit-border-top-right-radius:10px;-moz-border-radius-topright:10px;border-top-right-radius:10px;-webkit-border-top-left-radius:10px;-moz-border-radius-topleft:10px;border-top-left-radius:10px;border:1px solid #ccc;border-bottom:1px solid #bbb;background-color:#ebebeb;background-image:-moz-linear-gradient(top, #fff, #ccc);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#ccc));background-image:-webkit-linear-gradient(top, #fff, #ccc);background-image:-o-linear-gradient(top, #fff, #ccc);background-image:linear-gradient(to bottom, #fff, #ccc);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffcccccc', GradientType=0)}#qtypechoicecontainer{font-size:12px;color:#333;background:#F2F2F2;-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;border:1px solid #ccc;border-top:0 none;-webkit-box-shadow:5px 5px 20px 0 #666;-moz-box-shadow:5px 5px 20px 0 #666;box-shadow:5px 5px 20px 0 #666}#qtypechoicecontainer #chooseqtype{width:40em}#chooseqtypehead h3{margin:0;font-weight:normal}#chooseqtype .qtypes{position:relative;border-bottom:1px solid #bbb;padding:.24em 0}#chooseqtype .alloptions{overflow-x:hidden;overflow-y:auto;max-height:400px;max-height:calc(85vh);max-height:60vh;width:60%}#chooseqtype .qtypeoption{margin-bottom:0;padding:.3em .3em .3em 1.6em}#chooseqtype .qtypeoption img{vertical-align:text-bottom;padding-left:1em;padding-right:.5em}#chooseqtype .selected{background-color:#fff;-webkit-box-shadow:0 0 10px 0 #ccc;-moz-box-shadow:0 0 10px 0 #ccc;box-shadow:0 0 10px 0 #ccc}#chooseqtype .instruction,#chooseqtype .qtypesummary{display:none;position:absolute;top:0;right:0;bottom:0;left:60%;margin:0;overflow-x:hidden;padding:1.5em 1.6em;background-color:#fff;overflow-y:auto}#chooseqtype .instruction,#chooseqtype .selected .qtypesummary{display:block}#categoryquestions{margin:0}#categoryquestions td,#categoryquestions th{padding:0 .2em}#categoryquestions th{text-align:left;font-weight:normal}#categoryquestions .checkbox{padding-left:5px}#categoryquestions .checkbox input[type="checkbox"]{margin-left:0;float:none}#categoryquestions img.iconsmall{padding:0}#categoryquestions .iconcol{padding:3px}#categoryquestions label{margin:0}#page-mod-quiz-edit div.questionbankwindow div.header{margin:0}#page-mod-quiz-edit div.questionbankwindow.block{padding:0}.dir-rtl #categoryquestions th{text-align:right}.questionbank .singleselect{margin:0}#combinedfeedbackhdr div.fhtmleditor{padding:0}#combinedfeedbackhdr div.fcheckbox{margin-bottom:1em}#multitriesheader div.fitem_feditor{margin-top:1em}#multitriesheader div.fitem_fgroup{margin-bottom:1em}#multitriesheader div.fitem_fgroup fieldset.felement label{margin-left:.3em;margin-right:.3em}body.path-question-type .fitem_fgroup .accesshide{font:inherit;left:0;position:static;padding-right:.3em}.que{clear:left;text-align:left;margin:0 auto 1.8em auto}.dir-rtl .que{text-align:right}.que .info{float:left;width:7em;padding:.5em;margin-bottom:1.8em;background-color:#eee;border:1px solid #dcdcdc;-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px}.que h3.no{margin:0;font-size:.8em;line-height:1}.que span.qno{font-size:1.5em;font-weight:bold}.que .info>div{font-size:.8em;margin-top:.7em}.que .info .questionflag.editable{cursor:pointer}.que .info .editquestion img,.que .info .questionflag img,.que .info .questionflag input{vertical-align:bottom}.que .content{margin:0 0 0 8.5em}.que .formulation,.que .outcome,.que .comment{padding:8px 35px 8px 14px;margin-bottom:20px;text-shadow:0 1px 0 rgba(255,255,255,0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;color:#8a6d3b}.que .formulation{background-color:#d9edf7;border-color:#bce8f1;color:#3a87ad;color:#333}.formulation input[type="text"],.formulation select{width:auto;vertical-align:baseline}.path-mod-quiz input[size]{width:auto}.que .comment{background-color:#dff0d8;border-color:#d6e9c6;color:#468847}.que .history{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);border-color:#e3e3e3}.que .history blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.que .ablock{margin:.7em 0 .3em 0}.que .im-controls{margin-top:.5em;text-align:left}.dir-rtl .que .im-controls{text-align:right}.que .specificfeedback,.que .generalfeedback,.que .rightanswer,.que .im-feedback,.que .feedback,.que p{margin:0 0 .5em}.que .qtext{margin-bottom:1.5em}.que .correctness{display:inline-block;padding:2px 4px;font-size:11.844px;font-weight:bold;line-height:14px;color:#fff;vertical-align:baseline;white-space:nowrap;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.que .correctness:empty{display:none}.que .correctness-important{background-color:#b94a48}.que .correctness-important[href]{background-color:#953b39}.que .correctness-warning{background-color:#f89406}.que .correctness-warning[href]{background-color:#c67605}.que .correctness-success{background-color:#468847}.que .correctness-success[href]{background-color:#356635}.que .correctness-info{background-color:#3a87ad}.que .correctness-info[href]{background-color:#2d6987}.que .correctness-inverse{background-color:#333}.que .correctness-inverse[href]{background-color:#1a1a1a}.que .correctness.correct{background-color:#468847}.que .correctness.partiallycorrect{background-color:#f89406}.que .correctness.notanswered,.que .correctness.incorrect{background-color:#b94a48}.que .validationerror{color:#b94a48}.formulation .correct{background-color:#dff0d8}.formulation .partiallycorrect{background-color:#fcf8e3}.formulation .incorrect{background-color:#f2dede}.formulation select.correct,.formulation input.correct{color:#468847;background-color:#dff0d8;border-color:#468847;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.formulation select.correct:focus,.formulation input.correct:focus{border-color:#356635;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b}.formulation select.partiallycorrect,.formulation input.partiallycorrect{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.formulation select.partiallycorrect:focus,.formulation input.partiallycorrect:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b}.formulation select.incorrect,.formulation input.incorrect{color:#b94a48;background-color:#f2dede;border-color:#b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.formulation select.incorrect:focus,.formulation input.incorrect:focus{border-color:#953b39;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392}.que .grading,.que .comment,.que .commentlink,.que .history{margin-top:.5em}.que .history h3{margin:0 0 .2em;font-size:1em}.que .history table{width:100%;margin:0}.que .history .current{font-weight:bold}.que .questioncorrectnessicon{vertical-align:text-bottom}.que input.questionflagimage{padding-right:3px}.dir-rtl .que input.questionflagimage{padding-left:3px;padding-right:0}.importerror{margin-top:10px;border-bottom:1px solid #555}.mform .que.comment .fitemtitle{width:20%}#page-question-preview #techinfo{margin:1em 0}.dir-rtl #chooseqtype .instruction,.dir-rtl #chooseqtype .qtypesummary{right:60%;left:0;border-left:0;border-right:1px solid grey}#page-mod-quiz-edit .box.generalbox.questionbank{padding:.5em}#page-mod-quiz-edit .questionbank .categorypagingbarcontainer,#page-mod-quiz-edit .questionbank .categoryquestionscontainer,#page-mod-quiz-edit .questionbank .choosecategory{padding:0}#page-mod-quiz-edit .questionbank .choosecategory select{width:100%}#page-mod-quiz-edit div.questionbank .categoryquestionscontainer{background:transparent}#page-mod-quiz-edit #categoryquestions>thead{background:#FFF}#page-mod-quiz-edit #categoryquestions>tbody>tr:nth-of-type(even){background:#e4e4e4}#page-mod-quiz-edit .questionbankwindow div.header{color:#444;text-shadow:none;padding:3px;-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px;margin:0 -10px 0 -10px;padding:2px 10px 2px 10px;background:transparent}#page-mod-quiz-edit .questionbankwindow div.header a:link,#page-mod-quiz-edit .questionbankwindow div.header a:visited{color:#0070a8}#page-mod-quiz-edit .questionbankwindow div.header a:hover{color:#003d5c}#page-mod-quiz-edit .createnewquestion{padding:.3em 0}#page-mod-quiz-edit .createnewquestion div,#page-mod-quiz-edit .createnewquestion input{margin:0}#page-mod-quiz-edit .questionbankwindow div.header .title{color:#333}#page-mod-quiz-edit div.container div.generalbox{background-color:transparent;padding:1.5em}#page-mod-quiz-edit .categoryinfo{background-color:transparent;border-bottom:none}#page-mod-quiz-edit .createnewquestion .singlebutton input{margin-bottom:0}#page-mod-quiz-edit div.questionbank .categorysortopotionscontainer,#page-mod-quiz-edit div.questionbank .categoryselectallcontainer{padding:0 0 1.5em 0}#page-mod-quiz-edit div.questionbank .categorypagingbarcontainer{background-color:transparent;margin:0;border-top:0;border-bottom:0}#page-mod-quiz-edit div.questionbank .categorypagingbarcontainer .paging{padding:0 .3em}#page-mod-quiz-edit div.question div.content div.questioncontrols{background-color:#fff}#page-mod-quiz-edit div.question div.content div.points{margin-top:-0.5em;padding-bottom:0;border:none;background-color:#fff;position:static;width:12.1em;float:right;margin-right:60px}#page-mod-quiz-edit.dir-rtl div.question div.content div.points{float:left;margin-left:60px;margin-right:0}#page-mod-quiz-edit div.question div.content div.points br{display:none}#page-mod-quiz-edit div.question div.content div.points label{display:inline-block}#page-mod-quiz-edit div.quizpage .pagecontent .pagestatus{background-color:#fff}#page-mod-quiz-edit .quizpagedelete,#page-mod-quiz-edit .quizpagedelete img{background-color:transparent}#page-mod-quiz-edit div.quizpage .pagecontent{border:1px solid #ddd;-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px;overflow:hidden}#page-mod-quiz-edit div.questionbank .categoryinfo{padding:.3em 0}#page-mod-quiz-edit div.questionbank .modulespecificbuttonscontainer{padding:0}#page-mod-quiz-edit div.questionbank .modulespecificbuttonscontainer strong{display:block}#page-mod-quiz-edit div.questionbank .modulespecificbuttonscontainer hr,#page-mod-quiz-edit div.questionbank .modulespecificbuttonscontainer br{display:none}#page-mod-quiz-edit div.questionbank .modulespecificbuttonscontainer strong{margin-left:-0.3em}#page-mod-quiz-edit div.questionbank .modulespecificbuttonscontainer strong label{margin-left:.3em}#page-mod-quiz-edit div.questionbank .modulespecificbuttonscontainer input{margin-left:0}#page-mod-quiz-edit div.questionbank .modulespecificbuttonscontainer input+input{margin-left:5px}.questionbankwindow .module{width:auto}#page-mod-quiz-edit div.editq div.question div.content{background-color:#fff;border:1px solid #ddd;-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px;overflow:hidden}.path-mod-quiz .statedetails{display:block;font-size:.9em}a#hidebankcmd{color:#0070a8}.que.shortanswer .answer{padding:0}.que label{display:inline}body.path-question-type .mform fieldset.hidden{padding:0;margin:.7em 0 0}.userprofile .fullprofilelink{text-align:center;margin:10px}.userprofile .page-context-header{margin-bottom:10px}.userprofile .description{margin-top:10px;margin-bottom:30px}.userprofile .profile_tree{-webkit-column-count:2;-moz-column-count:2;column-count:2;-webkit-column-gap:20px;-moz-column-gap:20px;column-gap:20px}.userprofile .profile_tree section{display:inline-block;width:100%;border:1px solid #ddd;border-radius:4px;padding:0 15px;margin-bottom:20px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.userprofile .profile_tree section h3{font-size:18px;line-height:20px}.userprofile dl.list{*zoom:1}.userprofile dl.list:before,.userprofile dl.list:after{display:table;content:"";line-height:0}.userprofile dl.list:after{clear:both}.userprofile dl.list dt{float:left;width:180px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.userprofile dl.list dd{margin-left:200px}.user-box{margin:8px;width:115px;height:160px;text-align:center;float:left;clear:none}#page-user-profile .node_category ul,.path-user .node_category ul{margin-left:0;margin-right:0;list-style:none}#page-user-profile .node_category li,.path-user .node_category li{margin-top:5px}#page-user-profile .node_category .editprofile,.path-user .node_category .editprofile,#page-user-profile .node_category .viewmore,.path-user .node_category .viewmore{text-align:right}.dir-rtl .user-box{float:right}.dir-rtl .userprofile .node_category .editprofile,.dir-rtl .userprofile .node_category .viewmore{text-align:left}.dir-rtl .userprofile dl dd{margin-left:0;margin-right:10px}@media (max-width:480px){.userprofile .profile_tree{-webkit-column-count:1;-moz-column-count:1;column-count:1;-webkit-column-gap:20px;-moz-column-gap:20px;column-gap:20px}}.userlist .action-icon img{vertical-align:middle}.userlist #showall{margin:10px 0}.userlist .buttons{text-align:center}.userlist .buttons label{padding:0 3px}.userlist table#participants{text-align:center}.userlist table#participants td,.userlist table#participants th{vertical-align:middle;text-align:left;padding:4px}.userlist table.controls{width:100%}.userlist table.controls tr{vertical-align:top}.userlist table.controls .right{text-align:right}.userlist table.controls .groupselector{margin-bottom:0;margin-top:0}.userlist table.controls .groupselector label{display:block}.userinfobox{width:100%;border:1px solid;border-collapse:separate;padding:10px}.userinfobox .left,.userinfobox .side{width:100px;vertical-align:top}.userinfobox .userpicture{width:100px;height:100px}.userinfobox .content{vertical-align:top}.userinfobox .links{width:100px;padding:5px;vertical-align:bottom}.userinfobox .links a{display:block}.userinfobox .list td{padding:3px}.userinfobox .username{padding-bottom:20px;font-weight:bold}.userinfobox td.label{text-align:right;white-space:nowrap;vertical-align:top;font-weight:bold}.groupinfobox{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);border-color:#e3e3e3}.groupinfobox blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.groupinfobox .left{padding:10px;width:100px;vertical-align:top}.course-participation #showall{text-align:center;margin:10px 0}#user-policy .noticebox{text-align:center;margin-left:auto;margin-right:auto;margin-bottom:10px;width:80%;height:250px}#user-policy #policyframe{width:100%;height:100%}.iplookup #map{margin:auto}.userselector select{width:100%}.userselector div{margin-top:.2em}.userselector div label{margin-right:.3em}.userselector .userselector-infobelow{font-size:.8em}#userselector_options{padding:.3em 0}#userselector_options .collapsibleregioncaption{font-weight:bold}#userselector_options p{margin:.2em 0;text-align:left}.dir-rtl #userselector_options p{text-align:right}#page-user-profile .messagebox{text-align:center;margin-left:auto;margin-right:auto}#page-course-view-weeks .messagebox{text-align:center;margin-left:auto;margin-right:auto}.dir-rtl .userlist table#participants td,.dir-rtl .userlist table#participants th{text-align:right}.dir-rtl .userlist table#participants{margin:0 auto}#page-my-index.dir-rtl .block h3{text-align:right}.profileeditor>.singleselect{margin:0 .5em 0 0}.profileeditor>.singlebutton{display:inline-block;margin:0 0 0 .5em}.profileeditor>.singlebutton div,.profileeditor>.singlebutton input{margin:0}.dir-rtl .profileeditor>.singleselect{margin:0 0 0 .5em}.dir-rtl .profileeditor>.singlebutton{margin:0 .5em 0 0}#groupeditform .groups,#groupeditform .members{width:49%;float:left;text-align:left}.dir-rtl #groupeditform .groups,.dir-rtl #groupeditform .members{float:right;text-align:right}.preferences-group ul{list-style:none;margin-left:0;margin-right:0}.dir-rtl .preferences-group{float:right}.search-results .result{margin-left:0;margin-right:0}.dir-rtl .search-results .result{margin-right:15px;margin-left:0}.search-results .result .result-content{margin:7px 0}.search-results .result .filename{font-style:italic}.search-input-wrapper{margin:0 5px 0 2px;overflow:hidden;float:right;height:100%;width:16px;transition:width .5s ease,left .5s ease}.search-input-wrapper>div{float:left;margin:9px 0 10px 0}.dir-rtl .search-input-wrapper{margin:0 2px 0 5px;float:left}.dir-rtl .search-input-wrapper>div{float:right}.dir-rtl .search-input-wrapper>form{margin:7px 25px 7px 0}.search-input-wrapper>form{opacity:0;margin:7px 0 7px 25px;transition:opacity .5s ease-in-out}.search-input-wrapper>form>input{margin:0}.search-input-wrapper form.expanded{opacity:1}.search-input-wrapper.expanded{width:158px}/*! +.layout-option-noheader #page-header,.layout-option-nonavbar #page-navbar,.layout-option-nofooter #page-footer,.layout-option-nocourseheader .course-content-header,.layout-option-nocoursefooter .course-content-footer{display:none}.empty-region-side-pre #block-region-side-pre,.empty-region-side-post #block-region-side-post,.jsenabled.docked-region-side-post #block-region-side-post,.jsenabled.docked-region-side-pre #block-region-side-pre{display:none}.content-only #region-main.span9,.empty-region-side-post #region-bs-main-and-pre.span9,.empty-region-side-pre #region-bs-main-and-post.span9,.empty-region-side-post #region-bs-main-and-post.span9 #region-main.span8,.jsenabled.docked-region-side-post #region-bs-main-and-pre.span9,.jsenabled.docked-region-side-post #region-bs-main-and-post.span9 #region-main.span8,.jsenabled.docked-region-side-pre #region-bs-main-and-post.span9{width:100%}.empty-region-side-pre #region-bs-main-and-pre.span9 #region-main,.jsenabled.docked-region-side-pre #region-bs-main-and-pre.span9 #region-main{float:none;width:100%}.empty-region-side-pre #region-bs-main-and-post.span9 #region-main.span8,.jsenabled.docked-region-side-pre #region-bs-main-and-post.span9 #region-main.span8{float:right}.content-only #region-main-box,.content-only #region-main{width:100%}.empty-region-side-pre.used-region-side-post #region-main{width:100%}.empty-region-side-post.used-region-side-pre #region-main-box{width:100%}.jsenabled.docked-region-side-pre.empty-region-side-pre.used-region-side-post #region-main{width:100%}.jsenabled.docked-region-side-post.empty-region-side-post.used-region-side-pre #region-main-box{width:100%}.empty-region-side-post.used-region-side-pre #region-main.span8,.jsenabled.docked-region-side-post.used-region-side-pre #region-main.span8{width:74.46808511%;*width:74.41489362%}.empty-region-side-post.used-region-side-pre #block-region-side-pre.span4,.jsenabled.docked-region-side-post.used-region-side-pre #block-region-side-pre.span4{width:23.40425532%;*width:23.35106383%}.dir-ltr,.mdl-left,.dir-rtl .mdl-right{text-align:left}.dir-rtl,.mdl-right,.dir-rtl .mdl-left{text-align:right}#add,#remove,.centerpara,.mdl-align{text-align:center}a.dimmed,a.dimmed:link,a.dimmed:visited,a.dimmed_text,a.dimmed_text:link,a.dimmed_text:visited,.dimmed_text,.dimmed_text a,.dimmed_text a:link,.dimmed_text a:visited,.usersuspended,.usersuspended a,.usersuspended a:link,.usersuspended a:visited,.dimmed_category,.dimmed_category a{color:#999}.activity.label .dimmed_text{opacity:.5;filter:alpha(opacity=50)}.unlist,.unlist li,.inline-list,.inline-list li,.block .list,.block .list li,.section li.activity,.section li.movehere,.tabtree li{list-style:none;margin:0;padding:0}.inline,.inline-list li{display:inline}.notifytiny{font-size:10.5px}.notifytiny li,.notifytiny td{font-size:100%}.red,.notifyproblem{color:#b94a48}.green,.notifysuccess{color:#468847}.highlight{background:#d9edf7}.reportlink{text-align:right}a.autolink.glossary:hover{cursor:help}.collapsibleregioncaption{white-space:nowrap}.pagelayout-mydashboard.jsenabled .collapsibleregioncaption{cursor:pointer}.collapsibleregioncaption img{vertical-align:middle}.jsenabled .hiddenifjs{display:none}.visibleifjs{display:none}.jsenabled .visibleifjs{display:inline}.jsenabled .collapsibleregion{overflow:hidden}.jsenabled .collapsed .collapsibleregioninner{visibility:hidden}.collapsible-actions{display:none;text-align:right}.dir-rtl .collapsible-actions{text-align:left}.jsenabled .collapsible-actions{display:block}.collapsible-actions .collapseexpand{padding-left:20px;background:url([[pix:t/collapsed]]) 2px center no-repeat}.dir-rtl .collapsible-actions .collapseexpand{padding-right:20px;padding-left:0;background:url([[pix:t/collapsed_rtl]]) right center no-repeat}.collapsible-actions .collapse-all,.dir-rtl .collapsible-actions .collapse-all{background-image:url([[pix:t/expanded]])}.yui-overlay .yui-widget-bd{background-color:#FFEE69;border:1px solid #A6982B;border-top-color:#D4C237;color:#000000;left:0;padding:2px 5px;position:relative;top:0;z-index:1}.clearer{background:transparent;border-width:0;clear:both;display:block;height:1px;margin:0;padding:0}.bold,.warning,.errorbox .title,.pagingbar .title,.pagingbar .thispage{font-weight:bold}img.resize{height:1em;width:1em}.block img.resize,.breadcrumb img.resize{height:.9em;width:.8em}img.icon{height:16px;vertical-align:text-bottom;width:16px;padding-right:6px}.dir-rtl img.icon{padding-left:6px;padding-right:0}img.iconsmall{height:12px;margin-right:3px;vertical-align:middle;width:12px}img.iconhelp,.helplink img{height:16px;padding-left:3px;vertical-align:text-bottom;width:16px}h1 img.iconhelp,h1 img.icon,h2 img.iconhelp,h2 img.icon,h3 img.iconhelp,h3 img.icon,h4 img.iconhelp,h4 img.icon,h5 img.iconhelp,h5 img.icon,h6 img.iconhelp,h6 img.icon{vertical-align:middle;padding:4px}.dir-rtl img.iconhelp,.dir-rtl .helplink img{padding-right:3px;padding-left:0}img.iconlarge{height:24px;width:24px;vertical-align:middle}img.iconsort{vertical-align:text-bottom;padding-left:.3em;margin-bottom:.15em}.dir-rtl img.iconsort{padding-right:.3em;padding-left:0}img.icontoggle{height:17px;vertical-align:middle;width:50px}img.iconkbhelp{height:17px;width:49px}img.icon-pre,.dir-rtl img.icon-post{padding-right:3px;padding-left:0}img.icon-post,.dir-rtl img.icon-pre{padding-left:3px;padding-right:0}.boxaligncenter{margin-left:auto;margin-right:auto}.boxalignright{margin-left:auto;margin-right:0}.boxalignleft{margin-left:0;margin-right:auto}.boxwidthnarrow{width:30%}.boxwidthnormal{width:50%}.boxwidthwide{width:80%}.headermain{font-weight:bold}#maincontent{display:block;height:1px;overflow:hidden}img.uihint{cursor:help}#addmembersform table{margin-left:auto;margin-right:auto}table.flexible .emptyrow{display:none}img.emoticon{vertical-align:middle;width:15px;height:15px}form.popupform,form.popupform div{display:inline}.arrow_button input{overflow:hidden}.action-icon img.smallicon{vertical-align:text-bottom;margin:0 .3em}.no-overflow{overflow:auto;padding-bottom:1px}.pagelayout-report .no-overflow{overflow:visible}.no-overflow>.generaltable{margin-bottom:0}.accesshide{position:absolute;left:-10000px;font-weight:normal;font-size:1em}.dir-rtl .accesshide{top:-30000px;left:auto}span.hide,div.hide{display:none}a.skip-block,a.skip{position:absolute;top:-1000em;font-size:.85em;text-decoration:none}a.skip-block:focus,a.skip-block:active,a.skip:focus,a.skip:active{position:static;display:block}.skip-block-to{display:block;height:1px;overflow:hidden}.addbloglink{text-align:center}.blog_entry .audience{text-align:right;padding-right:4px}.blog_entry .tags{margin-top:15px}.blog_entry .tags .action-icon img.smallicon{height:16px;width:16px}.blog_entry .content{margin-left:43px}#page-group-index #groupeditform{text-align:center}#doc-contents h1{margin:1em 0 0 0}#doc-contents ul{margin:0;padding:0;width:90%}#doc-contents ul li{list-style-type:none}.groupmanagementtable td{vertical-align:top}.groupmanagementtable #existingcell,.groupmanagementtable #potentialcell{width:42%}.groupmanagementtable #buttonscell{width:16%}.groupmanagementtable #buttonscell p.arrow_button input{width:auto;min-width:80%;margin:0 auto}.groupmanagementtable #removeselect_wrapper,.groupmanagementtable #addselect_wrapper{width:100%}.groupmanagementtable #removeselect_wrapper label,.groupmanagementtable #addselect_wrapper label{font-weight:normal}.dir-rtl .groupmanagementtable p{text-align:right}#group-usersummary{width:14em}.groupselector{margin-top:3px;margin-bottom:3px;display:inline-block}.groupselector label{display:inline-block}.dataformatselector{margin:1em 0}.dataformatselector label{display:inline-block;margin:0 5px 10px 0;line-height:30px;vertical-align:top}.loginbox{margin:15px;overflow:visible}.loginbox.twocolumns{margin:15px}.loginbox h2,.loginbox .subcontent{margin:5px;padding:10px;text-align:center}.loginbox .loginpanel .desc{margin:0;padding:0;margin-bottom:5px;margin-top:15px}.loginbox .signuppanel .subcontent{text-align:left}.dir-rtl .loginbox .signuppanel .subcontent{text-align:right}.loginbox .loginsub{margin-left:0;margin-right:0}.loginbox .guestsub,.loginbox .forgotsub,.loginbox .potentialidps{margin:5px 12%}.loginbox .potentialidps .potentialidplist{margin-left:40%}.loginbox .potentialidps .potentialidplist div{text-align:left}.loginbox .loginform{margin-top:1em;text-align:left}.loginbox .loginform .form-label{float:left;text-align:right;width:49%;white-space:nowrap}.loginbox .loginform .form-input{float:right;width:50%}.loginbox .loginform .form-input input{width:6em}.loginbox .signupform{margin-top:1em;text-align:center}.loginbox.twocolumns .loginpanel,.loginbox.twocolumns .signuppanel{width:48%;border:0;margin:0;padding:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;float:left;margin-left:2.76243%;min-height:30px;margin-bottom:-2000px;padding-bottom:2000px}.dir-rtl .loginbox.twocolumns .loginpanel,.dir-rtl .loginbox.twocolumns .signuppanel{float:right}.loginbox .potentialidp .smallicon{vertical-align:text-bottom;margin:0 .3em}.notepost{margin-bottom:1em}.notepost .userpicture{float:left;margin-right:5px}.notepost .content,.notepost .footer{clear:both}.notesgroup{margin-left:20px}.path-my .coursebox .overview{margin:15px 30px 10px 30px}.path-my .coursebox .info{float:none;margin:0}.mod_introbox{padding:10px}table.mod_index{width:100%}.comment-ctrl{font-size:12px;display:none;margin:0;padding:0}.comment-ctrl h5{margin:0;padding:5px}.comment-area{max-width:400px;padding:5px}.comment-area textarea{width:100%;overflow:auto}.comment-area textarea.fullwidth{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.comment-area .fd{text-align:right}.comment-meta span{color:gray}.comment-link img{vertical-align:text-bottom}.comment-list{font-size:11px;overflow:auto;list-style:none;padding:0;margin:0}.comment-list li{margin:2px;list-style:none;margin-bottom:5px;clear:both;padding:.3em;position:relative}.comment-list li.first{display:none}.comment-paging{text-align:center}.comment-paging .pageno{padding:2px}.comment-paging .curpage{border:1px solid #CCC}.comment-message .picture{width:20px;float:left}.dir-rtl .comment-message .picture{float:right}.comment-message .text{margin:0;padding:0}.comment-message .text p{padding:0;margin:0 18px 0 0}.comment-delete{position:absolute;top:0;right:0;margin:.3em}.dir-rtl .comment-delete{position:absolute;left:0;right:auto;margin:.3em}.comment-report-selectall{display:none}.comment-link{display:none}.jsenabled .comment-link{display:block}.jsenabled .showcommentsnonjs{display:none}.jsenabled .comment-report-selectall{display:inline}.completion-expired{background:#f2dede}.completion-expected{font-size:10.5px}.completion-sortchoice,.completion-identifyfield{font-size:10.5px;vertical-align:bottom}.completion-progresscell{text-align:right}.completion-expired .completion-expected{font-weight:bold}img.user-image{height:100px;width:100px}#tag-search-box{text-align:center;margin:10px auto}.path-tag .tag-index-items .tagarea{border:1px solid #E3E3E3;border-radius:4px;padding:10px;margin-top:10px}.path-tag .tag-index-items .tagarea h3{display:block;padding:3px 0 10px 0;margin:0;font-size:1.1em;font-weight:bold;line-height:20px;color:#999;text-shadow:0 1px 0 rgba(255,255,255,0.5);text-transform:uppercase;word-wrap:break-word;border-bottom:solid 1px #E3E3E3;margin-bottom:10px}.path-tag .tagarea .controls,.path-tag .tagarea .taggeditems{*zoom:1}.path-tag .tagarea .controls:before,.path-tag .tagarea .taggeditems:before,.path-tag .tagarea .controls:after,.path-tag .tagarea .taggeditems:after{display:table;content:"";line-height:0}.path-tag .tagarea .controls:after,.path-tag .tagarea .taggeditems:after{clear:both}.path-tag .tagarea .controls,.path-tag .tag-backtoallitems{text-align:center}.path-tag .tagarea .controls .gotopage.nextpage{float:right}.path-tag .tagarea .controls .gotopage.prevpage{float:left}.path-tag .tagarea .controls .exclusivemode{display:inline-block}.dir-rtl.path-tag .tagarea .controls .gotopage.nextpage{float:left}.dir-rtl.path-tag .tagarea .controls .gotopage.prevpage{float:right}.path-tag .tagarea .controls.controls-bottom{margin-top:5px}.path-tag .tagarea .controls .gotopage.nextpage::after{padding-right:5px;padding-left:5px;content:"»"}.path-tag .tagarea .controls .gotopage.prevpage::before{padding-right:5px;padding-left:5px;content:"«"}span.flagged-tag,tr.flagged-tag,span.flagged-tag a,tr.flagged-tag a{color:#b94a48}.tag-management-table td,.tag-management-table th{vertical-align:middle;padding:4px}.tag-management-table .inplaceeditable.inplaceeditingon input{width:150px}.path-admin-tag .addstandardtags{float:right}.path-admin-tag .addstandardtags img{margin:0 5px}.dir-rtl.path-admin-tag .addstandardtags{float:left}.path-tag .tag-relatedtags{padding-top:10px}.path-tag .tag-management-box{text-align:right}.path-tag .tag-index-toc{padding:10px;text-align:center}.path-tag .tag-index-toc li,.path-tag .tag-management-box li{margin-left:5px;margin-right:5px}.path-tag .tag-management-box li a.edittag{background-image:url([[pix:moodle|i/settings]])}.path-tag .tag-management-box li a.flagasinappropriate{background-image:url([[pix:moodle|i/flagged]])}.path-tag .tag-management-box li a.removefrommyinterests{background-image:url([[pix:moodle|t/delete]])}.path-tag .tag-management-box li a.addtomyinterests{background-image:url([[pix:moodle|t/add]])}.path-tag .tag-management-box li a{background-repeat:no-repeat;background-position:left;padding-left:17px}.tag_feed.media-list .media .itemimage{float:left}.dir-rtl .tag_feed.media-list .media .itemimage{float:right}.tag_feed.media-list .media .itemimage img{height:35px;width:35px}.tag_feed.media-list .media .media-body{padding-right:10px;padding-left:10px}.tag_feed .media .muted a{color:#999}.tag_cloud{text-align:center}.tag_cloud .inline-list li{padding:0 .2em}.tag_cloud .tag_overflow{margin-top:1em;font-style:italic}.tag_cloud .s20{font-size:2.7em}.tag_cloud .s19{font-size:2.6em}.tag_cloud .s18{font-size:2.5em}.tag_cloud .s17{font-size:2.4em}.tag_cloud .s16{font-size:2.3em}.tag_cloud .s15{font-size:2.2em}.tag_cloud .s14{font-size:2.1em}.tag_cloud .s13{font-size:2em}.tag_cloud .s12{font-size:1.9em}.tag_cloud .s11{font-size:1.8em}.tag_cloud .s10{font-size:1.7em}.tag_cloud .s9{font-size:1.6em}.tag_cloud .s8{font-size:1.5em}.tag_cloud .s7{font-size:1.4em}.tag_cloud .s6{font-size:1.3em}.tag_cloud .s5{font-size:1.2em}.tag_cloud .s4{font-size:1.1em}.tag_cloud .s3{font-size:1em}.tag_cloud .s2{font-size:.9em}.tag_cloud .s1{font-size:.8em}.tag_cloud .s0{font-size:.7em}.tag_list ul{display:inline}.tag_list.hideoverlimit .overlimit{display:none}.tag_list .tagmorelink{display:none}.tag_list.hideoverlimit .tagmorelink{display:inline}.tag_list.hideoverlimit .taglesslink{display:none}#webservice-doc-generator td{text-align:left;border:0 solid black}.smartselect{position:absolute}.smartselect .smartselect_mask{background-color:#fff}.smartselect ul{padding:0;margin:0}.smartselect ul li{list-style:none}.smartselect .smartselect_menu{margin-right:5px}.safari .smartselect .smartselect_menu{margin-left:2px}.smartselect .smartselect_menu,.smartselect .smartselect_submenu{border:1px solid #000;background-color:#FFF;display:none}.smartselect .smartselect_menu.visible,.smartselect .smartselect_submenu.visible{display:block}.smartselect .smartselect_menu_content ul li{position:relative;padding:2px 5px}.smartselect .smartselect_menu_content ul li a{color:#333;text-decoration:none}.smartselect .smartselect_menu_content ul li a.selectable{color:inherit}.smartselect .smartselect_submenuitem{background-image:url([[pix:moodle|t/collapsed]]);background-repeat:no-repeat;background-position:100%}.smartselect.spanningmenu .smartselect_submenu{position:absolute;top:-1px;left:100%}.smartselect.spanningmenu .smartselect_submenu a{white-space:nowrap;padding-right:16px}.smartselect.spanningmenu .smartselect_menu_content ul li a.selectable:hover{text-decoration:underline}.smartselect.compactmenu .smartselect_submenu{position:relative;margin:2px -3px;margin-left:10px;display:none;border-width:0;z-index:1010}.smartselect.compactmenu .smartselect_submenu.visible{display:block}.smartselect.compactmenu .smartselect_menu{z-index:1000;overflow:hidden}.smartselect.compactmenu .smartselect_submenu .smartselect_submenu{z-index:1020}.smartselect.compactmenu .smartselect_submenuitem:hover>.smartselect_menuitem_label{font-weight:bold}#page-admin-registration-register .registration_textfield{width:300px}.userenrolment{width:100%;border-collapse:collapse}.userenrolment tr{vertical-align:top}.userenrolment td{padding:0;height:41px}.userenrolment .subfield{margin-right:5px}.userenrolment .col_userdetails .subfield_picture{float:left}.userenrolment .col_lastseen{width:150px}.userenrolment .col_role{width:262px}.userenrolment .col_role .roles,.userenrolment .col_group .groups{margin-right:30px}.userenrolment .col_role .role,.userenrolment .col_group .group{float:left;padding:3px;margin:3px;white-space:nowrap}.userenrolment .col_role .role a,.userenrolment .col_group .group a{margin-left:3px;cursor:pointer}.userenrolment .col_role .addrole,.userenrolment .col_group .addgroup{float:right;padding:3px;margin:3px}.userenrolment .col_role .addrole>a:hover,.userenrolment .col_group .addgroup>a:hover{border-bottom:1px solid #666}.userenrolment .col_role .addrole img,.userenrolment .col_group .addgroup img{vertical-align:baseline}.dir-rtl .userenrolment .col_role .role{float:right}.userenrolment .hasAllRoles .col_role .addrole{display:none}.userenrolment .col_enrol .enrolment{float:left;padding:3px;margin:3px}.userenrolment .col_enrol .enrolment a{float:right;margin-left:3px}#page-enrol-users .enrol_user_buttons{float:right}#page-enrol-users .enrol_user_buttons .enrolusersbutton{display:inline}#page-enrol-users .enrol_user_buttons .enrolusersbutton div,#page-enrol-users .enrol_user_buttons .enrolusersbutton form{display:inline;margin-right:0}#page-enrol-users #filterform{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);border-color:#e3e3e3;padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;display:inline-block}#page-enrol-users #filterform blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}#page-enrol-users #filterform .fitem{display:inline-block;line-height:40px;margin-right:.3em;white-space:nowrap}#page-enrol-users #filterform .fitem label{display:inline;line-height:20px;padding-right:.3em}#page-enrol-users #filterform .fitem :before,#page-enrol-users #filterform .fitem :after{display:inline}#page-enrol-users #filterform div,#page-enrol-users #filterform fieldset{display:inline;float:none;clear:none;width:auto;margin:0}#page-enrol-users #filterform select,#page-enrol-users #filterform .ftext input{width:7em}#page-enrol-users #filterform input,#page-enrol-users #filterform select{margin-bottom:0}#page-enrol-users .user-enroller-panel .uep-search-results .user .details{width:237px}#page-enrol-users .user-enroller-panel .uep-search-results .cohort .details{width:237px}.dir-rtl#page-enrol-users .col_userdetails .subfield_picture{float:right}.dir-rtl#page-enrol-users .enrol_user_buttons{float:left}.dir-rtl#page-enrol-users .enrol_user_buttons .enrolusersbutton{margin-left:0;margin-right:1em}.dir-rtl#page-enrol-users .enrol_user_buttons .enrolusersbutton div{margin-left:0}.dir-rtl#page-enrol-users #filterform .fitem{margin-right:0;margin-left:.3em}.dir-rtl#page-enrol-users #filterform .fitem label{padding-right:0;padding-left:.3em}#page-enrol-users .enrol-users-page-action input{margin-left:0}.dir-rtl .headermain{float:right}.dir-rtl .headermenu{float:left}.dir-rtl .loginbox .loginform .form-label{float:right;text-align:left}.dir-rtl .loginbox .loginform .form-input{text-align:right;margin-right:1%}.dir-rtl .yui3-menu-hidden{left:0}#page-admin-roles-define.dir-rtl #rolesform .felement{margin-right:180px}#page-message-edit.dir-rtl table.generaltable th.c0{text-align:right}.corelightbox{background-color:#CCC;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center}.corelightbox img{position:fixed;top:50%;left:50%}.mod-indent-outer{display:table}.mod-indent{display:table-cell}.label .mod-indent{float:left;padding-top:20px}.mod-indent-1{width:30px}.mod-indent-2{width:60px}.mod-indent-3{width:90px}.mod-indent-4{width:120px}.mod-indent-5{width:150px}.mod-indent-6{width:180px}.mod-indent-7{width:210px}.mod-indent-8{width:240px}.mod-indent-9{width:270px}.mod-indent-10{width:300px}.mod-indent-11{width:330px}.mod-indent-12{width:360px}.mod-indent-13{width:390px}.mod-indent-14{width:420px}.mod-indent-15{width:450px}.mod-indent-16{width:480px}.mod-indent-huge{width:480px}.resourcecontent .mediaplugin_mp3 object{height:25px;width:600px}.resourcecontent audio.mediaplugin_html5audio{width:600px}.resourceimage{max-width:100%}.mediaplugin_mp3 object{height:15px;width:300px}audio.mediaplugin_html5audio{width:300px}.core_media_preview.pagelayout-embedded #content{padding:0}.core_media_preview.pagelayout-embedded #maincontent{height:0}body#page-lib-editor-tinymce-plugins-moodlemedia-preview{padding:0;margin:0;min-width:0;background:none}.dir-rtl .ygtvtn,.dir-rtl .ygtvtm,.dir-rtl .ygtvtmh,.dir-rtl .ygtvtmhh,.dir-rtl .ygtvtp,.dir-rtl .ygtvtph,.dir-rtl .ygtvtphh,.dir-rtl .ygtvln,.dir-rtl .ygtvlm,.dir-rtl .ygtvlmh,.dir-rtl .ygtvlmhh,.dir-rtl .ygtvlp,.dir-rtl .ygtvlph,.dir-rtl .ygtvlphh,.dir-rtl .ygtvdepthcell,.dir-rtl .ygtvok,.dir-rtl .ygtvok:hover,.dir-rtl .ygtvcancel,.dir-rtl .ygtvcancel:hover{width:18px;height:22px;background-image:url([[pix:theme|yui2-treeview-sprite-rtl]]);background-repeat:no-repeat;cursor:pointer}.dir-rtl .ygtvtn{background-position:0 -5600px}.dir-rtl .ygtvtm{background-position:0 -4000px}.dir-rtl .ygtvtmh,.dir-rtl .ygtvtmhh{background-position:0 -4800px}.dir-rtl .ygtvtp{background-position:0 -6400px}.dir-rtl .ygtvtph,.dir-rtl .ygtvtphh{background-position:0 -7200px}.dir-rtl .ygtvln{background-position:0 -1600px}.dir-rtl .ygtvlm{background-position:0 0}.dir-rtl .ygtvlmh,.dir-rtl .ygtvlmhh{background-position:0 -800px}.dir-rtl .ygtvlp{background-position:0 -2400px}.dir-rtl .ygtvlph,.dir-rtl .ygtvlphh{background-position:0 -3200px}.dir-rtl .ygtvdepthcell{background-position:0 -8000px}.dir-rtl .ygtvok{background-position:0 -8800px}.dir-rtl .ygtvok:hover{background-position:0 -8844px}.dir-rtl .ygtvcancel{background-position:0 -8822px}.dir-rtl .ygtvcancel:hover{background-position:0 -8866px}.dir-rtl.yui-skin-sam .yui-panel .hd{text-align:right}.dir-rtl .yui-skin-sam .yui-layout .yui-layout-unit div.yui-layout-bd{text-align:right}.dir-rtl .clearlooks2.ie9 .mceAlert .mceMiddle span,.dir-rtl .clearlooks2 .mceConfirm .mceMiddle span{top:44px}.dir-rtl .o2k7Skin table,.dir-rtl .o2k7Skin tbody,.dir-rtl .o2k7Skin a,.dir-rtl .o2k7Skin img,.dir-rtl .o2k7Skin tr,.dir-rtl .o2k7Skin div,.dir-rtl .o2k7Skin td,.dir-rtl .o2k7Skin iframe,.dir-rtl .o2k7Skin span,.dir-rtl .o2k7Skin *,.dir-rtl .o2k7Skin .mceText,.dir-rtl .o2k7Skin .mceListBox .mceText{text-align:right}.path-rating .ratingtable{width:100%;margin-bottom:1em}.path-rating .ratingtable th.rating{width:100%}.path-rating .ratingtable td.rating,.path-rating .ratingtable td.time{white-space:nowrap;text-align:center}.initialbar a,.initialbar strong{padding-left:3px;padding-right:3px}.moodle-dialogue-base .moodle-dialogue-lightbox{background-color:#AAA}.moodle-dialogue-base .hidden,.moodle-dialogue-base .moodle-dialogue-hidden{display:none}.no-scrolling{overflow:hidden}.moodle-dialogue-base .moodle-dialogue-fullscreen{left:0;top:0;right:0;bottom:-50px;position:fixed}.moodle-dialogue-base .moodle-dialogue-fullscreen .moodle-dialogue-content{overflow:auto}.moodle-dialogue-base .moodle-dialogue-fullscreen .closebutton{width:28px;height:16px;background-size:100%}.moodle-dialogue-base .moodle-dialogue{padding:0;margin:0;background:none;border:none;z-index:600;outline:#000 dotted 0}.moodle-dialogue-base .moodle-dialogue-wrap{margin-top:-3px;margin-left:-3px;background-color:#fff;border:1px solid #ccc;-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;-webkit-box-shadow:5px 5px 20px 0 #666;-moz-box-shadow:5px 5px 20px 0 #666;box-shadow:5px 5px 20px 0 #666}.moodle-dialogue-base .moodle-dialogue-wrap .moodle-dialogue-hd,.moodle-dialogue-base .moodle-dialogue-wrap .moodle-dialogue-hd.yui3-widget-hd{margin:0;padding:5px;font-size:12px;font-weight:normal;letter-spacing:1px;color:#333;text-align:center;text-shadow:1px 1px 1px #fff;-webkit-border-radius:10px 10px 0 0;-moz-border-radius:10px 10px 0 0;border-radius:10px 10px 0 0;border-bottom:1px solid #bbb;background:#ccc;background-color:#ebebeb;background-image:-moz-linear-gradient(top, #fff, #ccc);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#ccc));background-image:-webkit-linear-gradient(top, #fff, #ccc);background-image:-o-linear-gradient(top, #fff, #ccc);background-image:linear-gradient(to bottom, #fff, #ccc);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffcccccc', GradientType=0);filter:0}.moodle-dialogue-base .moodle-dialogue-wrap .moodle-dialogue-hd h1{margin:0;padding:0;display:inline;font-size:100%;font-weight:bold}.moodle-dialogue-base .moodle-dialogue-wrap .moodle-dialogue-hd .yui3-widget-buttons{padding:5px}.moodle-dialogue-base .closebutton{width:25px;height:15px;float:right;vertical-align:middle;display:inline-block;cursor:pointer;padding:0;background-image:url([[pix:theme|sprite]]);background-repeat:no-repeat;border-style:none}.dir-rtl .moodle-dialogue-base .moodle-dialogue-wrap .moodle-dialogue-hd .yui3-widget-buttons{left:0;right:auto}.moodle-dialogue-base .moodle-dialogue .moodle-dialogue-bd{padding:1em;line-height:2em;color:#555;font-size:12px}.moodle-dialogue-base .moodle-dialogue-wrap .moodle-dialogue-content{padding:0;background:#FFF}.moodle-dialogue-base .moodle-dialogue-fullscreen .moodle-dialogue-hd{padding:10px;font-size:16px}.moodle-dialogue-base .moodle-dialogue-fullscreen .moodle-dialogue-content{overflow:auto;position:absolute;top:0;bottom:50px;left:0;right:0;margin:0;border:0}.moodle-dialogue-base .moodle-dialogue-fullscreen .moodle-dialogue-hd,.moodle-dialogue-base .moodle-dialogue-fullscreen .moodle-dialogue-wrap{border-radius:0}.moodle-dialogue-confirm .confirmation-dialogue{text-align:center}.moodle-dialogue-confirm .confirmation-dialogue input{text-align:center}.moodle-dialogue-exception .moodle-exception-message{text-align:center}.moodle-dialogue-exception .moodle-exception-param label{font-weight:bold}.moodle-dialogue-exception .param-stacktrace label{background-color:#EEE;border:1px solid #ccc;border-bottom-width:0}.moodle-dialogue-exception .param-stacktrace pre{border:1px solid #ccc;background-color:#fff}.moodle-dialogue-exception .param-stacktrace .stacktrace-file{color:navy;font-size:11.9px}.moodle-dialogue-exception .param-stacktrace .stacktrace-line{color:#b94a48;font-size:11.9px}.moodle-dialogue-exception .param-stacktrace .stacktrace-call{color:#333;font-size:90%;border-bottom:1px solid #eee}.moodle-dialogue-base .moodle-dialogue .moodle-dialogue-content .moodle-dialogue-ft{padding:0;margin:.7em 1em;text-align:right;background-color:#FFF;font-size:12px}.moodle-dialogue-confirm .confirmation-message{margin:.5em 1em}.moodle-dialogue-confirm .confirmation-dialogue input{min-width:80px}.moodle-dialogue-exception .moodle-exception-message{margin:1em}.moodle-dialogue-exception .moodle-exception-param{margin-bottom:.5em}.moodle-dialogue-exception .moodle-exception-param label{width:150px}.moodle-dialogue-exception .param-stacktrace label{display:block;margin:0;padding:4px 1em}.moodle-dialogue-exception .param-stacktrace pre{display:block;height:200px;overflow:auto}.moodle-dialogue-exception .param-stacktrace .stacktrace-file{display:inline-block;margin:4px 0}.moodle-dialogue-exception .param-stacktrace .stacktrace-line{display:inline-block;width:50px;margin:4px 1em}.moodle-dialogue-exception .param-stacktrace .stacktrace-call{padding-left:25px;margin-bottom:4px;padding-bottom:4px}.moodle-dialogue .moodle-dialogue-bd .content-lightbox{opacity:.75;filter:alpha(opacity=75);width:100%;height:100%;top:0;left:0;background-color:white;text-align:center;padding:10% 0}.moodle-dialogue .tooltiptext{max-height:300px}.moodle-dialogue-base .moodle-dialogue.moodle-dialogue-tooltip{z-index:3001}.moodle-dialogue-base .moodle-dialogue.moodle-dialogue-tooltip .moodle-dialogue-bd{overflow:auto}#page-question-edit.dir-rtl a.container-close{right:auto;left:6px}.chooserdialoguebody,.choosertitle{display:none}.moodle-dialogue.chooserdialogue .moodle-dialogue-content .moodle-dialogue-ft{margin:0}.chooserdialogue .moodle-dialogue-wrap .moodle-dialogue-bd{padding:0;background:#F2F2F2;-webkit-border-bottom-right-radius:10px;-moz-border-radius-bottomright:10px;border-bottom-right-radius:10px;-webkit-border-bottom-left-radius:10px;-moz-border-radius-bottomleft:10px;border-bottom-left-radius:10px}.choosercontainer #chooseform .submitbuttons{padding:.7em 0;text-align:center}@media (max-height:639px){.ios.safari .choosercontainer #chooseform .submitbuttons{padding:45px 0}}.choosercontainer #chooseform .submitbuttons input{min-width:100px;margin:0 .5em}.choosercontainer #chooseform .options{position:relative;border-bottom:1px solid #BBBBBB}.jschooser .choosercontainer #chooseform .alloptions{overflow-x:hidden;overflow-y:auto;max-width:20.3em;-webkit-box-shadow:inset 0 0 30px 0 #ccc;-moz-box-shadow:inset 0 0 30px 0 #ccc;box-shadow:inset 0 0 30px 0 #ccc}.jschooser .choosercontainer #chooseform .alloptions .option input[type=radio]{display:inline-block}.jschooser .choosercontainer #chooseform .alloptions .option .modicon{display:inline-block}.jschooser .choosercontainer #chooseform .alloptions .option .typename{display:inline-block;width:65%}.dir-rtl.jschooser .choosercontainer #chooseform .alloptions{max-width:18.3em}.choosercontainer #chooseform .moduletypetitle,.choosercontainer #chooseform .option,.choosercontainer #chooseform .nonoption{margin-bottom:0;padding:0 1.6em 0 1.6em}.choosercontainer #chooseform .moduletypetitle{text-transform:uppercase;padding-top:1.2em;padding-bottom:.4em}.choosercontainer #chooseform .option .typename,.choosercontainer #chooseform .option span.modicon img.icon,.choosercontainer #chooseform .nonoption .typename,.choosercontainer #chooseform .nonoption span.modicon img.icon{padding:0 0 0 .5em}.dir-rtl .choosercontainer #chooseform .option .typename,.dir-rtl .choosercontainer #chooseform .option span.modicon img.icon,.dir-rtl .choosercontainer #chooseform .nonoption .typename,.dir-rtl .choosercontainer #chooseform .nonoption span.modicon img.icon{padding:0 .5em 0 0}.chooserdialogue-course-modchooser .choosercontainer #chooseform .option span.modicon img.icon,.chooserdialogue-course-modchooser .choosercontainer #chooseform .nonoption span.modicon img.icon{height:24px;width:24px}.choosercontainer #chooseform .option input[type=radio],.choosercontainer #chooseform .option span.typename,.choosercontainer #chooseform .option span.modicon{vertical-align:middle}.choosercontainer #chooseform .option label{display:block;padding:.3em 0 .1em 0;border-bottom:1px solid #FFFFFF}.choosercontainer #chooseform .nonoption{padding-left:2.7em;padding-top:.3em;padding-bottom:.1em}.dir-rtl .choosercontainer #chooseform .nonoption{padding-right:2.7em;padding-left:0}.choosercontainer #chooseform .subtype{margin-bottom:0;padding:0 1.6em 0 3.2em}.dir-rtl .choosercontainer #chooseform .subtype{padding:0 3.2em 0 1.6em}.choosercontainer #chooseform .subtype .typename{margin:0 0 0 .2em}.dir-rtl .choosercontainer #chooseform .subtype .typename{margin:0 .2em 0 0}.jschooser .choosercontainer #chooseform .instruction,.jschooser .choosercontainer #chooseform .typesummary{display:none;position:absolute;top:0;right:0;bottom:0;left:20.3em;margin:0;padding:1.6em;background-color:#fff;overflow-x:hidden;overflow-y:auto;line-height:2em}.dir-rtl.jschooser .choosercontainer #chooseform .instruction,.dir-rtl.jschooser .choosercontainer #chooseform .typesummary{left:0;right:18.5em;border-right:1px solid grey}.jschooser .choosercontainer #chooseform .instruction,.choosercontainer #chooseform .selected .typesummary{display:block}.choosercontainer #chooseform .selected{background-color:#fff;-webkit-box-shadow:0 0 10px 0 #ccc;-moz-box-shadow:0 0 10px 0 #ccc;box-shadow:0 0 10px 0 #ccc}.section-modchooser-link img.smallicon{padding:3px}.formlistingradio{padding-bottom:25px;padding-right:10px}.formlistinginputradio{float:left}.formlistingmain{min-height:225px}.formlisting{position:relative;margin:15px 0;padding:1px 19px 14px;background-color:white;border:1px solid #DDD;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.formlistingmore{position:absolute;cursor:pointer;bottom:-1px;right:-1px;padding:3px 7px;font-size:12px;font-weight:bold;background-color:whiteSmoke;border:1px solid #ddd;color:#9DA0A4;-webkit-border-radius:4px 0 4px 0;-moz-border-radius:4px 0 4px 0;border-radius:4px 0 4px 0}.formlistingall{margin:15px 0;padding:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.formlistingrow{cursor:pointer;border-bottom:1px solid;border-color:#E1E1E8;border-left:1px solid #E1E1E8;border-right:1px solid #E1E1E8;background-color:#F7F7F9;-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;padding:6px;top:50%;left:50%;min-height:34px;float:left;width:150px}body.jsenabled .formlistingradio{display:none}body.jsenabled .formlisting{display:block}table.collection{width:100%;margin-bottom:20px;border:1px solid #ddd;border-collapse:separate;*border-collapse:collapse;border-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}table.collection th,table.collection td{padding:8px;line-height:20px;text-align:left;vertical-align:top;border-top:1px solid #ddd}table.collection th{font-weight:bold}table.collection thead th{vertical-align:bottom}table.collection caption+thead tr:first-child th,table.collection caption+thead tr:first-child td,table.collection colgroup+thead tr:first-child th,table.collection colgroup+thead tr:first-child td,table.collection thead:first-child tr:first-child th,table.collection thead:first-child tr:first-child td{border-top:0}table.collection tbody+tbody{border-top:2px solid #ddd}table.collection .table{background-color:#fff}table.collection th,table.collection td{border-left:1px solid #ddd}table.collection caption+thead tr:first-child th,table.collection caption+tbody tr:first-child th,table.collection caption+tbody tr:first-child td,table.collection colgroup+thead tr:first-child th,table.collection colgroup+tbody tr:first-child th,table.collection colgroup+tbody tr:first-child td,table.collection thead:first-child tr:first-child th,table.collection tbody:first-child tr:first-child th,table.collection tbody:first-child tr:first-child td{border-top:0}table.collection thead:first-child tr:first-child>th:first-child,table.collection tbody:first-child tr:first-child>td:first-child,table.collection tbody:first-child tr:first-child>th:first-child{-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px}table.collection thead:first-child tr:first-child>th:last-child,table.collection tbody:first-child tr:first-child>td:last-child,table.collection tbody:first-child tr:first-child>th:last-child{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px}table.collection thead:last-child tr:last-child>th:first-child,table.collection tbody:last-child tr:last-child>td:first-child,table.collection tbody:last-child tr:last-child>th:first-child,table.collection tfoot:last-child tr:last-child>td:first-child,table.collection tfoot:last-child tr:last-child>th:first-child{-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px}table.collection thead:last-child tr:last-child>th:last-child,table.collection tbody:last-child tr:last-child>td:last-child,table.collection tbody:last-child tr:last-child>th:last-child,table.collection tfoot:last-child tr:last-child>td:last-child,table.collection tfoot:last-child tr:last-child>th:last-child{-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px}table.collection tfoot+tbody:last-child tr:last-child td:first-child{-webkit-border-bottom-left-radius:0;-moz-border-radius-bottomleft:0;border-bottom-left-radius:0}table.collection tfoot+tbody:last-child tr:last-child td:last-child{-webkit-border-bottom-right-radius:0;-moz-border-radius-bottomright:0;border-bottom-right-radius:0}table.collection caption+thead tr:first-child th:first-child,table.collection caption+tbody tr:first-child td:first-child,table.collection colgroup+thead tr:first-child th:first-child,table.collection colgroup+tbody tr:first-child td:first-child{-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px}table.collection caption+thead tr:first-child th:last-child,table.collection caption+tbody tr:first-child td:last-child,table.collection colgroup+thead tr:first-child th:last-child,table.collection colgroup+tbody tr:first-child td:last-child{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px}table.collection tbody>tr:nth-child(odd)>td,table.collection tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}table.collection .name{text-align:left;vertical-align:middle}table.collection .awards{width:10%;text-align:center;vertical-align:middle}table.collection .criteria{width:40%;text-align:left;vertical-align:top}table.collection .badgeimage,table.collection .status{width:15%;text-align:center;vertical-align:middle}table.collection .description{width:25%;text-align:left}.dir-rtl table.collection .name,.dir-rtl table.collection .criteria,.dir-rtl table.collection .description{text-align:right}table.collection .actions{width:11em;text-align:center;vertical-align:middle}a.criteria-action{padding:0 3px;float:right}div.criteria-description{padding:10px 15px;margin:5px 0;background:none repeat scroll 0 0 #f9f9f9;border:1px solid #EEE}ul.badges{margin:0;list-style:none}.badges li{position:relative;display:inline-block;padding-top:1em;text-align:center;vertical-align:top;width:150px}.badges li .badge-name{display:block;padding:5px}.badges li>img{position:absolute}.badges li .badge-image{width:100px;height:100px;left:10px;top:0;z-index:1}.badges li .badge-actions{position:relative}.badges li .expireimage{width:100px;height:100px;left:25px;top:0;position:absolute;z-index:10;opacity:.85}#badge-image{background-color:transparent;padding:0;position:relative;min-width:100px;width:20%;display:inline-block;vertical-align:top;margin-top:17px}#badge-image .expireimage{width:100px;height:100px;left:0;top:0;opacity:.85;filter:alpha(opacity=85);position:absolute;z-index:10}#badge-image .singlebutton{padding-top:5px}#badge-image .singlebutton input{margin-left:0}.dir-rtl #badge-image{float:right}.dir-rtl #badge-image .expireimage{left:41px}#badge-details{display:inline-block;width:79%}#badge-overview dl,#badge-details dl{margin:0}#badge-overview dl dt,#badge-details dl dt,#badge-overview dl dd,#badge-details dl dd{vertical-align:top;padding:3px 0}#badge-overview dl dt,#badge-details dl dt{clear:both;display:inline-block;width:20%;min-width:100px}#badge-overview dl dd,#badge-details dl dd{display:inline-block;width:79%;margin-left:1%}.badge-profile{vertical-align:top}.connected{color:#468847}.notconnected{color:#b94a48}.connecting{color:#8a6d3b}#page-badges-award .recipienttable tr td{vertical-align:top}#page-badges-award .recipienttable tr td.actions .actionbutton{margin:.3em 0;padding:.5em 0;width:100%}#page-badges-award .recipienttable tr td.existing,#page-badges-award .recipienttable tr td.potential{width:42%}#issued-badge-table .activatebadge{display:inline-block}.statusbox.active{background-color:#dff0d8}.statusbox.inactive{background-color:#fcf8e3}.statusbox{text-align:center;margin-bottom:5px;padding:5px}.statusbox .activatebadge{display:inline-block}.statusbox .activatebadge input[type=submit]{margin:3px}.activatebadge{margin:0;text-align:left;vertical-align:middle}.dir-rtl .activatebadge{text-align:right}img#persona_signin{cursor:pointer}.addcourse{float:right}.invisiblefieldset{display:inline;margin:0;padding:0;border-width:0}.breadcrumb-nav{float:left;margin-bottom:10px}.dir-rtl .breadcrumb-nav{float:right}.breadcrumb-button .singlebutton div{margin-right:0}.breadcrumb-nav .breadcrumb{margin:0}.page-context-header{overflow:hidden}.page-context-header .page-header-image,.page-context-header .page-header-headings{display:block;position:relative}.page-context-header .page-header-image{margin-bottom:1em}.page-context-header .page-header-headings{margin-top:30px;margin-bottom:10px}.page-context-header .page-header-headings h1{display:block}.page-context-header .page-header-headings,.page-context-header .header-button-group{position:relative;line-height:24px;vertical-align:middle}.page-context-header .header-button-group{display:block}.page-context-header .header-button-group a{position:relative;top:-0.4em}.dir-ltr .page-context-header .page-header-image{float:left;margin-right:1em}.dir-ltr .page-context-header .header-button-group{float:left}.dir-rtl .page-context-header .page-header-image{float:right;margin-left:1em}.dir-rtl .page-context-header .header-button-group{float:right}.moodle-actionmenu,.moodle-actionmenu>ul,.moodle-actionmenu>ul>li{display:inline-block}.moodle-actionmenu ul{padding:0;margin:0;list-style-type:none}.section_action_menu .moodle-actionmenu ul.menubar{margin:0}.section_action_menu .moodle-actionmenu ul.menu{margin:0 10px 10px 0}.moodle-actionmenu .toggle-display,.moodle-actionmenu .menu-action-text{display:none}.jsenabled .moodle-actionmenu[data-enhance]{display:block}.jsenabled .moodle-actionmenu[data-enhance] .menu{display:none}.jsenabled .moodle-actionmenu[data-enhance] .toggle-display{display:inline;opacity:.5;filter:alpha(opacity=50)}.jsenabled .moodle-actionmenu[data-enhance] .toggle-display.textmenu{display:block;margin-left:4px;padding-left:4px;padding-right:4px}.jsenabled .moodle-actionmenu[data-enhance] .toggle-display.textmenu .iconsmall,.jsenabled .moodle-actionmenu[data-enhance] .toggle-display.textmenu .smallicon{margin:4px 4px 4px 0;padding:8px 4px 0 2px;vertical-align:text-bottom}.jsenabled .moodle-actionmenu[data-enhance] .toggle-display.textmenu .caret{margin-top:8px;margin-left:2px;border-top-color:#777}.jsenabled .moodle-actionmenu[data-enhance] .toggle-display.textmenu .caret:hover,.jsenabled .moodle-actionmenu[data-enhance] .toggle-display.textmenu .caret:active{border-top-color:#555}.jsenabled .moodle-actionmenu[data-enhanced] .toggle-display{opacity:1;filter:alpha(opacity=100)}.jsenabled .moodle-actionmenu[data-enhanced] .menu-action-text{display:inline}.jsenabled.dir-rtl .moodle-actionmenu[data-enhance] .toggle-display.textmenu{margin-left:initial;margin-right:4px}.jsenabled.dir-rtl .moodle-actionmenu[data-enhance] .toggle-display.textmenu .caret{margin-left:initial;margin-right:2px}.moodle-actionmenu[data-enhanced].show{position:relative}.moodle-actionmenu[data-enhanced].show .menu{display:block;position:absolute;text-align:left;background-color:#fff;border:1px solid rgba(0,0,0,0.2);z-index:1000;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;-webkit-box-shadow:5px 5px 20px 0 #666;-moz-box-shadow:5px 5px 20px 0 #666;box-shadow:5px 5px 20px 0 #666}.moodle-actionmenu[data-enhanced].show .menu a{display:block;color:#333;padding:2px 1em 2px 28px}.moodle-actionmenu[data-enhanced].show .menu a:hover{color:#fff;background-color:#0070a8}.moodle-actionmenu[data-enhanced].show .menu a:first-child{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px}.moodle-actionmenu[data-enhanced].show .menu a:last-child{-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px}.moodle-actionmenu[data-enhanced].show .menu a.hidden{display:none}.moodle-actionmenu[data-enhanced].show .menu img{vertical-align:middle}.moodle-actionmenu[data-enhanced].show .menu .iconsmall,.moodle-actionmenu[data-enhanced].show .menu .smallicon{margin:4px 4px 4px -24px;padding:4px}.moodle-actionmenu[data-enhanced].show .menu>li{display:block}.moodle-actionmenu[data-enhanced].show .menu.align-tl-bl{top:100%;left:0;margin-top:4px}.moodle-actionmenu[data-enhanced].show .menu.align-tr-bl{top:100%;right:100%}.moodle-actionmenu[data-enhanced].show .menu.align-bl-bl{bottom:100%;left:0}.moodle-actionmenu[data-enhanced].show .menu.align-br-bl{bottom:100%;right:100%}.moodle-actionmenu[data-enhanced].show .menu.align-tl-br{top:100%;left:100%}.moodle-actionmenu[data-enhanced].show .menu.align-tr-br{top:100%;right:0;margin-top:4px}.moodle-actionmenu[data-enhanced].show .menu.align-bl-br{bottom:100%;left:100%}.moodle-actionmenu[data-enhanced].show .menu.align-br-br{bottom:100%;right:0}.moodle-actionmenu[data-enhanced].show .menu.align-tl-tl{top:0;left:0}.moodle-actionmenu[data-enhanced].show .menu.align-tr-tl{top:0;right:100%;margin-right:4px}.moodle-actionmenu[data-enhanced].show .menu.align-bl-tl{bottom:100%;left:0;margin-bottom:4px}.moodle-actionmenu[data-enhanced].show .menu.align-br-tl{bottom:100%;right:100%}.moodle-actionmenu[data-enhanced].show .menu.align-tl-tr{top:0;left:100%;margin-left:4px}.moodle-actionmenu[data-enhanced].show .menu.align-tr-tr{top:0;right:0}.moodle-actionmenu[data-enhanced].show .menu.align-bl-tr{bottom:100%;left:100%}.moodle-actionmenu[data-enhanced].show .menu.align-br-tr{bottom:100%;right:0;margin-bottom:4px}.moodle-actionmenu[data-enhanced].show.nowrap-items .menu>li{white-space:nowrap}.block .moodle-actionmenu{text-align:right}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu{text-align:right;left:0;right:auto}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu a{padding:2px 28px 2px 1em}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu .iconsmall,.dir-rtl .moodle-actionmenu[data-enhanced].show .menu .smallicon{margin-right:-24px;margin-left:4px}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-tl-bl{left:auto;right:0}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-tr-bl{right:auto;left:100%}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-bl-bl{left:auto;right:0}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-br-bl{right:auto;left:100%}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-tl-br{left:auto;right:100%}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-tr-br{right:auto;left:0}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-bl-br{left:auto;right:100%}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-br-br{right:auto;left:0}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-tl-tl{left:auto;right:0}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-tr-tl{right:auto;left:100%}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-bl-tl{left:auto;right:0}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-br-tl{right:auto;left:100%}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-tl-tr{left:auto;right:100%}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-tr-tr{right:auto;left:0}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-bl-tr{left:auto;right:100%}.dir-rtl .moodle-actionmenu[data-enhanced].show .menu.align-br-tr{right:auto;left:0}.dir-rtl .block .moodle-actionmenu{text-align:right}ul.dragdrop-keyboard-drag li{list-style-type:none}.block-control-actions .moodle-core-dragdrop-draghandle img{width:12px;height:12px}a.disabled:hover,a.disabled{text-decoration:none;cursor:default;font-style:italic;color:#808080}body.lockscroll{height:100%;overflow:hidden}.dir-rtl ul{margin-left:0;margin-right:25px}.progressbar_container{max-width:500px;margin:0 auto}.ie10 .yui3-calendar-header-label{display:inline-block}dd:before,dd:after{display:block;content:" "}dd:after{clear:both}.nav-tabs>.active>a[href],.nav-tabs>.active>a[href]:hover,.nav-tabs>.active>a[href]:focus{cursor:pointer}span.inplaceeditable.inplaceeditingon{position:relative}span.inplaceeditable.inplaceeditingon span.editinstructions{margin-top:-30px;font-weight:normal;margin-right:-300px;margin-left:0}.dir-rtl span.inplaceeditable.inplaceeditingon span.editinstructions{margin-left:-300px;margin-right:0}.inplaceeditable.inplaceeditingon{position:relative}.inplaceeditable.inplaceeditingon .editinstructions{margin-top:-30px;font-weight:normal;margin-right:-300px;margin-left:0}.inplaceeditable.inplaceeditingon input{width:330px;height:16px;vertical-align:text-bottom;margin-bottom:0}.inplaceeditable.inplaceeditingon select{margin-bottom:0}.inplaceeditable .quickediticon img{opacity:.2}.inplaceeditable .quickeditlink{color:inherit;text-decoration:inherit}.inplaceeditable:hover .quickeditlink .quickediticon img,.inplaceeditable .quickeditlink:focus .quickediticon img{opacity:1}.inplaceeditable.inplaceeditable-toggle .quickediticon{display:none}.dir-rtl .inplaceeditable.inplaceeditingon .editinstructions{margin-left:-300px;margin-right:0}h3.sectionname .inplaceeditable.inplaceeditingon .editinstructions{margin-top:-20px}.formtable tbody th{font-weight:normal;text-align:right}.path-admin #assignrole{width:60%;margin-left:auto;margin-right:auto}.path-admin .admintable .leftalign{text-align:left}.dir-rtl.path-admin .admintable .leftalign{text-align:right}.environmenttable p.warn{background-color:#fcf8e3;color:#8a6d3b}.environmenttable .error,.environmenttable span.warn,.environmenttable .ok{display:inline-block;padding:2px 4px;font-size:11.844px;font-weight:bold;line-height:14px;color:#fff;vertical-align:baseline;white-space:nowrap;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.environmenttable .error:empty,.environmenttable span.warn:empty,.environmenttable .ok:empty{display:none}.environmenttable .error-important,.environmenttable span.warn-important,.environmenttable .ok-important{background-color:#b94a48}.environmenttable .error-important[href],.environmenttable span.warn-important[href],.environmenttable .ok-important[href]{background-color:#953b39}.environmenttable .error-warning,.environmenttable span.warn-warning,.environmenttable .ok-warning{background-color:#f89406}.environmenttable .error-warning[href],.environmenttable span.warn-warning[href],.environmenttable .ok-warning[href]{background-color:#c67605}.environmenttable .error-success,.environmenttable span.warn-success,.environmenttable .ok-success{background-color:#468847}.environmenttable .error-success[href],.environmenttable span.warn-success[href],.environmenttable .ok-success[href]{background-color:#356635}.environmenttable .error-info,.environmenttable span.warn-info,.environmenttable .ok-info{background-color:#3a87ad}.environmenttable .error-info[href],.environmenttable span.warn-info[href],.environmenttable .ok-info[href]{background-color:#2d6987}.environmenttable .error-inverse,.environmenttable span.warn-inverse,.environmenttable .ok-inverse{background-color:#333}.environmenttable .error-inverse[href],.environmenttable span.warn-inverse[href],.environmenttable .ok-inverse[href]{background-color:#1a1a1a}.environmenttable .error{background-color:#b94a48}.environmenttable span.warn{background-color:#f89406}.environmenttable .ok{background-color:#468847}.path-admin .admintable.environmenttable .name,.path-admin .admintable.environmenttable .info,.path-admin #assignrole .admintable .role,.path-admin #assignrole .admintable .userrole,.path-admin #assignrole .admintable .roleholder{white-space:nowrap}.path-admin .incompatibleblockstable td.c0{font-weight:bold}#page-admin-course-category .addcategory{padding:10px}#page-admin-course-index .editcourse{margin:20px auto}#page-admin-course-index .editcourse th,#page-admin-course-index .editcourse td{padding-left:10px;padding-right:10px}.timewarninghidden{display:none}.statusok,.statuswarning,.statusserious,.statuscritical{display:inline-block;padding:2px 4px;font-size:11.844px;font-weight:bold;line-height:14px;color:#fff;vertical-align:baseline;white-space:nowrap;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.statusok:empty,.statuswarning:empty,.statusserious:empty,.statuscritical:empty{display:none}.statusok-important,.statuswarning-important,.statusserious-important,.statuscritical-important{background-color:#b94a48}.statusok-important[href],.statuswarning-important[href],.statusserious-important[href],.statuscritical-important[href]{background-color:#953b39}.statusok-warning,.statuswarning-warning,.statusserious-warning,.statuscritical-warning{background-color:#f89406}.statusok-warning[href],.statuswarning-warning[href],.statusserious-warning[href],.statuscritical-warning[href]{background-color:#c67605}.statusok-success,.statuswarning-success,.statusserious-success,.statuscritical-success{background-color:#468847}.statusok-success[href],.statuswarning-success[href],.statusserious-success[href],.statuscritical-success[href]{background-color:#356635}.statusok-info,.statuswarning-info,.statusserious-info,.statuscritical-info{background-color:#3a87ad}.statusok-info[href],.statuswarning-info[href],.statusserious-info[href],.statuscritical-info[href]{background-color:#2d6987}.statusok-inverse,.statuswarning-inverse,.statusserious-inverse,.statuscritical-inverse{background-color:#333}.statusok-inverse[href],.statuswarning-inverse[href],.statusserious-inverse[href],.statuscritical-inverse[href]{background-color:#1a1a1a}.statusok{background-color:#468847}.statuswarning{background-color:#8a6d3b}.statusserious{background-color:#f89406}.statuscritical{background-color:#b94a48}#page-admin-report-capability-index #capabilitysearch{width:30em}#page-admin-report-backups-index .backup-error,#page-admin-report-backups-index .backup-unfinished{color:#b94a48}#page-admin-report-backups-index .backup-skipped,#page-admin-report-backups-index .backup-ok,#page-admin-report-backups-index .backup-notyetrun{color:#468847}#page-admin-report-backups-index .backup-warning{color:#8a6d3b}#page-admin-qtypes .disabled,#page-admin-qbehaviours .disabled{color:#999}#page-admin-qtypes #qtypes div,#page-admin-qtypes #qtypes form,#page-admin-qbehaviours #qbehaviours div,#page-admin-qbehaviours #qbehaviours form{display:inline}#page-admin-qtypes #qtypes img.spacer,#page-admin-qbehaviours #qbehaviours img.spacer{width:16px}img.iconsmall{margin:0;padding:.3em}#page-admin-qbehaviours .cell.c3,#page-admin-qtypes .cell.c3{font-size:10.5px}#page-admin-lang .generalbox,#page-admin-course-index .singlebutton,#page-admin-course-index .addcategory,#page-course-index .buttons,#page-course-index-category .buttons,#page-admin-course-category .addcategory,#page-admin-stickyblocks .generalbox,#page-admin-maintenance .buttons,#page-admin-course-index .buttons,#page-admin-course-category .buttons,#page-admin-index .copyright,#page-admin-index .copyrightnotice,#page-admin-index .adminerror .singlebutton,#page-admin-index .adminwarning .singlebutton,#page-admin-index #layout-table .singlebutton{text-align:center;margin-bottom:1em}.path-admin-roles .capabilitysearchui{text-align:left;margin-left:auto;margin-right:auto}#page-admin-roles-define .topfields{margin:1em 0 2em}#page-admin-roles-define .capdefault{background-color:#f5f5f5;border:1px solid #ddd}#page-filter-manage .backlink,.path-admin-roles .backlink{margin-top:1em}#page-admin-roles-explain #chooseuser h3,#page-admin-roles-usersroles .contextname{margin-top:0}#page-admin-roles-explain #chooseusersubmit{margin-top:0;text-align:center}#page-admin-roles-usersroles p{margin:0}#page-admin-roles-override .cell.c1,#page-admin-roles-assign .cell.c3,#page-admin-roles-assign .cell.c1{padding-top:.75em}#page-admin-roles-override .overridenotice,#page-admin-roles-define .definenotice{margin:1em 10% 2em 10%;text-align:left}#notice{width:60%;min-width:220px;margin:auto}#page-admin-index .releasenoteslink,#page-admin-index .adminwarning,#page-admin-index .adminerror{margin:auto;padding:8px 35px 8px 14px;margin-bottom:20px;text-shadow:0 1px 0 rgba(255,255,255,0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;color:#8a6d3b;width:60%;min-width:220px}#page-admin-index .adminerror{background-color:#f2dede;border-color:#eed3d7;color:#b94a48}#page-admin-index .releasenoteslink{background-color:#d9edf7;border-color:#bce8f1;color:#3a87ad}#page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo span{display:block}#page-admin-index .updateplugin div{margin-bottom:.5em}#page-admin-index .updateplugin .updatepluginconfirmexternal{padding:1em;background-color:#f2dede;border:1px solid #eed3d7}#page-admin-user-user_bulk #users .fgroup{white-space:nowrap}#page-admin-report-stats-index .graph{text-align:center;margin-bottom:1em}#page-admin-report-courseoverview-index .graph{text-align:center;margin-bottom:1em}#page-admin-lang .translator{border-width:1px;border-style:solid}.path-admin .roleassigntable{width:100%}.path-admin .roleassigntable td{vertical-align:top;padding:.2em .3em}.path-admin .roleassigntable p{text-align:left;margin:.2em 0}.path-admin .roleassigntable #existingcell,.path-admin .roleassigntable #potentialcell{width:42%}.path-admin .roleassigntable #existingcell p>label:first-child,.path-admin .roleassigntable #potentialcell p>label:first-child{font-weight:bold}.path-admin .roleassigntable #buttonscell{width:16%}.path-admin .roleassigntable #buttonscell #assignoptions{font-size:10.5px}.path-admin .roleassigntable #removeselect_wrapper,.path-admin .roleassigntable #addselect_wrapper{width:100%}.path-admin table.rolecap tr.rolecap th{text-align:left;font-weight:normal}.path-admin.dir-rtl table.rolecap tr.rolecap th{text-align:right}.path-admin .rolecap .hiddenrow{display:none}.path-admin #defineroletable .rolecap .inherit,.path-admin #defineroletable .rolecap .allow,.path-admin #defineroletable .rolecap .prevent,.path-admin #defineroletable .rolecap .prohibit{text-align:center;padding:0;min-width:3.5em}.path-admin .rolecap .cap-name,.path-admin .rolecap .note{display:block;font-size:10.5px;white-space:nowrap;font-weight:normal}.path-admin .rolecap label{display:block;text-align:center;padding:.5em;margin:0}.plugincheckwrapper{width:100%}.environmentbox{margin-top:1em}#mnetconfig table{margin-left:auto;margin-right:auto}.environmenttable .cell{padding:.15em .5em}.environmenttable img.iconhelp{padding-right:.3em}.dir-rtl .environmenttable img.iconhelp{padding-left:.3em;padding-right:0}#trustedhosts .generaltable{margin-left:auto;margin-right:auto;width:500px}#trustedhosts .standard{width:auto}#adminsettings legend{display:none}#adminsettings fieldset.error{margin:.2em 0 .5em 0}#adminsettings fieldset.error legend{display:block}.dir-rtl #admin-spelllanguagelist textarea,#page-admin-setting-editorsettingstinymce.dir-rtl .form-textarea textarea{text-align:left;direction:ltr}.adminsettingsflags{float:right}.dir-rtl .adminsettingsflags{float:left}.adminsettingsflags label{margin-right:7px}.dir-rtl .adminsettingsflags label{margin-left:7px}.form-description{clear:right}.dir-rtl .form-description{clear:left}.form-item .form-setting .form-htmlarea{width:640px;display:inline}.form-item .form-setting .form-htmlarea .htmlarea{width:640px;display:block}.form-item .form-setting .form-multicheckbox ul{list-style:none;padding:0;margin:7px 0 0 0}.form-item .form-setting .defaultsnext{margin-right:.5em;display:inline}.dir-rtl .form-item .form-setting .defaultsnext{margin-left:.5em;margin-right:0}.form-item .form-setting .locked-checkbox{margin-right:.2em;margin-left:.5em;display:inline}.dir-rtl .form-item .form-setting .locked-checkbox{margin-right:.5em;margin-left:.2em;display:inline}.form-item .form-setting .form-password .unmask,.form-item .form-setting .form-defaultinfo{display:inline-block}.form-item .pathok,.form-item .patherror{margin-left:.5em}#admin-emoticons td input{width:8em}#admin-emoticons td.c0 input{width:4em}#adminthemeselector .selectedtheme td.c0{border:1px solid #000;border-right-width:0}#adminthemeselector .selectedtheme td.c1{border:1px solid #000;border-left-width:0}.admin_colourpicker,.admin_colourpicker_preview{display:none}.jsenabled .admin_colourpicker_preview{display:inline}.jsenabled .admin_colourpicker{display:block;height:102px;width:410px;margin-bottom:10px}.admin_colourpicker .loadingicon{vertical-align:middle;margin-left:auto}.admin_colourpicker .colourdialogue{float:left;border:1px solid #000}.admin_colourpicker .previewcolour{border:1px solid #000;margin-left:301px}.admin_colourpicker .currentcolour{border:1px solid #000;margin-left:301px;border-top-width:0}.dir-rtl .form-item .form-setting,.dir-rtl .form-item .form-label,.dir-rtl .form-item .form-description,.dir-rtl.path-admin .roleassigntable p{text-align:right}#page-admin-index #notice .checkforupdates{text-align:center}#page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo.maturity200 .info.release{background-color:#d9edf7}#page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo.maturity100 .info.release,#page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo.maturity150 .info.release{background-color:#fcf8e3}#page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo.maturity50 .info.release{background-color:#f2dede}#page-admin-plugins #plugins-overview-panel .info{display:inline-block;margin-right:1em}#page-admin-plugins .checkforupdates{margin:10px 0}#page-admin-plugins .checkforupdates .singlebutton{margin:5px 0;padding:0}#page-admin-plugins .checkforupdates .singlebutton div,#page-admin-plugins .checkforupdates .singlebutton input{margin:0 3px 0 0}#page-admin-plugins .updateavailableinstallall{margin:5px 0;padding:0}#page-admin-plugins .updateavailableinstallall div,#page-admin-plugins .updateavailableinstallall input{margin:0 3px 5px 0}#page-admin-plugins #plugins-control-panel .status-missing td{background-color:#f2dede}#page-admin-plugins #plugins-control-panel .pluginname .displayname img.icon{padding-top:0;padding-bottom:0}#page-admin-plugins #plugins-control-panel .pluginname .componentname{font-size:11.9px;color:#999;margin-left:22px}#page-admin-plugins #plugins-control-panel .version .versionnumber{font-size:11.9px;color:#999}#page-admin-plugins #plugins-control-panel .uninstall a{color:#b94a48}#page-admin-plugins #plugins-control-panel .notes .label{margin-right:3px}#page-admin-plugins #plugins-control-panel .notes .requiredby{font-size:11.9px;color:#999}#plugins-check-page .page-description{color:#999}#plugins-check-page .checkforupdates .singlebutton{margin:5px 0;padding:0}#plugins-check-page .checkforupdates .singlebutton div,#plugins-check-page .checkforupdates .singlebutton input{margin:0 3px 0 0}#plugins-check-page #plugins-check-info .actions>div{display:inline-block;margin-right:1em}#plugins-check-page #plugins-check-info .actions .singlebutton{margin:5px 0;padding:0}#plugins-check-page #plugins-check-info .actions .singlebutton div,#plugins-check-page #plugins-check-info .actions .singlebutton input{margin:0 3px 0 0}#plugins-check-page #plugins-check .requires-ok{color:#999}#plugins-check-page #plugins-check .status-missing td,#plugins-check-page #plugins-check .status-downgrade td{background-color:#f2dede}#plugins-check-page #plugins-check .displayname .pluginicon{margin-right:5px;width:16px}#plugins-check-page #plugins-check .displayname .plugindir{color:#999;font-size:11.9px}#plugins-check-page #plugins-check .requires ul{margin-left:13px}#plugins-check-page #plugins-check .status .actionbutton{margin:5px 0;padding:0}#plugins-check-page #plugins-check .status .actionbutton input{margin:0}#plugins-check-page .plugins-check-dependencies-actions>div{display:inline-block;margin-right:1em}#plugins-check-page .plugins-check-dependencies-actions .singlebutton{margin:5px 0;padding:0}#plugins-check-page .plugins-check-dependencies-actions .singlebutton div,#plugins-check-page .plugins-check-dependencies-actions .singlebutton input{margin:0 3px 0 0}#plugins-check-page #plugins-check-available-dependencies .displayname .component{font-size:11.9px;color:#999}#plugins-check-page #plugins-check-available-dependencies .info .actions>div{display:inline-block;margin-right:1em}#plugins-check-page #plugins-check-available-dependencies .info .actions .dependencyinstall{display:block;margin:5px 0;padding:0}#plugins-check-page #plugins-check-available-dependencies .info .actions .dependencyinstall input{margin:0}#plugins-check-page .pluginupdateinfo,#plugins-control-panel .pluginupdateinfo{background-color:#d9edf7;padding:5px;margin:10px 0;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}#plugins-check-page .pluginupdateinfo.maturity50,#plugins-control-panel .pluginupdateinfo.maturity50{background-color:#f2dede}#plugins-check-page .pluginupdateinfo.maturity100,#plugins-control-panel .pluginupdateinfo.maturity100,#plugins-check-page .pluginupdateinfo.maturity150,#plugins-control-panel .pluginupdateinfo.maturity150{background-color:#fcf8e3}#plugins-check-page .pluginupdateinfo .info,#plugins-control-panel .pluginupdateinfo .info{display:inline-block}#plugins-check-page .pluginupdateinfo .separator:after,#plugins-control-panel .pluginupdateinfo .separator:after{content:" | "}#plugins-check-page .pluginupdateinfo .singlebutton,#plugins-control-panel .pluginupdateinfo .singlebutton{margin:5px 0;padding:0}#plugins-check-page .pluginupdateinfo .singlebutton div,#plugins-control-panel .pluginupdateinfo .singlebutton div,#plugins-check-page .pluginupdateinfo .singlebutton input,#plugins-control-panel .pluginupdateinfo .singlebutton input{margin:0 3px 0 0}.plugins-management-confirm-buttons>div{display:inline-block;margin:1em 1em 1em 0}.plugins-management-confirm-buttons .continue{padding:0}.plugins-management-confirm-buttons .continue div,.plugins-management-confirm-buttons .continue input{margin:0}.uninstalldeleteconfirmexternal{background-color:#fcf8e3;padding:.5em 1em;margin:5px 0 10px 0}#page-admin-index .upgradepluginsinfo{text-align:center}#page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo .separator:after{content:" | "}.dir-rtl #plugins-check .pluginupdateinfo{text-align:center;direction:ltr}.dir-rtl #plugins-check .requires-ok{text-align:left;direction:ltr}#page-admin-mnet-peers .box.deletedhosts{margin-bottom:1em;font-size:11.9px}#page-admin-mnet-peers .mform .deletedhostinfo{background-color:#f2dede;border:2px solid #eed3d7;padding:4px;margin-bottom:5px}#core-cache-plugin-summaries table,#core-cache-store-summaries table{width:100%}#core-cache-lock-summary table,#core-cache-definition-summaries table,#core-cache-mode-mappings table{margin:0 auto}#core-cache-store-summaries .default-store td{font-style:italic}#core-cache-rescan-definitions,#core-cache-mode-mappings .edit-link,#core-cache-lock-summary .new-instance{margin-top:.5em;text-align:center}.tinymcesubplugins img.icon{padding-top:0;padding-bottom:0}.maintenancewarning{padding:3px 1em;text-align:center;position:fixed;bottom:0;right:0;overflow:hidden;z-index:1}.maintenancewarning.error{color:#b94a48;background-color:#f2dede;border:2px solid #eed3d7;font-weight:bold}.maintenancewarning.warning{color:#8a6d3b;background-color:#fcf8e3;border:2px solid #fbeed5}#adminsettings .form-overridden{color:#3a87ad;background-color:#d9edf7}.calendar_event_course{background-color:#ffd3bd}.calendar_event_global{background-color:#d6f8cd}.calendar_event_group{background-color:#fee7ae}.calendar_event_user{background-color:#dce7ec}.path-calendar .calendartable{width:100%}.path-calendar .calendartable th,.path-calendar .calendartable td{width:14%;vertical-align:top;text-align:center;border:0}.path-calendar .calendar-controls .previous,.path-calendar .calendar-controls .next,.path-calendar .calendar-controls .current{display:block;float:left;width:12%}.path-calendar .calendar-controls .previous{text-align:left}.path-calendar .calendar-controls .current{text-align:center;width:76%}.path-calendar .calendar-controls .next{text-align:right}.path-calendar .filters table{border-collapse:separate;border-spacing:2px;width:100%}.path-calendar .cal_courses_flt{float:left}.path-calendar .cal_courses_flt label{margin-right:.45em}.path-calendar .maincalendar{vertical-align:top;padding:0}.path-calendar .maincalendar .bottom{text-align:center;padding:5px 0 0 0}.path-calendar .maincalendar .heightcontainer{height:100%;position:relative}.path-calendar .maincalendar .calendarmonth{width:98%;margin:10px auto}.path-calendar .maincalendar .calendarmonth ul{margin:0}.path-calendar .maincalendar .calendarmonth ul li{list-style-type:none;margin-top:4px}.path-calendar .maincalendar .calendarmonth td{height:5em}.path-calendar .maincalendar .calendar-controls .previous,.path-calendar .maincalendar .calendar-controls .next{width:30%}.path-calendar .maincalendar .calendar-controls .current{width:39.95%}.path-calendar .maincalendar .controls{width:98%;margin:10px auto}.path-calendar .maincalendar .calendar_event_course,.path-calendar .maincalendar .calendar_event_global,.path-calendar .maincalendar .calendar_event_group,.path-calendar .maincalendar .calendar_event_user{border-width:1px 1px 1px 12px;border-style:solid}.path-calendar .maincalendar .calendar_event_course{border-color:#ffd3bd}.path-calendar .maincalendar .calendar_event_global{border-color:#d6f8cd}.path-calendar .maincalendar .calendar_event_group{border-color:#fee7ae}.path-calendar .maincalendar .calendar_event_user{border-color:#dce7ec}.path-calendar .maincalendar .calendar-event-panel{background-color:#eee;border:2px solid #eee}.path-calendar .maincalendar .calendar-event-panel .yui3-overlay-content{padding:19px;background-color:#fdfdfd;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.path-calendar .maincalendar .calendar-controls .current{font-family:inherit;font-weight:bold;color:inherit;font-size:25px;line-height:1.2}.path-calendar .maincalendar .calendartable td,.path-calendar .maincalendar .calendartable li{padding:5px}.path-calendar .maincalendar .calendartable li{padding-left:10px;text-align:left}.path-calendar .maincalendar .header{overflow:hidden}.path-calendar .maincalendar .header .buttons{float:right}.path-calendar .maincalendar .eventlist{margin:0}.path-calendar .maincalendar .eventlist .event{width:92%;border-spacing:0;border-collapse:separate;position:relative;padding:20px 4%;margin-bottom:20px;background-color:#fdfdfd;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);list-style-type:none}.path-calendar .maincalendar .eventlist .event>img{padding-top:3px;float:left}.path-calendar .maincalendar .eventlist .event .name{font-size:17.5px;font-weight:200;line-height:24px;float:left;margin:0}.path-calendar .maincalendar .eventlist .event .name,.path-calendar .maincalendar .eventlist .event .course{margin-bottom:5px}.path-calendar .maincalendar .eventlist .event .date{float:right}.path-calendar .maincalendar .eventlist .event .course,.path-calendar .maincalendar .eventlist .event .subscription{float:left;clear:left}.path-calendar .maincalendar .eventlist .event .side{width:22px}.path-calendar .maincalendar .eventlist .event .description{background-color:#fff;padding:5px;clear:both}.path-calendar .maincalendar .eventlist .event .description .commands{position:absolute;right:0;top:0;margin:3px}.path-calendar .maincalendar .eventlist .event .commands{position:absolute;top:2px;right:2px}.path-calendar .maincalendar .eventlist .event .commands a{margin:0 3px}.dir-rtl.path-calendar .cal_courses_flt{float:right}.dir-rtl.path-calendar .cal_courses_flt label{margin-left:.45em;margin-right:0}.dir-rtl.path-calendar .maincalendar .calendar_event_course,.dir-rtl.path-calendar .maincalendar .calendar_event_global,.dir-rtl.path-calendar .maincalendar .calendar_event_group,.dir-rtl.path-calendar .maincalendar .calendar_event_user{border-left-width:1px;border-right-width:12px}.dir-rtl.path-calendar .maincalendar .calendar-controls .next{text-align:left}.dir-rtl.path-calendar .maincalendar .calendar-controls .previous{text-align:right}.dir-rtl.path-calendar .maincalendar .calendartable td,.dir-rtl.path-calendar .maincalendar .calendartable li{text-align:right}.dir-rtl.path-calendar .maincalendar .calendartable li{padding-right:10px;padding-left:5px}.dir-rtl.path-calendar .maincalendar .header .buttons{float:left}.dir-rtl.path-calendar .maincalendar .eventlist .event>img{float:right}.dir-rtl.path-calendar .maincalendar .eventlist .event .name{float:right}.dir-rtl.path-calendar .maincalendar .eventlist .event .date{float:left}.dir-rtl.path-calendar .maincalendar .eventlist .event .description .commands{right:inherit;left:0}.dir-rtl.path-calendar .maincalendar .eventlist .event .course,.dir-rtl.path-calendar .maincalendar .eventlist .event .subscription{float:right;clear:right}.dir-rtl.path-calendar .maincalendar .eventlist .event .commands{left:2px;right:inherit}#page-calendar-export .indent{padding-left:20px}.block .minicalendar{max-width:280px;margin:0 auto;width:100%}.block .minicalendar th,.block .minicalendar td{padding:2px;font-size:.8em;text-align:center}.block .minicalendar td.weekend{color:#999}.block .minicalendar td a{width:100%;height:100%;display:block}.block .minicalendar td.duration_global{border-top:1px solid #d6f8cd;border-bottom:1px solid #d6f8cd}.block .minicalendar td.duration_global.duration_finish{background-color:#d6f8cd}.block .minicalendar td.duration_course{border-top:1px solid #ffd3bd;border-bottom:1px solid #ffd3bd}.block .minicalendar td.duration_course.duration_finish{background-color:#ffd3bd}.block .minicalendar td.duration_group{border-top:1px solid #fee7ae;border-bottom:1px solid #fee7ae}.block .minicalendar td.duration_group.duration_finish{background-color:#fee7ae}.block .minicalendar td.duration_user{border-top:1px solid #dce7ec;border-bottom:1px solid #dce7ec}.block .minicalendar td.duration_user.duration_finish{background-color:#dce7ec}.block .minicalendar caption{font-size:inherit;font-weight:inherit;line-height:inherit;text-align:center}.block .calendar-event-panel{background-color:#eee;border:1px solid #eee}.block .calendar-event-panel .yui3-overlay-content{padding:19px;background-color:#fdfdfd;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.block .calendar-event-panel .yui3-overlay-content h2.eventtitle{line-height:1.2;font-size:18px}.block .calendar-event-panel .yui3-overlay-content .eventcontent img{padding-right:5px}.block .calendar-controls .previous,.block .calendar-controls .current,.block .calendar-controls .next{display:block;float:left}.block .calendar-controls .previous{text-align:left;width:12%}.block .calendar-controls .current{text-align:center;width:76%}.block .calendar-controls .next{text-align:right;width:12%}.block .calendar_filters ul{list-style:none;margin:0}.block .calendar_filters li{margin-bottom:.2em}.block .calendar_filters li span img{padding:0 .2em}.block .calendar_filters .eventname{padding-left:.2em}.block .content h3.eventskey{margin-top:.5em}.dir-rtl .block .calendar_filters .eventname{padding-right:.2em;padding-left:0}.dir-rtl .block .calendar-event-panel .yui3-overlay-content .eventcontent img{padding-right:0;padding-left:5px}.ical-link{font-size:10px;font-weight:bold;background-color:#f60;padding:0 5px;color:#fff;border-top:1px solid #f93;border-left:1px solid #f93;border-bottom:1px solid #013;border-right:1px solid #013}.ical-link:hover,.ical-link:active,.ical-link:focus,.ical-link:visited{color:#fff;text-decoration:none}@media (min-width:768px){#page-calender-view .container-fluid{min-width:1024px}}.section_add_menus{text-align:right;clear:both}.section-modchooser{clear:both}.dir-rtl .section_add_menus{text-align:left;clear:both}.section_add_menus .horizontal div,.section_add_menus .horizontal form{display:inline}.section_add_menus optgroup{font-weight:normal;font-style:italic}.section_add_menus .urlselect{margin-left:.4em}.dir-rtl .section_add_menus .urlselect{margin-right:.4em;margin-left:0}.section_add_menus .urlselect select{margin-left:.2em}.dir-rtl .section_add_menus .urlselect select{margin-right:.2em;margin-left:0}.section_add_menus .urlselect img.iconhelp{padding:0;margin:0;vertical-align:text-bottom}.sitetopic ul.section{margin:0}.course-content ul.section{margin:1em}.section .side.left{float:left}.section .side.right{float:right}.section .spinner{height:16px;width:16px}.section .activity .spinner{left:100%;position:absolute;vertical-align:text-bottom}.section .activity .editing_move{position:absolute;left:0;top:0}.section .activity .mod-indent-outer{padding-left:32px}.section .activity .actions{position:absolute;right:0;top:0}.section .activity .contentwithoutlink,.section .activity .activityinstance{min-width:40%;display:table-cell;padding-right:4px;min-height:2em}.section .activity .contentwithoutlink .dimmed img.activityicon,.section .activity .activityinstance .dimmed img.activityicon{opacity:.5;filter:alpha(opacity=50)}.section .label .contentwithoutlink,.section .label .activityinstance{padding-right:32px;display:block;height:inherit}.section .label .mod-indent-outer{padding-left:24px;display:block}.section .filler{width:16px;height:16px;padding:.3em;display:inline-block}.section .activity.editor_displayed a.editing_title,.section .activity.editor_displayed .moodle-actionmenu{display:none}.section .activity.editor_displayed div.activityinstance{padding-right:initial}.section .activity.editor_displayed div.activityinstance input{margin-bottom:initial;padding-top:initial;padding-bottom:initial;vertical-align:text-bottom}.dir-rtl .section .side.left{float:right}.dir-rtl .section .side.right{float:left}.dir-rtl .section .activity .spinner{left:auto;right:100%}.dir-rtl .section .activity .mod-indent-outer{padding-left:initial;padding-right:32px}.dir-rtl .section .activity .actions{left:0;right:auto}.dir-rtl .section .activity .contentwithoutlink,.dir-rtl .section .activity .activityinstance{padding-left:4px;padding-right:initial}.dir-rtl .section .activity .editing_move{left:auto;right:0}.dir-rtl .section .activity.editor_displayed div.activityinstance{padding-left:initial}.activity img.activityicon{margin-right:6px;vertical-align:text-bottom}.dir-rtl .section .activity img.activityicon{margin-left:6px;margin-right:0}.section .activity .activityinstance,.section .activity .activityinstance div{display:inline-block}.editing .section .activity .contentwithoutlink,.editing .section .activity .activityinstance{padding-right:200px}.dir-rtl.editing .section .activity .contentwithoutlink,.dir-rtl.editing .section .activity .activityinstance{padding-left:200px;padding-right:0}.editing_show+.editing_assign,.editing_hide+.editing_assign{margin-left:20px}.section .activity .commands{white-space:nowrap;display:inline}.section .activity.modtype_label.label{font-weight:normal;padding:.2em}.section li.activity{padding:.2em;clear:both}.section .activity .activityinstance .groupinglabel{padding-left:30px}.dir-rtl .section .activity .activityinstance .groupinglabel{padding-right:30px}.section .activity .availabilityinfo,.section .activity .contentafterlink{margin-top:.5em;margin-left:30px}.dir-rtl .section .activity .availabilityinfo,.dir-rtl .section .activity .contentafterlink{margin-left:0;margin-right:30px}.section .activity .contentafterlink p{margin:.5em 0}.editing .section .activity:hover,.editing .section .activity.action-menu-shown{background-color:#eee}.course-content .current{background-color:#d9edf7}.course-content .section-summary{border:1px solid #ddd;margin-top:5px;list-style:none}.course-content .section-summary .section-title{margin:2px 5px 10px 5px}.course-content .section-summary .summarytext{margin:2px 5px 2px 5px}.course-content .section-summary .section-summary-activities .activity-count{color:#999;font-size:11.9px;margin:3px;white-space:nowrap;display:inline-block}.course-content .section-summary .summary{margin-top:5px}.course-content .single-section{margin-top:1em}.course-content .single-section .section-navigation{display:block;padding:.5em;margin-bottom:-0.5em}.course-content .single-section .section-navigation .title{font-weight:bold;font-size:108%;clear:both}.course-content .single-section .section-navigation .mdl-left{font-weight:normal;float:left;margin-right:1em}.dir-rtl .course-content .single-section .section-navigation .mdl-left{float:right}.course-content .single-section .section-navigation .mdl-left .larrow{margin-right:.1em}.course-content .single-section .section-navigation .mdl-right{font-weight:normal;float:right;margin-left:1em}.dir-rtl .course-content .single-section .section-navigation .mdl-right{float:left}.course-content .single-section .section-navigation .mdl-right .rarrow{margin-left:.1em}.course-content .single-section .section-navigation .mdl-bottom{margin-top:0}.course-content ul li.section.main{border-bottom:2px solid #ddd;margin-top:0}.course-content ul li.section.hidden .sectionname>span,.course-content ul li.section.hidden .content>div,.course-content ul li.section.hidden .activity .activityinstance{opacity:.5}.course-content ul li.section.hidden .sectionname>span,.course-content ul li.section.hidden .activity .activityinstance{margin-left:10px;margin-right:10px}.course-content ul.topics li.section .content,.course-content ul.weeks li.section .content{margin-right:20px;margin-left:20px;padding:0}.course-content{margin-top:0}.course-content ul.topics li.section{padding-bottom:20px}.course-content ul.topics li.section .summary{margin-left:25px}.course-content li.section ul{list-style:disc}.course-content li.section ul ul{list-style:circle}.course-content li.section ul ul ul{list-style:square}.course-content li.section li.activity ul{list-style:disc}.course-content li.section li.activity ul ul{list-style:circle}.course-content li.section li.activity ul ul ul{list-style:square}.path-course-view .completionprogress{margin-left:25px}.path-course-view .completionprogress{display:block;float:right;height:20px;position:relative}#page-site-index .subscribelink{text-align:right}#site-news-forum h2,#frontpage-course-list h2,#frontpage-category-names h2,#frontpage-category-combo h2{margin-bottom:9px}.path-course-view a.reduce-sections{padding-left:.2em}.path-course-view .subscribelink{text-align:right}.path-course-view .unread{margin-left:30px}.dir-rtl.path-course-view .unread{margin-right:30px}.path-course-view .block.drag .header{cursor:move}.path-course-view .completionprogress{text-align:right}.dir-rtl.path-course-view .completionprogress{text-align:left}.path-course-view .single-section .completionprogress{margin-right:5px}.path-course-view .section .summary{line-height:normal}.path-site li.activity>div,.path-course-view li.activity>div{position:relative;padding:0 16px 0 0}.dir-rtl.path-site li.activity>div,.dir-rtl.path-course-view li.activity>div{position:relative;padding:0 0 0 16px}.path-course-view li.activity span.autocompletion img{vertical-align:text-bottom}.path-course-view li.activity form.togglecompletion img{max-width:none}.path-course-view li.activity form.togglecompletion .ajaxworking{width:16px;height:16px;position:absolute;right:22px;top:3px;background:url([[pix:i/ajaxloader]]) no-repeat}.dir-rtl.path-course-view .completionprogress{float:none}.dir-rtl.path-course-view li.activity form.togglecompletion .ajaxworking{right:-22px}li.section.hidden span.commands a.editing_hide,li.section.hidden span.commands a.editing_show{cursor:default}ul.weeks h3.sectionname{white-space:nowrap}.editing ul.weeks h3.sectionname{white-space:normal}.single-section h3.sectionname{text-align:center;clear:both}.section img.movetarget{height:16px;width:80px}input.titleeditor{width:330px;vertical-align:text-bottom}span.editinstructions{position:absolute;top:0;margin-top:-22px;margin-left:30px;line-height:16px;font-size:11.9px;padding:.1em .4em;background-color:#d9edf7;color:#3a87ad;text-decoration:none;z-index:9999;-webkit-box-shadow:2px 2px 5px 1px #ccc;-moz-box-shadow:2px 2px 5px 1px #ccc;box-shadow:2px 2px 5px 1px #ccc;border:1px solid #bce8f1}#dndupload-status{position:fixed;left:0;width:40%;margin:0 30%;padding:6px;border:1px solid #bce8f1;text-align:center;background:#d9edf7;color:#3a87ad;z-index:1;-webkit-box-shadow:2px 2px 5px 1px #ccc;-moz-box-shadow:2px 2px 5px 1px #ccc;box-shadow:2px 2px 5px 1px #ccc;-webkit-border-radius:8px;-moz-border-radius:8px;border-radius:8px}.dndupload-preview{color:#909090;border:1px dashed #909090;list-style:none;margin-top:.2em;padding:.3em}.dndupload-preview img.icon{vertical-align:text-bottom;padding:0}.dndupload-progress-outer{overflow:hidden;height:20px;margin-bottom:20px;background-color:#f7f7f7;background-image:-moz-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9));background-image:-webkit-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-o-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:linear-gradient(to bottom, #f5f5f5, #f9f9f9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.dndupload-progress-inner{width:0;height:100%;color:#fff;float:left;font-size:12px;text-align:center;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top, #149bdf, #0480be);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be));background-image:-webkit-linear-gradient(top, #149bdf, #0480be);background-image:-o-linear-gradient(top, #149bdf, #0480be);background-image:linear-gradient(to bottom, #149bdf, #0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width .6s ease;-moz-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.dndupload-hidden{display:none}#page-course-pending .singlebutton,#page-course-index .singlebutton,#page-course-index-category .singlebutton,#page-course-editsection .singlebutton{text-align:center}#page-admin-course-manage #movecourses td img{margin:0 .22em;vertical-align:text-bottom}#page-admin-course-manage #movecourses td img.icon{padding:0}#coursesearch{margin-top:1em;text-align:center}#page-course-pending .pendingcourserequests{margin-bottom:1em}#page-course-pending .pendingcourserequests .singlebutton{display:inline}#page-course-pending .pendingcourserequests .cell{padding:0 5px}#page-course-pending .pendingcourserequests .cell.c6{white-space:nowrap}.coursebox{margin-bottom:15px;border:1px dotted #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;padding:5px}.coursebox>.info>.coursename a{display:block;background-image:url([[pix:moodle|i/course]]);background-repeat:no-repeat;padding-left:21px;background-position:left .2em}.dir-rtl .coursebox>.info>.coursename a{padding-left:0;padding-right:21px;background-position:right .2em}.coursebox>.info>.coursename,.coursebox .content .teachers,.coursebox .content .courseimage,.coursebox .content .coursefile{float:left;clear:left}.coursebox .content .teachers,.coursebox .content .courseimage,.coursebox .content .coursefile{width:40%}.dir-rtl .coursebox>.info>.coursename,.dir-rtl .coursebox .teachers,.dir-rtl .coursebox .content .courseimage,.dir-rtl .coursebox .content .coursefile{float:right;clear:right}.coursebox>.info>h3.coursename{margin:5px;line-height:1}.coursebox>.info>.coursename{margin:5px;padding:0}.coursebox .content .teachers li{list-style-type:none;padding:0;margin:0}.coursebox .enrolmenticons{padding:3px 0;float:right}.coursebox .moreinfo{padding:3px 0;float:right}.coursebox .enrolmenticons img,.coursebox .moreinfo img{margin:0 .2em}.coursebox .content{clear:both}.coursebox .content .summary,.coursebox .content .coursecat{float:right;width:55%}.coursebox .content .coursecat{text-align:right;clear:right}.coursebox.remotecoursebox .remotecourseinfo{float:left;width:40%}.coursebox .content .courseimage img{max-width:100px;max-height:100px}.coursebox .content .coursecat,.coursebox .content .summary,.coursebox .content .courseimage,.coursebox .content .coursefile,.coursebox .content .teachers,.coursebox.remotecoursebox .remotecourseinfo{margin:3px 5px;padding:0}.coursebox.remotehost>.info>.categoryname a{background-image:url([[pix:moodle|i/mnethost]])}.dir-rtl .coursebox>.info>.categoryname a{padding-left:0;padding-right:21px;background-position:center right}.dir-rtl .coursebox>.info>.categoryname,.dir-rtl .coursebox .teachers,.dir-rtl .coursebox .content .courseimage,.dir-rtl .coursebox .content .coursefile{float:right;clear:right}.dir-rtl .coursebox .enrolmenticons,.dir-rtl .coursebox .moreinfo{float:left}.dir-rtl .coursebox .summary,.dir-rtl .coursebox .coursecat{float:left}.dir-rtl .coursebox .coursecat{text-align:left;clear:left}.coursebox.collapsed{margin-bottom:0}.coursebox.collapsed>.content{display:none}.courses .coursebox.collapsed{border:1px solid #ddd;padding:5px}.courses .coursebox.even{background-color:#f9f9f9}.courses .coursebox:hover,.course_category_tree .courses>.paging.paging-morelink:hover{background-color:#f5f5f5}.course_category_tree .category .numberofcourse{font-size:11.9px}.course_category_tree .controls{visibility:hidden}.course_category_tree .controls div{display:inline;cursor:pointer}.jsenabled .course_category_tree .controls{visibility:visible}.course_category_tree .controls{margin-bottom:5px;text-align:right;float:right}.course_category_tree .controls div{padding-right:2em;font-size:75%}.course_category_tree .category>.info>.categoryname{background-image:url([[pix:moodle|t/collapsed_empty]]);background-repeat:no-repeat;padding:2px 18px;margin:3px;background-position:center left}.dir-rtl .course_category_tree .category>.info>.categoryname{background-image:url([[pix:moodle|t/collapsed_empty_rtl]]);background-position:center right}.course_category_tree .category.with_children>.info>.categoryname{background-image:url([[pix:moodle|t/expanded]]);cursor:pointer}.course_category_tree .category.with_children.collapsed>.info>.categoryname{background-image:url([[pix:moodle|t/collapsed]])}.dir-rtl .course_category_tree .category.with_children.collapsed>.info>.categoryname{background-image:url([[pix:moodle|t/collapsed_rtl]])}.course_category_tree .category.collapsed>.content{display:none}.course_category_tree .category>.info{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);border-color:#e3e3e3;min-height:0;padding:0;margin:3px 0;margin-bottom:3px;clear:both}.course_category_tree .category>.info blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.course_category_tree.frontpage-category-names .category>.info{background:none;border:none;margin:0}.course_category_tree .category>.content{padding-left:16px}.dir-rtl .course_category_tree .category>.content{padding-left:0;padding-right:16px}.course_category_tree .subcategories>.paging,.courses>.paging{margin:0;padding:5px;text-align:center}.courses>.paging.paging-morelink,.course_category_tree .subcategories>.paging.paging-morelink{text-align:left}.course_category_tree .paging.paging-morelink a{font-size:11.9px}.dir-rtl .courses>.paging.paging-morelink,.dir-rtl .course_category_tree .paging.paging-morelink{text-align:right}#page-course-index-category .generalbox.info{margin-bottom:15px;border:1px dotted #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;padding:5px}#page-course-index-category .categorypicker{text-align:center;margin:10px 0 20px}.section .summary .iconsmall,.section .activity .iconsmall{width:16px;height:16px}.section .editing_title .iconsmall{width:12px;height:12px;margin:8px 8px 0 0;padding:4px 8px 0 0;vertical-align:text-bottom}.section .moodle-actionmenu .iconsmall{max-width:none !important;width:16px;height:16px;padding:4px;vertical-align:text-bottom}.section .moodle-actionmenu[data-enhanced] .menu img{width:12px;height:12px}.dir-rtl .section .editing_title .iconsmall{margin:8px 0 0 8px;padding:4px 0 0 8px}#course-category-listings{background-color:transparent;margin-bottom:200px}#course-category-listings.columns-2>#course-listing>div{position:relative;left:-1px}#course-category-listings.columns-3>#course-listing>div{height:100%}#course-category-listings>div>div{min-height:300px}#course-category-listings>div>div>ul.ml>li:first-child>div{border-top:0}#course-category-listings h3{margin:0;padding:.4rem .6rem .3rem}#course-category-listings h4{margin:1rem 0 0;padding:.6rem 1rem .5rem}#course-category-listings .moodle-actionmenu{white-space:nowrap}#course-category-listings .moodle-actionmenu[data-enhance] .toggle-display img{width:auto}#course-category-listings .moodle-actionmenu[data-enhance] .toggle-display.textmenu{padding-right:4px}#course-category-listings .moodle-actionmenu[data-enhance] .toggle-display.textmenu .caret{margin-top:12px}#course-category-listings .listing-actions{text-align:center;padding:.4rem .3rem .3rem;line-height:2.2em}#course-category-listings .listing-actions>a,#course-category-listings .listing-actions>.moodle-actionmenu{display:inline-block}#course-category-listings .listing-actions>.moodle-actionmenu .menu a{padding-left:1rem}#course-category-listings .listing-actions .moodle-actionmenu:not([data-enhanced]) li{line-height:normal}#course-category-listings .listing-actions .moodle-actionmenu:not([data-enhanced])>.menubar a{color:inherit;display:inline-block}#course-category-listings .listing-actions .moodle-actionmenu:not([data-enhanced])>.menubar a>img{display:none}#course-category-listings .listing-actions .moodle-actionmenu:not([data-enhanced])>.menubar a .caret{display:none}#course-category-listings .listing-actions .moodle-actionmenu:not([data-enhanced])>.menu .menu-action-text{display:inline-block}#course-category-listings ul.ml{list-style:none;margin:1rem 0}#course-category-listings ul.ml ul.ml{margin:0}#course-category-listings li{line-height:2.2em}#course-category-listings li>div:hover{background-color:#f5f5f5}#course-category-listings li .tree-icon{margin:2px 6px 0 0;width:12px;vertical-align:inherit}#course-category-listings li[data-selected='1']>div{background-color:#f9f9f9}#course-category-listings li[data-selected='1']>div:hover{background-color:#f5f5f5}#course-category-listings li .tree-icon{margin-left:0}#course-category-listings li li .tree-icon{margin-left:1em}#course-category-listings li li li .tree-icon{margin-left:2em}#course-category-listings li li li li .tree-icon{margin-left:3em}#course-category-listings li li li li li .tree-icon{margin-left:4em}#course-category-listings li li li li li li .tree-icon{margin-left:4.5em}#course-category-listings li li li li li li li .tree-icon{margin-left:5em}#course-category-listings li li li li li li li li .tree-icon{margin-left:5.5em}#course-category-listings .item-actions{margin-right:1em;display:inline-block;display:initial}#course-category-listings .item-actions>a img,#course-category-listings .item-actions .menubar img{margin:0 4px;height:12px;padding:0;vertical-align:inherit}#course-category-listings .item-actions.show .menu li{line-height:20px}#course-category-listings .item-actions.show .menu img{width:12px;max-width:none}#course-category-listings .item-actions .menu-action-text{vertical-align:inherit}#course-category-listings .listitem>div>.float-left{float:left}#course-category-listings .listitem>div>.float-right{float:right;text-align:right}#course-category-listings .listitem>div .item-actions .action-show{display:none}#course-category-listings .listitem>div .item-actions .action-hide{display:inline}#course-category-listings .listitem>div .without-actions{color:#333}#course-category-listings .listitem>div .idnumber{color:#a1a1a8;margin-right:2em}#course-category-listings .listitem[data-visible="0"]{color:#999}#course-category-listings .listitem[data-visible="0"]>div>a{color:#999}#course-category-listings .listitem[data-visible="0"]>div .item-actions .action-show{display:inline}#course-category-listings .listitem[data-visible="0"]>div .item-actions .action-hide{display:none}#course-category-listings .listitem.highlight{background-color:transparent}#course-category-listings .listitem.highlight>div,#course-category-listings .listitem.highlight>div:hover,#course-category-listings .listitem.highlight[data-selected='1']>div{background-color:#f5f5f5}#course-category-listings #course-listing .listitem .categoryname{display:inline-block;margin-left:1em;color:#a1a1a8}#course-category-listings #course-listing .listitem .coursename{display:inline-block}#course-category-listings #course-listing .listitem>div{padding-left:1rem}#course-category-listings #course-listing>.firstpage .listitem:first-child>div .item-actions .action-moveup,#course-category-listings #course-listing>.lastpage .listitem:last-child>div .item-actions .action-movedown{display:none}#course-category-listings #course-listing .bulk-action-checkbox{margin:-2px 6px 0 0}#course-category-listings #category-listing .listitem.collapsed>ul.ml{display:none}#course-category-listings #category-listing .listitem>div>.ba-checkbox{width:2.2em;text-align:center;margin:-1px .5em 0 0;padding-top:2px}#course-category-listings #category-listing .listitem.highlight>div>.ba-checkbox{background-color:#f5f5f5}#course-category-listings #category-listing .listitem[data-selected='1']>div>.ba-checkbox{margin:0 .5em 0 0;padding:0;background-color:inherit}#course-category-listings #category-listing .listitem:first-child>div .item-actions .action-moveup,#course-category-listings #category-listing .listitem:last-child>div .item-actions .action-movedown{display:none}#course-category-listings #category-listing .course-count{color:#a1a1a8;margin-right:2rem;min-width:3.5em;display:inline-block}#course-category-listings #category-listing .course-count .smallicon{width:12px;margin-left:4px;vertical-align:inherit}#course-category-listings #category-listing .bulk-action-checkbox{margin-right:-3px}#course-category-listings #category-listing .category-listing>ul>.listitem:first-child{position:relative}#course-category-listings #category-listing .category-bulk-actions{margin:0 .5em .5em;position:relative}#course-category-listings .detail-pair{border-bottom:1px solid #ddd;margin:0 1rem}#course-category-listings .detail-pair>*{display:inline-block;line-height:2.2rem}#course-category-listings .detail-pair .pair-key{font-weight:bold;vertical-align:top}#course-category-listings .detail-pair .pair-key span{margin-right:1rem;display:block}#course-category-listings .detail-pair .pair-value select{max-width:100%}#course-category-listings .bulk-actions .detail-pair>*{display:block;width:100%}#course-category-listings .listing-pagination{text-align:center}#course-category-listings .listing-pagination .yui3-button{background-color:#fff;border:0;margin:.4rem .2rem .45rem;font-size:10.4px}#course-category-listings .listing-pagination .yui3-button.active-page{background-color:#e6e6e6}#course-category-listings .listing-pagination-totals{text-align:center}#course-category-listings .listing-pagination-totals.dimmed{color:#999;margin:.4rem 1rem .45rem}#course-category-listings .select-a-category .notifymessage,#course-category-listings .select-a-category .alert{margin:1em}#course-category-listings #course-listing .listitem .drag-handle{display:none}.jsenabled #course-category-listings #course-listing .listitem .drag-handle{display:inline-block;margin:0 6px 0 0;cursor:pointer}.dir-rtl #course-category-listings #category-listing,.dir-rtl #course-category-listings #course-listing{float:right;margin-left:0}.dir-rtl #course-category-listings .listitem>div>.float-left{float:right}.dir-rtl #course-category-listings .listitem>div>.float-right{float:left;text-align:left}.dir-rtl #course-category-listings li .tree-icon{margin:2px 0 0 6px}.dir-rtl #course-category-listings li .tree-icon{margin-right:0}.dir-rtl #course-category-listings li li .tree-icon{margin-right:1em}.dir-rtl #course-category-listings li li li .tree-icon{margin-right:2em}.dir-rtl #course-category-listings li li li li .tree-icon{margin-right:3em}.dir-rtl #course-category-listings li li li li li .tree-icon{margin-right:4em}.dir-rtl #course-category-listings li li li li li li .tree-icon{margin-right:4.5em}.dir-rtl #course-category-listings li li li li li li li .tree-icon{margin-right:5em}.dir-rtl #course-category-listings li li li li li li li li .tree-icon{margin-right:5.5em}.dir-rtl #course-category-listings #category-listing .listitem>div{margin-right:.5em;margin-left:0}.dir-rtl #course-category-listings #category-listing .listitem>div>.ba-checkbox{margin:-1px 0 0 .5em}.dir-rtl #course-category-listings #category-listing .listitem[data-selected='1']>div>.ba-checkbox{margin:0 0 0 .5em}.dir-rtl #course-category-listings #category-listing .course-count{margin-left:2rem}.dir-rtl #course-category-listings #category-listing .course-count .smallicon{margin-left:0;margin-right:4px}.dir-rtl #course-category-listings #category-listing .bulk-action-checkbox{margin-left:-3px;margin-right:0}.dir-rtl #course-category-listings #course-listing{padding-right:24px}.dir-rtl #course-category-listings #course-listing .listitem .idnumber{color:#a1a1a8;padding-right:2em}.dir-rtl #course-category-listings #course-listing .listitem .categoryname{display:inline-block;margin-right:1em;margin-left:0}.dir-rtl #course-category-listings #course-listing .listitem .drag-handle{margin:0 6px 0 6px}.dir-rtl #course-category-listings #course-listing .listitem>div{padding-left:1rem}.dir-rtl #course-category-listings #course-listing .bulk-action-checkbox{vertical-align:middle;margin:-2px 0 0 6px}.dir-rtl #course-category-listings .detail-pair>*{float:right;margin-right:0}.dir-rtl #course-category-listings .detail-pair .pair-key span{margin-right:0;margin-left:0}.dir-rtl #course-category-listings .detail-pair .pair-value{margin-right:.5em}.coursecat-management-header{vertical-align:middle}.coursecat-management-header h2{display:inline-block;text-align:left}.coursecat-management-header>div{display:inline-block;float:right;line-height:40px}.coursecat-management-header>div>div{margin-left:1em;margin:10px 0;display:inline-block}.coursecat-management-header select{max-width:300px;cursor:pointer;padding:.4em .5em .45em 1em;vertical-align:baseline;white-space:nowrap}.coursecat-management-header .view-mode-selector .moodle-actionmenu{white-space:nowrap;display:inline-block}.coursecat-management-header .view-mode-selector .moodle-actionmenu[data-enhanced].show .menu a{padding-left:1em}.dir-rtl .coursecat-management-header h2{text-align:right}.dir-rtl .coursecat-management-header>div{float:left;margin-right:1em;margin-left:0}.course-being-dragged-proxy{border:0;color:#0070a8;vertical-align:middle;padding:0 0 0 4em}.course-being-dragged{opacity:.5;filter:alpha(opacity=50)}@media (min-width:1200px) and (max-width:1600px){#course-category-listings.columns-3{background-color:transparent;border:0}#course-category-listings.columns-3 #category-listing,#course-category-listings.columns-3 #course-listing{width:50%}#course-category-listings.columns-3 #category-listing>div,#course-category-listings.columns-3 #course-listing>div,#course-category-listings.columns-3 #course-detail>div{background-color:transparent}#course-category-listings.columns-3 #course-detail{width:100%;margin-top:1em}}@media (max-width:1199px){#course-category-listings.columns-2,#course-category-listings.columns-3{background-color:transparent;border:0}#course-category-listings.columns-2 #category-listing,#course-category-listings.columns-3 #category-listing,#course-category-listings.columns-2 #course-listing,#course-category-listings.columns-3 #course-listing,#course-category-listings.columns-2 #course-detail,#course-category-listings.columns-3 #course-detail{width:100%;margin:0 0 1em}#course-category-listings.columns-2 #category-listing>div,#course-category-listings.columns-3 #category-listing>div,#course-category-listings.columns-2 #course-listing>div,#course-category-listings.columns-3 #course-listing>div,#course-category-listings.columns-2 #course-detail>div,#course-category-listings.columns-3 #course-detail>div{background-color:transparent}}.filemanager,.filepicker,.file-picker{font-size:11px}.filemanager a,.file-picker a,.filemanager a:hover,.file-picker a:hover{color:#555555;text-decoration:none}.filemanager input[type="text"],.file-picker input[type="text"]{width:265px}.filemanager .fp-license td,.file-picker .fp-setlicense td{max-width:265px}.filemanager .fp-license select,.file-picker .fp-setlicense select{max-width:100%}.fp-content-center{height:100%;width:100%;display:table-cell;vertical-align:middle}.fp-content-hidden{visibility:hidden}.yui3-panel-focused{outline:none}#filesskin .yui3-panel-content{padding-bottom:20px;background:#F2F2F2;-webkit-border-radius:8px;-moz-border-radius:8px;border-radius:8px;border:1px solid #fff;display:inline-block;*display:inline;*zoom:1;-webkit-box-shadow:5px 5px 20px 0 #666;-moz-box-shadow:5px 5px 20px 0 #666;box-shadow:5px 5px 20px 0 #666}#filesskin .yui3-widget-hd{-webkit-border-radius:10px 10px 0 0;-moz-border-radius:10px 10px 0 0;border-radius:10px 10px 0 0;border-bottom:1px solid #BBBBBB;padding:5px;text-align:center;font-size:12px;color:#333;letter-spacing:1px;text-shadow:1px 1px 1px #fff;filter:dropshadow(color=#FFFFFF, offx=1, offy=1);background-color:#ebebeb;background-image:-moz-linear-gradient(top, #fff, #ccc);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#ccc));background-image:-webkit-linear-gradient(top, #fff, #ccc);background-image:-o-linear-gradient(top, #fff, #ccc);background-image:linear-gradient(to bottom, #fff, #ccc);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffcccccc', GradientType=0)}.fp-panel-button{background:#fff;padding:3px 20px 2px 20px;text-align:center;margin:10px;-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;display:inline-block;*display:inline;*zoom:1;-webkit-box-shadow:2px 2px 3px .1px #999;-moz-box-shadow:2px 2px 3px .1px #999;box-shadow:2px 2px 3px .1px #999}.moodle-dialogue h3{font-size:14px;margin:0;line-height:20px}.moodle-dialogue-base .filepicker .moodle-dialogue-wrap .moodle-dialogue-bd{padding:0}#filesskin .file-picker.fp-generallayout{width:859px;background:#FFFFFF;-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;border:1px solid #CCCCCC;position:relative}.file-picker .fp-repo-area{width:180px;overflow:auto;display:inline-block;*display:inline;*zoom:1;float:left;height:525px;border-right:1px solid #BBBBBB}.dir-rtl .file-picker .fp-repo-area{border-left:1px solid #BBBBBB;border-right:none;float:right}.file-picker .fp-repo-items{float:none;width:auto;margin-left:181px}.moodle-dialogue-fullscreen .file-picker .fp-repo-items{margin-left:0;margin-right:0;float:left}.dir-rtl .file-picker .fp-repo-items{margin-left:0;margin-right:181px}.dir-rtl .moodle-dialogue-fullscreen .file-picker .fp-repo-items{margin-left:0;margin-right:0;float:right}.file-picker .fp-navbar{background:#F2F2F2;border-bottom:1px solid #BBBBBB;min-height:40px;overflow:hidden}.file-picker .fp-navbar .fp-viewbar{margin:4px}.file-picker .fp-content{background:#FFFFFF;clear:none;overflow:auto;height:452px}.filepicker.moodle-dialogue-fullscreen .file-picker .fp-content{width:100%}.file-picker .fp-content-loading{height:100%;width:100%;display:table;text-align:center}.file-picker .fp-content .fp-object-container{width:98%;height:98%}.dir-rtl .file-picker .fp-list{text-align:right}.dir-rtl .file-picker .fp-toolbar{padding:4px}.dir-rtl .file-picker .fp-list{text-align:right}.dir-rtl .file-picker .fp-repo-name{display:inline}.dir-rtl .file-picker .fp-pathbar{text-align:right;display:block;border-top:none}.dir-rtl .file-picker div.bd{text-align:right}.dir-rtl #filemenu .yuimenuitemlabel{text-align:right}.dir-rtl .filepicker .yui-layout-unit-left{left:500px}.dir-rtl .filepicker .yui-layout-unit-center{left:0}.dir-rtl .filemanager-toolbar a{padding:0}.file-picker .fp-list{list-style-type:none;padding:0;float:left;width:100%;margin:0}.dir-rtl .file-picker .fp-list{text-align:right;float:left}.file-picker .fp-list .fp-repo a{display:block;padding:.5em .7em}.file-picker .fp-list .fp-repo.active{background:#F2F2F2}.file-picker .fp-list .fp-repo-icon{padding:0 7px 0 5px;width:16px;height:16px}.fp-toolbar{float:left}.dir-rtl .fp-toolbar{float:right}.fp-toolbar.empty{display:none}.dir-rtl .fp-toolbar div.disabled,.fp-toolbar .disabled{display:none}.fp-toolbar div{display:block;float:left;margin-right:4px}.dir-rtl .fp-toolbar div{display:block;float:right;margin-left:4px;margin-right:0}.fp-toolbar img{vertical-align:-15%;margin-right:5px}.fp-toolbar .fp-tb-search{width:235px;height:27px}.fp-toolbar .fp-tb-search input{background:#FFFFFF url('[[pix:a/search]]') no-repeat 7px 7px;padding:2px 6px 1px 27px;width:200px;height:27px;border:1px solid #BBBBBB}.fp-viewbar{float:right;height:30px;border:1px solid #CCC;border-bottom:1px solid #B3B3B3;border-radius:4px;background:white}.fp-repo-items fp-viewbar{margin:4px}.dir-rtl .fp-toolbar img{vertical-align:-35%}.dir-rtl .fp-viewbar{float:left}.fp-viewbar a{width:30px;height:30px;border-right:1px solid #CCC;display:block;float:left}.fp-viewbar a.checked:hover,.fp-viewbar a:hover{background-image:radial-gradient(ellipse at center, #ffffff 60%, #dfdfdf 100%);background-color:#ebebeb}.fp-viewbar a.checked,.fp-viewbar a:active{background-image:radial-gradient(ellipse at center, #ffffff 40%, #dfdfdf 100%);background-color:#dfdfdf}.fp-viewbar a.fp-vb-icons{border-radius:4px 0 0 4px}.fp-viewbar a.fp-vb-tree{border-right:0;border-radius:0 4px 4px 0}.fp-viewbar a img{margin:7px}.fp-viewbar.disabled a{opacity:.45;background:none;cursor:default}.file-picker .fp-clear-left{clear:left}.dir-rtl .fp-vb-details a:hover{background:none;border:20px solid black}.dir-rtl .fp-vb-details.checked a:hover{background:none;border:40px solid black}.dir-rtl .fp-vb-tree a:hover{background:none;border:30px solid black}.dir-rtl .fp-vb-tree.checked a:hover{background:none;border:50px solid black}.file-picker .fp-pathbar{display:table-row}.fp-pathbar.empty{display:none}.fp-pathbar .fp-path-folder{background:url('[[pix:theme|fp/path_folder]]') no-repeat 0 0;width:27px;height:12px;margin-left:4px}.dir-rtl .fp-pathbar .fp-path-folder{background:url('[[pix:theme|fp/path_folder_rtl]]') no-repeat right top;width:auto;height:12px;margin-left:4px}.dir-rtl .fp-pathbar span{display:inline-block;*display:inline;*zoom:1;float:right;margin-left:32px}.fp-pathbar .fp-path-folder-name{margin-left:32px;line-height:20px}.dir-rtl .fp-pathbar .fp-path-folder-name{margin-right:32px;line-height:20px}.fp-iconview .fp-file{float:left;text-align:center;position:relative;margin:10px 10px 35px}.fp-iconview .fp-thumbnail{min-width:110px;min-height:110px;line-height:110px;text-align:center;border:1px solid #FFFFFF;display:block}.fp-iconview .fp-thumbnail img{border:1px solid #ddd;padding:3px;vertical-align:middle;-webkit-box-shadow:1px 1px 2px 0 #ccc;-moz-box-shadow:1px 1px 2px 0 #ccc;box-shadow:1px 1px 2px 0 #ccc}.fp-iconview .fp-thumbnail:hover{background:#fff;border:1px solid #ddd;-webkit-box-shadow:inset 0 0 10px 0 #ccc;-moz-box-shadow:inset 0 0 10px 0 #ccc;box-shadow:inset 0 0 10px 0 #ccc}.fp-iconview .fp-filename-field{height:33px;word-wrap:break-word;overflow:hidden;position:absolute}.fp-iconview .fp-filename-field:hover{overflow:visible;z-index:1000}.fp-iconview .fp-filename-field .fp-filename{background:#FFFFFF;padding-top:5px;padding-bottom:12px;min-width:112px}.dir-rtl .fp-iconview .fp-file{float:right}.file-picker .yui3-datatable table{border:0 solid #BBBBBB;width:100%}#filesskin .file-picker .yui3-datatable-header{background:#FFFFFF;border-bottom:1px solid #CCCCCC;border-left:0 solid #FFFFFF;color:#555555}#filesskin .file-picker .yui3-datatable-odd .yui3-datatable-cell{background-color:#F6F6F6;border-left:0 solid #F6F6F6}#filesskin .file-picker .yui3-datatable-even .yui3-datatable-cell{background-color:#FFFFFF;border-left:0 solid #FFFFFF}.dir-rtl .file-picker .yui3-datatable-header{text-align:right}.file-picker .ygtvtn,.filemanager .ygtvtn{background:url('[[pix:moodle|y/tn]]') 0 0 no-repeat;width:17px;height:22px}.dir-rtl .filemanager .ygtvtn,.dir-rtl .file-picker .ygtvtn{background:url('[[pix:moodle|y/tn_rtl]]') 0 0 no-repeat;width:17px;height:22px}.file-picker .ygtvtm,.filemanager .ygtvtm{background:url('[[pix:moodle|y/tm]]') 0 10px no-repeat;width:13px;height:12px;cursor:pointer}.file-picker .ygtvtmh,.filemanager .ygtvtmh{background:url('[[pix:moodle|y/tm]]') 0 10px no-repeat;width:13px;height:12px;cursor:pointer}.file-picker .ygtvtp,.filemanager .ygtvtp{background:url('[[pix:moodle|y/tp]]') 0 10px no-repeat;width:13px;height:12px;cursor:pointer}.dir-rtl .file-picker .ygtvtp,.dir-rtl .filemanager .ygtvtp{background:url('[[pix:moodle|y/tp_rtl]]') 0 10px no-repeat}.file-picker .ygtvtph,.filemanager .ygtvtph{background:url('[[pix:moodle|y/tp]]') 0 10px no-repeat;width:13px;height:22px;cursor:pointer}.dir-rtl .file-picker .ygtvtph,.dir-rtl .filemanager .ygtvtph{background:url('[[pix:moodle|y/tp_rtl]]') 0 10px no-repeat}.file-picker .ygtvln,.filemanager .ygtvln{background:url('[[pix:moodle|y/ln]]') 0 0 no-repeat;width:17px;height:22px}.dir-rtl .file-picker .ygtvln,.dir-rtl .filemanager .ygtvln{background:url('[[pix:moodle|y/ln_rtl]]') 0 0 no-repeat}.file-picker .ygtvlm,.filemanager .ygtvlm{background:url('[[pix:moodle|y/lm]]') 0 10px no-repeat;width:13px;height:12px;cursor:pointer}.file-picker .ygtvlmh,.filemanager .ygtvlmh{background:url('[[pix:moodle|y/lm]]') 0 10px no-repeat;width:13px;height:12px;cursor:pointer}.file-picker .ygtvlp,.filemanager .ygtvlp{background:url('[[pix:moodle|y/lp]]') 0 10px no-repeat;width:13px;height:12px;cursor:pointer}.dir-rtl .file-picker .ygtvlp,.dir-rtl .filemanager .ygtvlp{background:url('[[pix:moodle|y/lp_rtl]]') 0 10px no-repeat}.file-picker .ygtvlph,.filemanager .ygtvlph{background:url('[[pix:moodle|y/lp]]') 0 10px no-repeat;width:13px;height:12px;cursor:pointer}.dir-rtl .file-picker .ygtvlph,.dir-rtl .filemanager .ygtvlph{background:url('[[pix:moodle|y/lp_rtl]]') 0 10px no-repeat}.file-picker .ygtvloading,.filemanager .ygtvloading{background:transparent url('[[pix:moodle|y/loading]]') 0 0 no-repeat;width:16px;height:22px}.file-picker .ygtvdepthcell,.filemanager .ygtvdepthcell{background:url('[[pix:moodle|y/vline]]') 0 0 no-repeat;width:17px;height:32px}.file-picker .ygtvblankdepthcell,.filemanager .ygtvblankdepthcell{width:17px;height:22px}a.ygtvspacer:hover{color:transparent;text-decoration:none}.ygtvlabel,.ygtvlabel:link,.ygtvlabel:visited,.ygtvlabel:hover{background-color:transparent;cursor:pointer;margin-left:2px;text-decoration:none}.file-picker .ygtvfocus,.filemanager .ygtvfocus{background-color:#EEEEEE}.fp-filename-icon{margin-top:10px;display:block;position:relative}.fp-icon{float:left;margin-top:-7px;width:24px;height:24px;margin-right:10px;text-align:center;line-height:24px}.dir-rtl .fp-icon{float:right;margin-left:10px;margin-right:0}.fp-icon img{max-height:24px;max-width:24px;vertical-align:middle}.fp-filename{padding-right:10px}.dir-rtl .fp-filename{padding-left:10px;padding-right:0}.file-picker .fp-login-form{height:100%;width:100%;display:table}.file-picker .fp-login-form table{margin:0 auto}.file-picker .fp-login-form p{text-align:center;margin-top:3em}.file-picker .fp-login-form .fp-login-input label{text-align:right;display:block}.file-picker .fp-login-form .fp-login-input .input{text-align:left}.file-picker .fp-login-form input[type="checkbox"]{width:15px;height:15px}.file-picker .fp-upload-form{height:100%;width:100%;display:table}.file-picker .fp-upload-form table{margin:0 auto}.file-picker.fp-dlg{text-align:center}.file-picker.fp-dlg .fp-dlg-text{padding:30px 20px 10px;font-size:12px}.file-picker.fp-dlg .fp-dlg-buttons{margin:0 20px}.file-picker.fp-msg{text-align:center}.file-picker.fp-msg .fp-msg-text{padding:40px 20px 10px 20px;min-width:200px;max-width:500px;max-height:300px;overflow:auto;font-size:12px}.file-picker.fp-msg.fp-msg-error .fp-msg-text{padding:40px 20px 10px 20px;font-size:12px}.file-picker .fp-content-error{height:100%;width:100%;display:table;text-align:center}.file-picker .fp-content-error .fp-error{height:100%;width:100%;display:table-cell;vertical-align:middle;padding:40px 20px 10px 20px;font-size:12px}.file-picker .fp-nextpage{clear:both}.file-picker .fp-nextpage .fp-nextpage-loading{display:none}.file-picker .fp-nextpage.loading .fp-nextpage-link{display:none}.file-picker .fp-nextpage.loading .fp-nextpage-loading{display:block;text-align:center;height:100px;padding-top:50px}.fp-select form{padding:20px 20px 0}.fp-select .fp-select-loading{text-align:center;margin-top:20px}.fp-select .fp-hr{clear:both;height:1px;background-color:#FFFFFF;border-bottom:1px solid #BBBBBB;width:auto;margin:10px 0}.fp-select table{padding:0 0 10px}.fp-select table .mdl-right{min-width:84px}.fp-select .fp-reflist .mdl-right{vertical-align:top}.fp-select .fp-select-buttons{float:right}.fp-select .fp-info{display:block;clear:both;padding:1px 20px 0}.fp-select .fp-thumbnail{float:left;min-width:110px;min-height:110px;line-height:110px;text-align:center;margin:10px 20px 0 0;background:#fff;border:1px solid #ddd;-webkit-box-shadow:inset 0 0 10px 0 #ccc;-moz-box-shadow:inset 0 0 10px 0 #ccc;box-shadow:inset 0 0 10px 0 #ccc}.fp-select .fp-thumbnail img{border:1px solid #DDDDDD;padding:3px;vertical-align:middle;margin:10px}.fp-select .fp-fileinfo{display:inline-block;*display:inline;*zoom:1;margin-top:10px}.file-picker.fp-select .fp-fileinfo{max-width:240px}.fp-select .fp-fileinfo div{padding-bottom:5px}.file-picker.fp-select .uneditable{display:none}.file-picker.fp-select .fp-select-loading{display:none}.file-picker.fp-select.loading .fp-select-loading{display:block}.file-picker.fp-select.loading form{display:none}.fp-select .fp-dimensions.fp-unknown{display:none}.fp-select .fp-size.fp-unknown{display:none}.filemanager-loading{display:none}.jsenabled .filemanager-loading{display:block;margin-top:100px}.filemanager.fm-loading .filemanager-toolbar,.filemanager.fm-loading .fp-pathbar,.filemanager.fm-loading .filemanager-container,.filemanager.fm-loaded .filemanager-loading,.filemanager.fm-maxfiles .fp-btn-add,.filemanager.fm-maxfiles .dndupload-message,.filemanager.fm-noitems .fp-btn-download,.filemanager .fm-empty-container,.filemanager.fm-noitems .filemanager-container .fp-content{display:none}.filemanager .fp-img-downloading{display:none;padding-top:7px}.filemanager .filemanager-updating{display:none;text-align:center}.filemanager.fm-updating .filemanager-updating{display:block;margin-top:37px}.filemanager.fm-updating .fm-content-wrapper,.filemanager.fm-nomkdir .fp-btn-mkdir,.fitem.disabled .filemanager .filemanager-toolbar,.fitem.disabled .filemanager .fp-pathbar,.fitem.disabled .filemanager .fp-restrictions,.fitem.disabled .filemanager .fm-content-wrapper{display:none}.filemanager .fp-restrictions{text-align:right}.filemanager .fp-navbar{background:#F2F2F2;border:1px solid #BBBBBB;border-bottom:none}.filemanager-toolbar{padding:4px;overflow:hidden}.fp-pathbar{border-top:1px solid #BBBBBB;padding:5px 8px 1px;min-height:20px}.file-picker .fp-toolbar{padding:4px}.fp-toolbar .fp-btn-add,.fp-toolbar .fp-btn-download,.fp-toolbar .fp-btn-mkdir,.fp-toolbar .fp-tb-help,.fp-toolbar .fp-tb-manage,.fp-toolbar .fp-tb-logout,.fp-toolbar .fp-tb-refresh{border:1px solid #CCC;border-bottom:1px solid #B3B3B3;border-radius:4px;background:white;width:30px;height:30px}.fp-toolbar a:hover{background-image:radial-gradient(ellipse at center, #ffffff 60%, #dfdfdf 100%);background-color:#ebebeb}.fp-toolbar a:active{background-image:radial-gradient(ellipse at center, #ffffff 40%, #dfdfdf 100%);background-color:#dfdfdf}.fp-btn-add a,.fp-btn-download a,.fp-btn-mkdir a,.fp-tb-help a,.fp-tb-manage a,.fp-tb-logout a,.fp-tb-refresh a{display:block;width:30px;height:30px;border-radius:4px}.fp-btn-add img,.fp-btn-download img,.fp-btn-mkdir img,.fp-tb-help img,.fp-tb-manage img,.fp-tb-logout img,.fp-tb-refresh img{margin:7px}.filemanager .fp-pathbar.empty{display:none}.filepicker-filelist,.filemanager-container{background:#FFFFFF;clear:both;overflow:auto;border:1px solid #BBBBBB;min-height:140px;position:relative}.filemanager .fp-content{overflow:auto;max-height:472px;min-height:157px}.filemanager-container,.filepicker-filelist{overflow:hidden}.fitem.disabled .filepicker-filelist,.fitem.disabled .filemanager-container{background-color:#EBEBE4}.fitem.disabled .fp-btn-choose{color:#999}.fitem.disabled .filepicker-filelist .filepicker-filename{display:none}.fp-iconview .fp-reficons1{position:absolute;height:100%;width:100%;top:0;left:0}.fp-iconview .fp-reficons2{position:absolute;height:100%;width:100%;top:0;left:0}.fp-iconview .fp-file.fp-hasreferences .fp-reficons1{background:url('[[pix:theme|fp/link]]') no-repeat;background-position:bottom right}.fp-iconview .fp-file.fp-isreference .fp-reficons2{background:url('[[pix:theme|fp/alias]]') no-repeat;background-position:bottom left}.filemanager .fp-iconview .fp-file.fp-originalmissing .fp-thumbnail img{display:none}.filemanager .fp-iconview .fp-file.fp-originalmissing .fp-thumbnail{background:url([[pix:s/dead]]) no-repeat;background-position:center center}.filemanager .yui3-datatable table{border:0 solid #BBBBBB;width:100%}.filemanager .yui3-datatable-header{background:#FFFFFF !important;border-bottom:1px solid #CCCCCC !important;border-left:0 solid #FFFFFF !important;color:#555555 !important}.filemanager .yui3-datatable-odd .yui3-datatable-cell{background-color:#F6F6F6 !important;border-left:0 solid #F6F6F6}.filemanager .yui3-datatable-even .yui3-datatable-cell{background-color:#FFFFFF !important;border-left:0 solid #FFFFFF}.filemanager .fp-filename-icon.fp-hasreferences .fp-reficons1{background:url('[[pix:theme|fp/link_sm]]') no-repeat 0 0;height:100%;width:100%;position:absolute;top:8px;left:17px;z-index:1000}.filemanager .fp-filename-icon.fp-isreference .fp-reficons2{background:url('[[pix:theme|fp/alias_sm]]') no-repeat 0 0;height:100%;width:100%;position:absolute;top:9px;left:-6px;z-index:1001}.filemanager .fp-contextmenu{display:none}.filemanager .fp-iconview .fp-folder.fp-hascontextmenu .fp-contextmenu{display:block;position:absolute;right:7px;bottom:5px}.filemanager .fp-treeview .fp-folder.fp-hascontextmenu .fp-contextmenu,.filemanager .fp-tableview .fp-folder.fp-hascontextmenu .fp-contextmenu{display:inline;position:absolute;left:14px;margin-right:-20px;top:6px}.dir-rtl .filemanager .fp-iconview .fp-folder.fp-hascontextmenu .fp-contextmenu{left:7px;right:inherit}.dir-rtl .filemanager .fp-treeview .fp-folder.fp-hascontextmenu .fp-contextmenu,.dir-rtl .filemanager .fp-tableview .fp-folder.fp-hascontextmenu .fp-contextmenu{left:inherit;right:16px;margin-right:0}.filepicker-filelist .filepicker-container,.filemanager.fm-noitems .fm-empty-container{display:block;position:absolute;top:10px;bottom:10px;left:10px;right:10px;border:2px dashed #BBBBBB;padding-top:85px;text-align:center}.filepicker-filelist .dndupload-target,.filemanager-container .dndupload-target{background:#FFFFFF;position:absolute;top:10px;bottom:10px;left:10px;right:10px;border:2px dashed #fb7979;padding-top:85px;text-align:center;-webkit-box-shadow:0 0 0 10px #fff;-moz-box-shadow:0 0 0 10px #fff;box-shadow:0 0 0 10px #fff}.filepicker-filelist.dndupload-over .dndupload-target,.filemanager-container.dndupload-over .dndupload-target{background:#FFFFFF;position:absolute;top:10px;bottom:10px;left:10px;right:10px;border:2px dashed #6c8cd3;padding-top:85px;text-align:center}.dndupload-message{display:none}.dndsupported .dndupload-message{display:inline}.dnduploadnotsupported-message{display:none}.dndnotsupported .dnduploadnotsupported-message{display:inline}.dndupload-target{display:none}.dndsupported .dndupload-ready .dndupload-target{display:block}.dndupload-uploadinprogress{display:none;text-align:center}.dndupload-uploading .dndupload-uploadinprogress{display:block}.dndupload-arrow{background:url([[pix:theme|fp/dnd_arrow]]) center no-repeat;width:100%;height:80px;position:absolute;top:5px}.fitem.disabled .filepicker-container,.fitem.disabled .fm-empty-container{display:none}.dndupload-progressbars{padding:10px;display:none}.dndupload-inprogress .dndupload-progressbars{display:block}.dndupload-inprogress .fp-content{display:none}.filemanager.fm-noitems .dndupload-inprogress .fm-empty-container{display:none}.filepicker-filelist.dndupload-inprogress .filepicker-container{display:none}.filepicker-filelist.dndupload-inprogress a{display:none}.filemanager.fp-select .fp-select-loading{display:none}.filemanager.fp-select.loading .fp-select-loading{display:block}.filemanager.fp-select.loading form{display:none}.filemanager.fp-select.fp-folder .fp-license,.filemanager.fp-select.fp-folder .fp-author,.filemanager.fp-select.fp-file .fp-file-unzip,.filemanager.fp-select.fp-folder .fp-file-unzip,.filemanager.fp-select.fp-file .fp-file-zip,.filemanager.fp-select.fp-zip .fp-file-zip{display:none}.filemanager.fp-select .fp-file-setmain,.filemanager.fp-select .fp-file-setmain-help{display:none}.filemanager.fp-select.fp-cansetmain .fp-file-setmain,.filemanager.fp-select.fp-cansetmain .fp-file-setmain-help{display:inline-block;*display:inline;*zoom:1}.filemanager .fp-mainfile .fp-filename{font-weight:bold}.filemanager.fp-select.fp-folder .fp-file-download{display:none}.fm-operation{font-weight:bold}.filemanager.fp-select .fp-original.fp-unknown,.filemanager.fp-select .fp-original .fp-originloading{display:none}.filemanager.fp-select .fp-original.fp-loading .fp-originloading{display:inline}.filemanager.fp-select .fp-reflist.fp-unknown,.filemanager.fp-select .fp-reflist .fp-reflistloading{display:none}.filemanager.fp-select .fp-refcount{max-width:265px}.filemanager.fp-select .fp-reflist.fp-loading .fp-reflistloading{display:inline}.filemanager.fp-select .fp-reflist .fp-value{background:#F9F9F9;border:1px solid #BBBBBB;padding:8px 7px;margin:0;max-width:265px;max-height:75px;overflow:auto}.filemanager.fp-select .fp-reflist .fp-value li{padding-bottom:7px}.filemanager.fp-mkdir-dlg{text-align:center}.filemanager.fp-mkdir-dlg .fp-mkdir-dlg-text{text-align:left;margin:20px}.dir-rtl .filemanager .fp-mkdir-dlg p{text-align:right}.filemanager.fp-dlg{text-align:center}.filemanager.fp-dlg .fp-dlg-text{padding:0 10px;min-width:200px;max-width:340px;max-height:300px;overflow:auto;line-height:22px;margin:40px 20px 20px;font-size:12px}.file-picker div.bd{text-align:left}.dir-rtl .filemanager .fp-restrictions{text-align:left}.dir-rtl .file-picker div.bd,.dir-rtl .file-picker .fp-pathbar,.dir-rtl .file-picker .fp-list,.dir-rtl #filemenu .yuimenuitemlabel,.dir-rtl .filemanager-container .yui3-skin-sam .yui3-datatable-header{text-align:right}.dir-rtl .filepicker .yui-layout-unit-left{left:500px}.dir-rtl .filepicker .yui-layout-unit-center{left:0}.dir-rtl .file-picker .fp-toolbar .fp-tb-search input{background-position:208px 7px;padding:2px 30px 1px 3px}.dir-rtl .file-picker .fp-toolbar div{float:right;margin-left:4px}.fp-formset{max-width:500px;padding:10px}.fp-formset input[type="file"]{line-height:inherit}.fp-forminset{max-width:400px;padding:0 10px}.fp-forminset .control-group.control-radio{margin-bottom:0}.fp-forminset .control-group label.control-label{width:105px}.fp-forminset .control-group label.control-radio{float:right;text-align:left;width:215px}.fp-forminset .control-group .controls{margin-left:125px}.fp-forminset .control-group .controls select{width:100%}.fp-forminset .control-group .controls.control-radio input{margin-top:3px}.fp-forminset .fp-select-buttons{float:none}.fp-forminset input[type="text"]{width:228px}.fp-fileinfo .fp-value{display:inline-block;padding-left:5px}.dir-rtl .fp-forminset{max-width:400px}.dir-rtl .fp-forminset .control-group label.control-label{float:right;text-align:left}.dir-rtl .fp-forminset .control-group label.control-radio{float:left;text-align:right;width:215px}.dir-rtl .fp-forminset .control-group .controls{margin-left:0;margin-right:125px}.dir-rtl .fp-forminset .fp-select-buttons{float:left}.dir-rtl .fp-forminset input[type="text"]{width:228px}.dir-rtl .fp-fileinfo .fp-value{display:inline-block;padding-right:5px}.dir-rtl .fp-select .fp-thumbnail{margin:10px 0 0 0}.dir-rtl .filepicker .fp-formset label{float:right;text-align:left}.dir-rtl .filepicker .fp-formset .controls{margin-left:0;text-align:right}.message-discussion-noframes h1{font-size:1em}.message-discussion-noframes #userinfo .commands,.message .noframesjslink,.message .link{font-size:11.9px}.message .heading{font-size:1em;font-weight:bold;clear:both;word-wrap:break-word}.message .author{font-weight:bold}.message .time{font-style:italic}#page-message-user .commands span{font-size:.7em}#page-message-user .name{font-weight:bold;font-size:1.1em}.message .time{color:#999}#page-message-messages{padding:10px}#page-message-send .notifysuccess{padding:1px}#page-message-send td.fixeditor{text-align:center}.message{overflow:hidden}.message .note{padding:10px}table.message .searchresults td{padding:5px}.message .contactselector{max-width:380px;margin:0 auto;width:auto}@media screen and (min-width:1000px){.message .contactselector{float:left;padding:0 8px 0 0}}.message .contactselector .singleselect{width:240px}.message .contactselector .paging{z-index:1;position:relative}.message .contactselector #message_participants{max-width:240px}.message .contactselector .message-contacts{list-style-type:none;margin:0}.message .contactselector .message-contacts li{clear:both;position:relative;min-height:23px;padding:2px}.message .contactselector .message-contacts li:nth-child(odd){background:rgba(120,120,255,0.1)}.message .contactselector .message-contacts li>div{display:block;height:100%}.message .contactselector .message-contacts li>div>*{vertical-align:middle;display:inline-block}.message .contactselector .message-contacts li>div.pix{position:relative;float:left}.message .contactselector .message-contacts li>div.link{white-space:nowrap;width:60px;position:relative;float:right;text-align:right}.message .contactselector .message-contacts li>div.contact{position:relative;word-break:break-all}.message .contactselector .message-contacts li>div.contact a{line-height:22px}.dir-ltr .message .message-contacts li{text-align:left}.dir-ltr .message .message-contacts li>div.pix{float:left}.dir-ltr .message .message-contacts li>div.link{float:right;text-align:right}.dir-ltr .message .message-contacts li>div.contact{margin:0 60px 0 24px}.dir-ltr .message .message-contacts li>div.contact.nolinks{margin:0 0 0 24px}.dir-rtl .message .message-contacts li{text-align:right}.dir-rtl .message .message-contacts li>div.pix{float:right}.dir-rtl .message .message-contacts li>div.link{float:left;text-align:left}.dir-rtl .message .message-contacts li>div.contact{margin:0 24px 0 60px}.dir-rtl .message .message-contacts li>div.contact.nolinks{margin:0 24px 0 0}@media screen and (min-width:1000px){.dir-rtl .message .contactselector{float:right}}.message .messagearea{float:none;overflow:hidden;min-height:200px;min-width:300px}@media screen and (min-width:1000px){.message .messagearea{border-left:1px solid #ddd;padding:0 8px}}@media screen and (max-width:1000px){.message .messagearea{width:100%}}#message_user_pictures{text-align:center}.dir-rtl #message_user_pictures{direction:rtl}.message .messagearea .messagehistorytype{clear:both;padding-bottom:20px}.message .messagearea .messagehistory .user{vertical-align:top;width:45%;min-width:100px;display:inline-block}.message .messagearea .messagehistory .user>div{text-align:center}.message .messagearea .messagehistory .between{display:inline-block;width:16px;margin:0 1%;padding-top:40px}@media screen and (max-width:320px){.message .messagearea{min-width:0}.message .messagearea .messagehistory .user{max-width:70px;min-width:0}.message .messagearea .messagehistory .user .userpicture{width:50px;height:auto}}@media screen and (min-width:800px){.message .messagearea .messagehistory .between{margin:0 3%}.message .messagearea .messagehistory .user{width:32%}}@media screen and (min-width:1200px){.message .messagearea .messagehistory .user{width:25%}}.message .messagearea .messagehistory .heading{width:100%;clear:both}.message .messagearea .messagehistory .left{margin-bottom:10px;width:50%;float:left;position:relative;clear:both}.dir-rtl .message .messagearea .messagehistory .left{float:right}.message .messagearea .messagehistory .right{margin-bottom:10px;width:50%;float:right;position:relative;clear:both}.dir-rtl .message .messagearea .messagehistory .right{float:left}.message .messagearea .messagehistory .notification{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);border-color:#e3e3e3;padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;margin-bottom:0;margin-top:5px}.message .messagearea .messagehistory .notification blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.dir-ltr .message .messagearea .messagehistory .message{margin-right:20px}.dir-ltr .message .messagearea .messagehistory .right .message{margin-left:20px}.dir-rtl .message .messagearea .messagehistory .message{margin-left:20px}.dir-rtl .message .messagearea .messagehistory .right .message{margin-right:20px}.message .messagearea .messagehistory .messageactive{background-color:#f5f5f5}.message .messagearea .messagehistory .messagecontent .deleteicon{width:20px;position:absolute;top:-2px}.dir-ltr .message .messagearea .messagehistory .messagecontent .deleteicon{right:0}.dir-rtl .message .messagearea .messagehistory .messagecontent .deleteicon{left:0}.message .messagearea .messagesend{padding-top:20px;clear:both}.message .messagearea .messagesend .messagesendbox{width:100%;box-sizing:border-box}.message .messagearea .messagesend fieldset{padding:0;margin:0}.message .messagearea .messagerecent{text-align:left;width:100%}.message .messagearea .messagerecent .singlemessage{border-bottom:1px solid #ddd;padding:10px}.message .messagearea .messagerecent .singlemessage .otheruser span{padding:5px}.message .messagearea .messagerecent .singlemessage .messagedate{float:right}.message .hiddenelement{display:none}.message .visible{display:inline}.message #usergroupselector.fieldset,.message #viewing{width:100%}.messagesearchresults{margin-bottom:40px}.messagesearchresults td{padding:0 10px 0 20px}.messagesearchresults td span{white-space:nowrap}.messagesearchresults td img.userpicture{padding-right:.45em;vertical-align:text-bottom}.dir-rtl .messagesearchresults td img.userpicture{padding-left:.45em;padding-right:0}.messagesearchresults td span img{padding:0 0 0 .45em;vertical-align:text-bottom}.dir-rtl .messagesearchresults td span img{padding:0 .45em 0 0}#newmessageoverlay{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);border-color:#e3e3e3;margin:0 1em;position:fixed;bottom:0;right:0}#newmessageoverlay blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}#newmessageoverlay #usermessage{padding:10px}#page-user-action_redir #edit-messagebody{width:auto}.core_message-messenger-sendmessage-hidden{display:none}.core_message-messenger-sendmessage .message-actions{position:relative}.core_message-messenger-sendmessage .message-area{height:240px;max-height:100%;position:relative;margin-bottom:10px}.core_message-messenger-sendmessage .message-input{width:100%;height:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.core_message-messenger-sendmessage .message-send{margin:0;float:right}.core_message-messenger-sendmessage .message-notice-area{display:table;position:absolute;top:0;bottom:0;left:0;right:0;width:100%;height:100%}.core_message-messenger-sendmessage .message-notice{display:table-cell;vertical-align:middle;text-align:center}.core_message-messenger-sendmessage .message-notice>div{background:#eee;padding:5px;font-size:12px}.core_message-messenger-sendmessage .message-footer{margin-top:3px;line-height:20px}.core_message-messenger-sendmessage .message-history{position:absolute;bottom:0}.dir-rtl .core_message-messenger-sendmessage .message-send{float:left}.questionbank h2{margin-top:0}.questioncategories h3{margin-top:0}#chooseqtypebox{margin-top:1em}#chooseqtype h3{margin:0 0 .3em}#chooseqtype .instruction{display:none}#chooseqtype .fakeqtypes{border-top:1px solid silver}#chooseqtype .qtypeoption{margin-bottom:.5em}#chooseqtype label{display:block}#chooseqtype .qtypename img{padding:0 .3em}#chooseqtype .qtypename{display:inline-table;width:16em}#chooseqtype .qtypesummary{display:block;margin:0 2em}#chooseqtype .submitbuttons{margin:.7em 0;text-align:center}#qtypechoicecontainer{display:none}#qtypechoicecontainer_c.yui-panel-container.shadow .underlay{background:none}#qtypechoicecontainer.yui-panel .hd{color:#333;letter-spacing:1px;text-shadow:1px 1px 1px #fff;-webkit-border-top-right-radius:10px;-moz-border-radius-topright:10px;border-top-right-radius:10px;-webkit-border-top-left-radius:10px;-moz-border-radius-topleft:10px;border-top-left-radius:10px;border:1px solid #ccc;border-bottom:1px solid #bbb;background-color:#ebebeb;background-image:-moz-linear-gradient(top, #fff, #ccc);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#ccc));background-image:-webkit-linear-gradient(top, #fff, #ccc);background-image:-o-linear-gradient(top, #fff, #ccc);background-image:linear-gradient(to bottom, #fff, #ccc);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffcccccc', GradientType=0)}#qtypechoicecontainer{font-size:12px;color:#333;background:#F2F2F2;-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;border:1px solid #ccc;border-top:0 none;-webkit-box-shadow:5px 5px 20px 0 #666;-moz-box-shadow:5px 5px 20px 0 #666;box-shadow:5px 5px 20px 0 #666}#qtypechoicecontainer #chooseqtype{width:40em}#chooseqtypehead h3{margin:0;font-weight:normal}#chooseqtype .qtypes{position:relative;border-bottom:1px solid #bbb;padding:.24em 0}#chooseqtype .alloptions{overflow-x:hidden;overflow-y:auto;max-height:400px;max-height:calc(85vh);max-height:60vh;width:60%}#chooseqtype .qtypeoption{margin-bottom:0;padding:.3em .3em .3em 1.6em}#chooseqtype .qtypeoption img{vertical-align:text-bottom;padding-left:1em;padding-right:.5em}#chooseqtype .selected{background-color:#fff;-webkit-box-shadow:0 0 10px 0 #ccc;-moz-box-shadow:0 0 10px 0 #ccc;box-shadow:0 0 10px 0 #ccc}#chooseqtype .instruction,#chooseqtype .qtypesummary{display:none;position:absolute;top:0;right:0;bottom:0;left:60%;margin:0;overflow-x:hidden;padding:1.5em 1.6em;background-color:#fff;overflow-y:auto}#chooseqtype .instruction,#chooseqtype .selected .qtypesummary{display:block}#categoryquestions{margin:0}#categoryquestions td,#categoryquestions th{padding:0 .2em}#categoryquestions th{text-align:left;font-weight:normal}#categoryquestions .checkbox{padding-left:5px}#categoryquestions .checkbox input[type="checkbox"]{margin-left:0;float:none}#categoryquestions img.iconsmall{padding:0}#categoryquestions .iconcol{padding:3px}#categoryquestions label{margin:0}#page-mod-quiz-edit div.questionbankwindow div.header{margin:0}#page-mod-quiz-edit div.questionbankwindow.block{padding:0}.dir-rtl #categoryquestions th{text-align:right}.questionbank .singleselect{margin:0}#combinedfeedbackhdr div.fhtmleditor{padding:0}#combinedfeedbackhdr div.fcheckbox{margin-bottom:1em}#multitriesheader div.fitem_feditor{margin-top:1em}#multitriesheader div.fitem_fgroup{margin-bottom:1em}#multitriesheader div.fitem_fgroup fieldset.felement label{margin-left:.3em;margin-right:.3em}body.path-question-type .fitem_fgroup .accesshide{font:inherit;left:0;position:static;padding-right:.3em}.que{clear:left;text-align:left;margin:0 auto 1.8em auto}.dir-rtl .que{text-align:right}.que .info{float:left;width:7em;padding:.5em;margin-bottom:1.8em;background-color:#eee;border:1px solid #dcdcdc;-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px}.que h3.no{margin:0;font-size:.8em;line-height:1}.que span.qno{font-size:1.5em;font-weight:bold}.que .info>div{font-size:.8em;margin-top:.7em}.que .info .questionflag.editable{cursor:pointer}.que .info .editquestion img,.que .info .questionflag img,.que .info .questionflag input{vertical-align:bottom}.que .content{margin:0 0 0 8.5em}.que .formulation,.que .outcome,.que .comment{padding:8px 35px 8px 14px;margin-bottom:20px;text-shadow:0 1px 0 rgba(255,255,255,0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;color:#8a6d3b}.que .formulation{background-color:#d9edf7;border-color:#bce8f1;color:#3a87ad;color:#333}.formulation input[type="text"],.formulation select{width:auto;vertical-align:baseline}.path-mod-quiz input[size]{width:auto}.que .comment{background-color:#dff0d8;border-color:#d6e9c6;color:#468847}.que .history{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);border-color:#e3e3e3}.que .history blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.que .ablock{margin:.7em 0 .3em 0}.que .im-controls{margin-top:.5em;text-align:left}.dir-rtl .que .im-controls{text-align:right}.que .specificfeedback,.que .generalfeedback,.que .rightanswer,.que .im-feedback,.que .feedback,.que p{margin:0 0 .5em}.que .qtext{margin-bottom:1.5em}.que .correctness{display:inline-block;padding:2px 4px;font-size:11.844px;font-weight:bold;line-height:14px;color:#fff;vertical-align:baseline;white-space:nowrap;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.que .correctness:empty{display:none}.que .correctness-important{background-color:#b94a48}.que .correctness-important[href]{background-color:#953b39}.que .correctness-warning{background-color:#f89406}.que .correctness-warning[href]{background-color:#c67605}.que .correctness-success{background-color:#468847}.que .correctness-success[href]{background-color:#356635}.que .correctness-info{background-color:#3a87ad}.que .correctness-info[href]{background-color:#2d6987}.que .correctness-inverse{background-color:#333}.que .correctness-inverse[href]{background-color:#1a1a1a}.que .correctness.correct{background-color:#468847}.que .correctness.partiallycorrect{background-color:#f89406}.que .correctness.notanswered,.que .correctness.incorrect{background-color:#b94a48}.que .validationerror{color:#b94a48}.formulation .correct{background-color:#dff0d8}.formulation .partiallycorrect{background-color:#fcf8e3}.formulation .incorrect{background-color:#f2dede}.formulation select.correct,.formulation input.correct{color:#468847;background-color:#dff0d8;border-color:#468847;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.formulation select.correct:focus,.formulation input.correct:focus{border-color:#356635;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b}.formulation select.partiallycorrect,.formulation input.partiallycorrect{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.formulation select.partiallycorrect:focus,.formulation input.partiallycorrect:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b}.formulation select.incorrect,.formulation input.incorrect{color:#b94a48;background-color:#f2dede;border-color:#b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.formulation select.incorrect:focus,.formulation input.incorrect:focus{border-color:#953b39;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392}.que .grading,.que .comment,.que .commentlink,.que .history{margin-top:.5em}.que .history h3{margin:0 0 .2em;font-size:1em}.que .history table{width:100%;margin:0}.que .history .current{font-weight:bold}.que .questioncorrectnessicon{vertical-align:text-bottom}.que input.questionflagimage{padding-right:3px}.dir-rtl .que input.questionflagimage{padding-left:3px;padding-right:0}.importerror{margin-top:10px;border-bottom:1px solid #555}.mform .que.comment .fitemtitle{width:20%}#page-question-preview #techinfo{margin:1em 0}.dir-rtl #chooseqtype .instruction,.dir-rtl #chooseqtype .qtypesummary{right:60%;left:0;border-left:0;border-right:1px solid grey}#page-mod-quiz-edit .box.generalbox.questionbank{padding:.5em}#page-mod-quiz-edit .questionbank .categorypagingbarcontainer,#page-mod-quiz-edit .questionbank .categoryquestionscontainer,#page-mod-quiz-edit .questionbank .choosecategory{padding:0}#page-mod-quiz-edit .questionbank .choosecategory select{width:100%}#page-mod-quiz-edit div.questionbank .categoryquestionscontainer{background:transparent}#page-mod-quiz-edit #categoryquestions>thead{background:#FFF}#page-mod-quiz-edit #categoryquestions>tbody>tr:nth-of-type(even){background:#e4e4e4}#page-mod-quiz-edit .questionbankwindow div.header{color:#444;text-shadow:none;padding:3px;-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px;margin:0 -10px 0 -10px;padding:2px 10px 2px 10px;background:transparent}#page-mod-quiz-edit .questionbankwindow div.header a:link,#page-mod-quiz-edit .questionbankwindow div.header a:visited{color:#0070a8}#page-mod-quiz-edit .questionbankwindow div.header a:hover{color:#003d5c}#page-mod-quiz-edit .createnewquestion{padding:.3em 0}#page-mod-quiz-edit .createnewquestion div,#page-mod-quiz-edit .createnewquestion input{margin:0}#page-mod-quiz-edit .questionbankwindow div.header .title{color:#333}#page-mod-quiz-edit div.container div.generalbox{background-color:transparent;padding:1.5em}#page-mod-quiz-edit .categoryinfo{background-color:transparent;border-bottom:none}#page-mod-quiz-edit .createnewquestion .singlebutton input{margin-bottom:0}#page-mod-quiz-edit div.questionbank .categorysortopotionscontainer,#page-mod-quiz-edit div.questionbank .categoryselectallcontainer{padding:0 0 1.5em 0}#page-mod-quiz-edit div.questionbank .categorypagingbarcontainer{background-color:transparent;margin:0;border-top:0;border-bottom:0}#page-mod-quiz-edit div.questionbank .categorypagingbarcontainer .paging{padding:0 .3em}#page-mod-quiz-edit div.question div.content div.questioncontrols{background-color:#fff}#page-mod-quiz-edit div.question div.content div.points{margin-top:-0.5em;padding-bottom:0;border:none;background-color:#fff;position:static;width:12.1em;float:right;margin-right:60px}#page-mod-quiz-edit.dir-rtl div.question div.content div.points{float:left;margin-left:60px;margin-right:0}#page-mod-quiz-edit div.question div.content div.points br{display:none}#page-mod-quiz-edit div.question div.content div.points label{display:inline-block}#page-mod-quiz-edit div.quizpage .pagecontent .pagestatus{background-color:#fff}#page-mod-quiz-edit .quizpagedelete,#page-mod-quiz-edit .quizpagedelete img{background-color:transparent}#page-mod-quiz-edit div.quizpage .pagecontent{border:1px solid #ddd;-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px;overflow:hidden}#page-mod-quiz-edit div.questionbank .categoryinfo{padding:.3em 0}#page-mod-quiz-edit div.questionbank .modulespecificbuttonscontainer{padding:0}#page-mod-quiz-edit div.questionbank .modulespecificbuttonscontainer strong{display:block}#page-mod-quiz-edit div.questionbank .modulespecificbuttonscontainer hr,#page-mod-quiz-edit div.questionbank .modulespecificbuttonscontainer br{display:none}#page-mod-quiz-edit div.questionbank .modulespecificbuttonscontainer strong{margin-left:-0.3em}#page-mod-quiz-edit div.questionbank .modulespecificbuttonscontainer strong label{margin-left:.3em}#page-mod-quiz-edit div.questionbank .modulespecificbuttonscontainer input{margin-left:0}#page-mod-quiz-edit div.questionbank .modulespecificbuttonscontainer input+input{margin-left:5px}.questionbankwindow .module{width:auto}#page-mod-quiz-edit div.editq div.question div.content{background-color:#fff;border:1px solid #ddd;-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px;overflow:hidden}.path-mod-quiz .statedetails{display:block;font-size:.9em}a#hidebankcmd{color:#0070a8}.que.shortanswer .answer{padding:0}.que label{display:inline}body.path-question-type .mform fieldset.hidden{padding:0;margin:.7em 0 0}.userprofile .fullprofilelink{text-align:center;margin:10px}.userprofile .page-context-header{margin-bottom:10px}.userprofile .description{margin-top:10px;margin-bottom:30px}.userprofile .profile_tree{-webkit-column-count:2;-moz-column-count:2;column-count:2;-webkit-column-gap:20px;-moz-column-gap:20px;column-gap:20px}.userprofile .profile_tree section{display:inline-block;width:100%;border:1px solid #ddd;border-radius:4px;padding:0 15px;margin-bottom:20px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.userprofile .profile_tree section h3{font-size:18px;line-height:20px}.userprofile dl.list{*zoom:1}.userprofile dl.list:before,.userprofile dl.list:after{display:table;content:"";line-height:0}.userprofile dl.list:after{clear:both}.userprofile dl.list dt{float:left;width:180px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.userprofile dl.list dd{margin-left:200px}.user-box{margin:8px;width:115px;height:160px;text-align:center;float:left;clear:none}#page-user-profile .node_category ul,.path-user .node_category ul{margin-left:0;margin-right:0;list-style:none}#page-user-profile .node_category li,.path-user .node_category li{margin-top:5px}#page-user-profile .node_category .editprofile,.path-user .node_category .editprofile,#page-user-profile .node_category .viewmore,.path-user .node_category .viewmore{text-align:right}.dir-rtl .user-box{float:right}.dir-rtl .userprofile .node_category .editprofile,.dir-rtl .userprofile .node_category .viewmore{text-align:left}.dir-rtl .userprofile dl dd{margin-left:0;margin-right:10px}@media (max-width:480px){.userprofile .profile_tree{-webkit-column-count:1;-moz-column-count:1;column-count:1;-webkit-column-gap:20px;-moz-column-gap:20px;column-gap:20px}}.userlist .action-icon img{vertical-align:middle}.userlist #showall{margin:10px 0}.userlist .buttons{text-align:center}.userlist .buttons label{padding:0 3px}.userlist table#participants{text-align:center}.userlist table#participants td,.userlist table#participants th{vertical-align:middle;text-align:left;padding:4px}.userlist table.controls{width:100%}.userlist table.controls tr{vertical-align:top}.userlist table.controls .right{text-align:right}.userlist table.controls .groupselector{margin-bottom:0;margin-top:0}.userlist table.controls .groupselector label{display:block}.userinfobox{width:100%;border:1px solid;border-collapse:separate;padding:10px}.userinfobox .left,.userinfobox .side{width:100px;vertical-align:top}.userinfobox .userpicture{width:100px;height:100px}.userinfobox .content{vertical-align:top}.userinfobox .links{width:100px;padding:5px;vertical-align:bottom}.userinfobox .links a{display:block}.userinfobox .list td{padding:3px}.userinfobox .username{padding-bottom:20px;font-weight:bold}.userinfobox td.label{text-align:right;white-space:nowrap;vertical-align:top;font-weight:bold}.groupinfobox{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);border-color:#e3e3e3}.groupinfobox blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.groupinfobox .left{padding:10px;width:100px;vertical-align:top}.course-participation #showall{text-align:center;margin:10px 0}#user-policy .noticebox{text-align:center;margin-left:auto;margin-right:auto;margin-bottom:10px;width:80%;height:250px}#user-policy #policyframe{width:100%;height:100%}.iplookup #map{margin:auto}.userselector select{width:100%}.userselector div{margin-top:.2em}.userselector div label{margin-right:.3em}.userselector .userselector-infobelow{font-size:.8em}#userselector_options{padding:.3em 0}#userselector_options .collapsibleregioncaption{font-weight:bold}#userselector_options p{margin:.2em 0;text-align:left}.dir-rtl #userselector_options p{text-align:right}#page-user-profile .messagebox{text-align:center;margin-left:auto;margin-right:auto}#page-course-view-weeks .messagebox{text-align:center;margin-left:auto;margin-right:auto}.dir-rtl .userlist table#participants td,.dir-rtl .userlist table#participants th{text-align:right}.dir-rtl .userlist table#participants{margin:0 auto}#page-my-index.dir-rtl .block h3{text-align:right}.profileeditor>.singleselect{margin:0 .5em 0 0}.profileeditor>.singlebutton{display:inline-block;margin:0 0 0 .5em}.profileeditor>.singlebutton div,.profileeditor>.singlebutton input{margin:0}.dir-rtl .profileeditor>.singleselect{margin:0 0 0 .5em}.dir-rtl .profileeditor>.singlebutton{margin:0 .5em 0 0}#groupeditform .groups,#groupeditform .members{width:49%;float:left;text-align:left}.dir-rtl #groupeditform .groups,.dir-rtl #groupeditform .members{float:right;text-align:right}.preferences-group ul{list-style:none;margin-left:0;margin-right:0}.dir-rtl .preferences-group{float:right}.search-results .result{margin-left:0;margin-right:0}.dir-rtl .search-results .result{margin-right:15px;margin-left:0}.search-results .result .result-content{margin:7px 0}.search-results .result .filename{font-style:italic}.search-input-wrapper{margin:0 5px 0 2px;overflow:hidden;float:right;height:100%;width:16px;transition:width .5s ease,left .5s ease}.search-input-wrapper>div{float:left;margin:9px 0 10px 0}.dir-rtl .search-input-wrapper{margin:0 2px 0 5px;float:left}.dir-rtl .search-input-wrapper>div{float:right}.dir-rtl .search-input-wrapper>form{margin:7px 25px 7px 0}.search-input-wrapper>form{opacity:0;margin:7px 0 7px 25px;transition:opacity .5s ease-in-out}.search-input-wrapper>form>input{margin:0}.search-input-wrapper form.expanded{opacity:1}.search-input-wrapper.expanded{width:158px}/*! * Bootstrap v2.3.2 * * Copyright 2013 Twitter, Inc From bdb80f6a5fd505af6885a0fc820890737f7ce655 Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Mon, 11 Apr 2016 14:17:43 +1000 Subject: [PATCH 3/5] MDL-51603 tablelib: Delegate to dataformat for downloading --- lang/en/deprecated.txt | 6 ++ lang/en/table.php | 4 +- lib/tablelib.php | 155 ++++++++++++++++++++++++++++++----------- 3 files changed, 124 insertions(+), 41 deletions(-) diff --git a/lang/en/deprecated.txt b/lang/en/deprecated.txt index 7da297ae24b..159bc3b4608 100644 --- a/lang/en/deprecated.txt +++ b/lang/en/deprecated.txt @@ -30,3 +30,9 @@ settypeofficial,core_tag filetoolarge,core maxbytesforfile,core maxbytes,core_error +downloadcsv,core_table +downloadexcel,core_table +downloadods,core_table +downloadoptions,core_table +downloadtsv,core_table +downloadxhtml,core_table diff --git a/lang/en/table.php b/lang/en/table.php index 69a0a8e5047..8ab01a3e29e 100644 --- a/lang/en/table.php +++ b/lang/en/table.php @@ -22,7 +22,9 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -$string['downloadas'] = 'Download table data as {$a->formatsmenu} {$a->downloadbutton}'; +$string['downloadas'] = 'Download table data as'; + +// Deprecated since Moodle 3.1. $string['downloadcsv'] = 'Comma separated values text file'; $string['downloadexcel'] = 'Excel spreadsheet'; $string['downloadods'] = 'OpenDocument spreadsheet'; diff --git a/lib/tablelib.php b/lib/tablelib.php index e921805f064..7d4b5bc52d1 100644 --- a/lib/tablelib.php +++ b/lib/tablelib.php @@ -113,12 +113,6 @@ class flexible_table { */ var $downloadable = false; - /** - * @var string which download plugin to use. Default '' means none - print - * html table with paging. - */ - var $defaultdownloadformat = 'csv'; - /** * @var bool Has start output been called yet? */ @@ -157,10 +151,10 @@ class flexible_table { * for you (even if the param is '', which means no download this time. * Also you can call this method with no params to get the current set * download type. - * @param string $download download type. One of csv, tsv, xhtml, ods, etc + * @param string $download dataformat type. One of csv, xhtml, ods, etc * @param string $filename filename for downloads without file extension. * @param string $sheettitle title for downloaded data. - * @return string download type. One of csv, tsv, xhtml, ods, etc + * @return string download dataformat type. One of csv, xhtml, ods, etc */ function is_downloading($download = null, $filename='', $sheettitle='') { if ($download!==null) { @@ -184,8 +178,7 @@ class flexible_table { $this->exportclass = $exportclass; $this->exportclass->table = $this; } else if (is_null($this->exportclass) && !empty($this->download)) { - $classname = 'table_'.$this->download.'_export_format'; - $this->exportclass = new $classname($this); + $this->exportclass = new table_dataformat_export_format($this, $this->download); if (!$this->exportclass->document_started()) { $this->exportclass->start_document($this->filename); } @@ -1018,44 +1011,23 @@ class flexible_table { } return $row; } - /** - * This function is not part of the public api. - */ - function get_download_menu() { - $allclasses= get_declared_classes(); - $exportclasses = array(); - foreach ($allclasses as $class) { - $matches = array(); - if (preg_match('/^table\_([a-z]+)\_export\_format$/', $class, $matches)) { - $type = $matches[1]; - $exportclasses[$type]= get_string("download$type", 'table'); - } - } - return $exportclasses; - } /** - * This function is not part of the public api. + * Get the html for the download buttons + * + * Usually only use internally */ - function download_buttons() { + public function download_buttons() { + global $OUTPUT; + if ($this->is_downloadable() && !$this->is_downloading()) { - $downloadoptions = $this->get_download_menu(); - - $downloadelements = new stdClass(); - $downloadelements->formatsmenu = html_writer::select($downloadoptions, - 'download', $this->defaultdownloadformat, false); - $downloadelements->downloadbutton = ''; - $html = '
'; - $html .= '
'; - $html .= html_writer::tag('label', get_string('downloadas', 'table', $downloadelements)); - $html .= '
'; - - return $html; + return $OUTPUT->download_dataformat_selector(get_string('downloadas', 'table'), + $this->baseurl->out_omit_querystring(), 'download', $this->baseurl->params()); } else { return ''; } } + /** * This function is not part of the public api. * You don't normally need to call this. It is called automatically when @@ -1820,6 +1792,109 @@ class table_ods_export_format extends table_spreadsheet_export_format_parent { } } +/** + * Dataformat exporter + * + * @package core + * @subpackage tablelib + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class table_dataformat_export_format extends table_default_export_format_parent { + + /** @var $dataformat */ + protected $dataformat; + + /** @var $rownum */ + protected $rownum = 0; + + /** @var $columns */ + protected $columns; + + /** + * Constructor + * + * @param string $table An sql table + * @param string $dataformat type of dataformat for export + */ + public function __construct(&$table, $dataformat) { + parent::__construct($table); + + if (!NO_OUTPUT_BUFFERING) { + // Ideally this would throw a coding exception but we are more forgiving here due + // to the large amount of code already using table_sql. + debugging("NO_OUTPUT_BUFFERING should be set to true before downloading", DEBUG_DEVELOPER); + } + + $classname = 'dataformat_' . $dataformat . '\writer'; + if (!class_exists($classname)) { + throw new coding_exception("Unable to locate dataformat/$dataformat/classes/writer.php"); + } + $this->dataformat = new $classname; + + // The dataformat export time to first byte could take a while to generate... + set_time_limit(0); + + // Close the session so that the users other tabs in the same session are not blocked. + \core\session\manager::write_close(); + } + + /** + * Start document + * + * @param string $filename + */ + public function start_document($filename) { + $this->filename = $filename; + $this->documentstarted = true; + $this->dataformat->set_filename($filename); + $this->dataformat->send_http_headers(); + } + + /** + * Start export + * + * @param string $sheettitle optional spreadsheet worksheet title + */ + public function start_table($sheettitle) { + $this->dataformat->set_sheettitle($sheettitle); + } + + /** + * Output headers + * + * @param array $headers + */ + public function output_headers($headers) { + $this->columns = $headers; + $this->dataformat->write_header($headers); + } + + /** + * Add a row of data + * + * @param array $row One record of data + */ + public function add_data($row) { + $this->dataformat->write_record($row, $this->rownum++); + return true; + } + + /** + * Finish export + */ + public function finish_table() { + $this->dataformat->write_footer($this->columns); + } + + /** + * Finish download + */ + public function finish_document() { + exit; + } + +} /** * @package moodlecore From 5684cc2d1e39a48673339255b28fb8020610d9cf Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Tue, 19 Apr 2016 23:31:58 +1000 Subject: [PATCH 4/5] MDL-51603 tablelib: Removed legacy export classes --- lib/tablelib.php | 324 ----------------------------------------------- 1 file changed, 324 deletions(-) diff --git a/lib/tablelib.php b/lib/tablelib.php index 7d4b5bc52d1..d33942c3ce1 100644 --- a/lib/tablelib.php +++ b/lib/tablelib.php @@ -1676,122 +1676,6 @@ class table_default_export_format_parent { } } - -/** - * @package moodlecore - * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class table_spreadsheet_export_format_parent extends table_default_export_format_parent { - var $currentrow; - var $workbook; - var $worksheet; - /** - * @var object format object - format for normal table cells - */ - var $formatnormal; - /** - * @var object format object - format for header table cells - */ - var $formatheaders; - - /** - * should be overriden in child class. - */ - var $fileextension; - - /** - * This method will be overridden in the child class. - */ - function define_workbook() { - } - - function start_document($filename) { - $filename = $filename.'.'.$this->fileextension; - $this->define_workbook(); - // format types - $this->formatnormal = $this->workbook->add_format(); - $this->formatnormal->set_bold(0); - $this->formatheaders = $this->workbook->add_format(); - $this->formatheaders->set_bold(1); - $this->formatheaders->set_align('center'); - // Sending HTTP headers - $this->workbook->send($filename); - $this->documentstarted = true; - } - - function start_table($sheettitle) { - $this->worksheet = $this->workbook->add_worksheet($sheettitle); - $this->currentrow=0; - } - - function output_headers($headers) { - $colnum = 0; - foreach ($headers as $item) { - $this->worksheet->write($this->currentrow,$colnum,$item,$this->formatheaders); - $colnum++; - } - $this->currentrow++; - } - - function add_data($row) { - $colnum = 0; - foreach ($row as $item) { - $this->worksheet->write($this->currentrow,$colnum,$item,$this->formatnormal); - $colnum++; - } - $this->currentrow++; - return true; - } - - function add_seperator() { - $this->currentrow++; - return true; - } - - function finish_table() { - } - - function finish_document() { - $this->workbook->close(); - exit; - } -} - - -/** - * @package moodlecore - * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class table_excel_export_format extends table_spreadsheet_export_format_parent { - var $fileextension = 'xls'; - - function define_workbook() { - global $CFG; - require_once("$CFG->libdir/excellib.class.php"); - // Creating a workbook - $this->workbook = new MoodleExcelWorkbook("-"); - } - -} - - -/** - * @package moodlecore - * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class table_ods_export_format extends table_spreadsheet_export_format_parent { - var $fileextension = 'ods'; - function define_workbook() { - global $CFG; - require_once("$CFG->libdir/odslib.class.php"); - // Creating a workbook - $this->workbook = new MoodleODSWorkbook("-"); - } -} - /** * Dataformat exporter * @@ -1896,211 +1780,3 @@ class table_dataformat_export_format extends table_default_export_format_parent } -/** - * @package moodlecore - * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class table_text_export_format_parent extends table_default_export_format_parent { - protected $seperator = "tab"; - protected $mimetype = 'text/tab-separated-values'; - protected $ext = '.txt'; - protected $myexporter; - - public function __construct() { - $this->myexporter = new csv_export_writer($this->seperator, '"', $this->mimetype); - } - - public function start_document($filename) { - $this->filename = $filename; - $this->documentstarted = true; - $this->myexporter->set_filename($filename, $this->ext); - } - - public function start_table($sheettitle) { - //nothing to do here - } - - public function output_headers($headers) { - $this->myexporter->add_data($headers); - } - - public function add_data($row) { - $this->myexporter->add_data($row); - return true; - } - - public function finish_table() { - //nothing to do here - } - - public function finish_document() { - $this->myexporter->download_file(); - exit; - } - - /** - * Format a row of data. - * @param array $data - */ - protected function format_row($data) { - $escapeddata = array(); - foreach ($data as $value) { - $escapeddata[] = '"' . str_replace('"', '""', $value) . '"'; - } - return implode($this->seperator, $escapeddata) . "\n"; - } -} - - -/** - * @package moodlecore - * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class table_tsv_export_format extends table_text_export_format_parent { - protected $seperator = "tab"; - protected $mimetype = 'text/tab-separated-values'; - protected $ext = '.txt'; -} - -require_once($CFG->libdir . '/csvlib.class.php'); -/** - * @package moodlecore - * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class table_csv_export_format extends table_text_export_format_parent { - protected $seperator = "comma"; - protected $mimetype = 'text/csv'; - protected $ext = '.csv'; -} - -/** - * @package moodlecore - * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class table_xhtml_export_format extends table_default_export_format_parent { - function start_document($filename) { - header("Content-Type: application/download\n"); - header("Content-Disposition: attachment; filename=\"$filename.html\""); - header("Expires: 0"); - header("Cache-Control: must-revalidate,post-check=0,pre-check=0"); - header("Pragma: public"); - //html headers - echo << - - - - - - -$filename - - -EOF; - $this->documentstarted = true; - } - - function start_table($sheettitle) { - $this->table->sortable(false); - $this->table->collapsible(false); - echo "

{$sheettitle}

"; - $this->table->start_html(); - } - - function output_headers($headers) { - $this->table->print_headers(); - echo html_writer::start_tag('tbody'); - } - - function add_data($row) { - $this->table->print_row($row); - return true; - } - - function add_seperator() { - $this->table->print_row(NULL); - return true; - } - - function finish_table() { - $this->table->finish_html(); - } - - function finish_document() { - echo "\n"; - exit; - } - - function format_text($text, $format=FORMAT_MOODLE, $options=NULL, $courseid=NULL) { - if (is_null($options)) { - $options = new stdClass; - } - //some sensible defaults - if (!isset($options->para)) { - $options->para = false; - } - if (!isset($options->newlines)) { - $options->newlines = false; - } - if (!isset($options->smiley)) { - $options->smiley = false; - } - if (!isset($options->filter)) { - $options->filter = false; - } - return format_text($text, $format, $options); - } -} From 1e29afd4ea9443a7b53a582fc215608d62d4b610 Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Mon, 11 Apr 2016 14:18:18 +1000 Subject: [PATCH 5/5] MDL-51603 report_log: Allow streaming downloads --- report/log/index.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/report/log/index.php b/report/log/index.php index d5e00ee29df..79bcff15520 100644 --- a/report/log/index.php +++ b/report/log/index.php @@ -22,6 +22,10 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +if (!empty($_GET['download'])) { + define('NO_OUTPUT_BUFFERING', true); +} + require('../../config.php'); require_once($CFG->dirroot.'/course/lib.php'); require_once($CFG->dirroot.'/report/log/locallib.php');