Merge branch 'MDL-65538-master' of git://github.com/sarjona/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Wed, 17 Jul 2019 02:55:43 +0000 (10:55 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Wed, 17 Jul 2019 02:55:43 +0000 (10:55 +0800)
451 files changed:
analytics/classes/local/target/base.php
analytics/tests/privacy_test.php
badges/award.php
badges/criteria/award_criteria_badge.php
badges/lib/awardlib.php
badges/tests/behat/award_badge_groups.feature [new file with mode: 0644]
calendar/amd/build/calendar_threemonth.min.js
calendar/amd/src/calendar_threemonth.js
dataformat/pdf/classes/writer.php
files/converter/googledrive/classes/converter.php
lib/adminlib.php
lib/ajax/service-nologin.php
lib/ajax/service.php
lib/amd/build/ajax.min.js
lib/amd/build/checkbox-toggleall.min.js
lib/amd/build/icon_system_fontawesome.min.js
lib/amd/build/modal_confirm.min.js [deleted file]
lib/amd/build/modal_events.min.js
lib/amd/build/modal_factory.min.js
lib/amd/build/str.min.js
lib/amd/build/templates.min.js
lib/amd/src/ajax.js
lib/amd/src/checkbox-toggleall.js
lib/amd/src/icon_system_fontawesome.js
lib/amd/src/modal_confirm.js [deleted file]
lib/amd/src/modal_events.js
lib/amd/src/modal_factory.js
lib/amd/src/str.js
lib/amd/src/templates.js
lib/classes/component.php
lib/classes/dataformat/spout_base.php
lib/classes/output/checkbox_toggleall.php [new file with mode: 0644]
lib/classes/scss.php
lib/maxmind/GeoIp2/Record/Country.php
lib/maxmind/GeoIp2/Record/RepresentedCountry.php
lib/maxmind/GeoIp2/Record/Traits.php
lib/maxmind/GeoIp2/WebService/Client.php
lib/maxmind/MaxMind/Db/Reader.php
lib/maxmind/MaxMind/Db/Reader/Decoder.php
lib/maxmind/MaxMind/Db/Reader/Metadata.php
lib/maxmind/MaxMind/Db/Reader/Util.php
lib/maxmind/readme_moodle.txt
lib/minify/matthiasmullie-minify/LICENSE [new file with mode: 0755]
lib/minify/matthiasmullie-minify/src/CSS.php
lib/minify/matthiasmullie-minify/src/Exception.php
lib/minify/matthiasmullie-minify/src/Exceptions/BasicException.php
lib/minify/matthiasmullie-minify/src/Exceptions/FileImportException.php
lib/minify/matthiasmullie-minify/src/Exceptions/IOException.php
lib/minify/matthiasmullie-minify/src/JS.php
lib/minify/matthiasmullie-minify/src/Minify.php
lib/minify/matthiasmullie-pathconverter/LICENSE [new file with mode: 0644]
lib/minify/matthiasmullie-pathconverter/src/Converter.php
lib/mlbackend/php/classes/processor.php
lib/mlbackend/php/phpml/LICENSE
lib/mlbackend/php/phpml/readme_moodle.txt
lib/mlbackend/php/phpml/src/Phpml/Association/Apriori.php
lib/mlbackend/php/phpml/src/Phpml/Classification/DecisionTree.php
lib/mlbackend/php/phpml/src/Phpml/Classification/DecisionTree/DecisionTreeLeaf.php
lib/mlbackend/php/phpml/src/Phpml/Classification/Ensemble/AdaBoost.php
lib/mlbackend/php/phpml/src/Phpml/Classification/Ensemble/Bagging.php
lib/mlbackend/php/phpml/src/Phpml/Classification/Ensemble/RandomForest.php
lib/mlbackend/php/phpml/src/Phpml/Classification/KNearestNeighbors.php
lib/mlbackend/php/phpml/src/Phpml/Classification/Linear/Adaline.php
lib/mlbackend/php/phpml/src/Phpml/Classification/Linear/DecisionStump.php
lib/mlbackend/php/phpml/src/Phpml/Classification/Linear/LogisticRegression.php
lib/mlbackend/php/phpml/src/Phpml/Classification/Linear/Perceptron.php
lib/mlbackend/php/phpml/src/Phpml/Classification/MLPClassifier.php
lib/mlbackend/php/phpml/src/Phpml/Classification/NaiveBayes.php
lib/mlbackend/php/phpml/src/Phpml/Classification/SVC.php
lib/mlbackend/php/phpml/src/Phpml/Classification/WeightedClassifier.php
lib/mlbackend/php/phpml/src/Phpml/Clustering/Clusterer.php
lib/mlbackend/php/phpml/src/Phpml/Clustering/DBSCAN.php
lib/mlbackend/php/phpml/src/Phpml/Clustering/FuzzyCMeans.php
lib/mlbackend/php/phpml/src/Phpml/Clustering/KMeans.php
lib/mlbackend/php/phpml/src/Phpml/Clustering/KMeans/Cluster.php
lib/mlbackend/php/phpml/src/Phpml/Clustering/KMeans/Point.php
lib/mlbackend/php/phpml/src/Phpml/Clustering/KMeans/Space.php
lib/mlbackend/php/phpml/src/Phpml/CrossValidation/RandomSplit.php
lib/mlbackend/php/phpml/src/Phpml/CrossValidation/Split.php
lib/mlbackend/php/phpml/src/Phpml/CrossValidation/StratifiedRandomSplit.php
lib/mlbackend/php/phpml/src/Phpml/Dataset/ArrayDataset.php
lib/mlbackend/php/phpml/src/Phpml/Dataset/CsvDataset.php
lib/mlbackend/php/phpml/src/Phpml/Dataset/Dataset.php
lib/mlbackend/php/phpml/src/Phpml/Dataset/Demo/GlassDataset.php
lib/mlbackend/php/phpml/src/Phpml/Dataset/Demo/IrisDataset.php
lib/mlbackend/php/phpml/src/Phpml/Dataset/Demo/WineDataset.php
lib/mlbackend/php/phpml/src/Phpml/Dataset/FilesDataset.php
lib/mlbackend/php/phpml/src/Phpml/Dataset/MnistDataset.php [new file with mode: 0644]
lib/mlbackend/php/phpml/src/Phpml/Dataset/SvmDataset.php [new file with mode: 0644]
lib/mlbackend/php/phpml/src/Phpml/DimensionReduction/EigenTransformerBase.php
lib/mlbackend/php/phpml/src/Phpml/DimensionReduction/KernelPCA.php
lib/mlbackend/php/phpml/src/Phpml/DimensionReduction/LDA.php
lib/mlbackend/php/phpml/src/Phpml/DimensionReduction/PCA.php
lib/mlbackend/php/phpml/src/Phpml/Estimator.php
lib/mlbackend/php/phpml/src/Phpml/Exception/DatasetException.php
lib/mlbackend/php/phpml/src/Phpml/Exception/FileException.php
lib/mlbackend/php/phpml/src/Phpml/Exception/InvalidArgumentException.php
lib/mlbackend/php/phpml/src/Phpml/Exception/InvalidOperationException.php [new file with mode: 0644]
lib/mlbackend/php/phpml/src/Phpml/Exception/LibsvmCommandException.php [new file with mode: 0644]
lib/mlbackend/php/phpml/src/Phpml/Exception/MatrixException.php
lib/mlbackend/php/phpml/src/Phpml/Exception/NormalizerException.php
lib/mlbackend/php/phpml/src/Phpml/Exception/SerializeException.php
lib/mlbackend/php/phpml/src/Phpml/FeatureExtraction/StopWords.php
lib/mlbackend/php/phpml/src/Phpml/FeatureExtraction/TfIdfTransformer.php
lib/mlbackend/php/phpml/src/Phpml/FeatureExtraction/TokenCountVectorizer.php
lib/mlbackend/php/phpml/src/Phpml/FeatureSelection/ScoringFunction.php [new file with mode: 0644]
lib/mlbackend/php/phpml/src/Phpml/FeatureSelection/ScoringFunction/ANOVAFValue.php [new file with mode: 0644]
lib/mlbackend/php/phpml/src/Phpml/FeatureSelection/ScoringFunction/UnivariateLinearRegression.php [new file with mode: 0644]
lib/mlbackend/php/phpml/src/Phpml/FeatureSelection/SelectKBest.php [new file with mode: 0644]
lib/mlbackend/php/phpml/src/Phpml/FeatureSelection/VarianceThreshold.php [new file with mode: 0644]
lib/mlbackend/php/phpml/src/Phpml/Helper/OneVsRest.php
lib/mlbackend/php/phpml/src/Phpml/Helper/Optimizer/ConjugateGradient.php
lib/mlbackend/php/phpml/src/Phpml/Helper/Optimizer/GD.php
lib/mlbackend/php/phpml/src/Phpml/Helper/Optimizer/Optimizer.php
lib/mlbackend/php/phpml/src/Phpml/Helper/Optimizer/StochasticGD.php
lib/mlbackend/php/phpml/src/Phpml/Helper/Predictable.php
lib/mlbackend/php/phpml/src/Phpml/Helper/Trainable.php
lib/mlbackend/php/phpml/src/Phpml/IncrementalEstimator.php
lib/mlbackend/php/phpml/src/Phpml/Math/Comparison.php [new file with mode: 0644]
lib/mlbackend/php/phpml/src/Phpml/Math/Distance.php
lib/mlbackend/php/phpml/src/Phpml/Math/Distance/Chebyshev.php
lib/mlbackend/php/phpml/src/Phpml/Math/Distance/Distance.php [new file with mode: 0644]
lib/mlbackend/php/phpml/src/Phpml/Math/Distance/Euclidean.php
lib/mlbackend/php/phpml/src/Phpml/Math/Distance/Manhattan.php
lib/mlbackend/php/phpml/src/Phpml/Math/Distance/Minkowski.php
lib/mlbackend/php/phpml/src/Phpml/Math/Kernel.php
lib/mlbackend/php/phpml/src/Phpml/Math/Kernel/RBF.php
lib/mlbackend/php/phpml/src/Phpml/Math/LinearAlgebra/EigenvalueDecomposition.php
lib/mlbackend/php/phpml/src/Phpml/Math/LinearAlgebra/LUDecomposition.php
lib/mlbackend/php/phpml/src/Phpml/Math/Matrix.php
lib/mlbackend/php/phpml/src/Phpml/Math/Product.php
lib/mlbackend/php/phpml/src/Phpml/Math/Set.php
lib/mlbackend/php/phpml/src/Phpml/Math/Statistic/ANOVA.php [new file with mode: 0644]
lib/mlbackend/php/phpml/src/Phpml/Math/Statistic/Correlation.php
lib/mlbackend/php/phpml/src/Phpml/Math/Statistic/Covariance.php
lib/mlbackend/php/phpml/src/Phpml/Math/Statistic/Gaussian.php
lib/mlbackend/php/phpml/src/Phpml/Math/Statistic/Mean.php
lib/mlbackend/php/phpml/src/Phpml/Math/Statistic/StandardDeviation.php
lib/mlbackend/php/phpml/src/Phpml/Math/Statistic/Variance.php [new file with mode: 0644]
lib/mlbackend/php/phpml/src/Phpml/Metric/Accuracy.php
lib/mlbackend/php/phpml/src/Phpml/Metric/ClassificationReport.php
lib/mlbackend/php/phpml/src/Phpml/Metric/ConfusionMatrix.php
lib/mlbackend/php/phpml/src/Phpml/ModelManager.php
lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/ActivationFunction.php
lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/ActivationFunction/BinaryStep.php
lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/ActivationFunction/Gaussian.php
lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/ActivationFunction/HyperbolicTangent.php
lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/ActivationFunction/PReLU.php [new file with mode: 0644]
lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/ActivationFunction/Sigmoid.php
lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/ActivationFunction/ThresholdedReLU.php [new file with mode: 0644]
lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Layer.php
lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Network.php
lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Network/LayeredNetwork.php
lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Network/MultilayerPerceptron.php
lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Node.php
lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Node/Bias.php
lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Node/Input.php
lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Node/Neuron.php
lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Node/Neuron/Synapse.php
lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Training/Backpropagation.php
lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Training/Backpropagation/Sigma.php
lib/mlbackend/php/phpml/src/Phpml/Pipeline.php
lib/mlbackend/php/phpml/src/Phpml/Preprocessing/Imputer.php
lib/mlbackend/php/phpml/src/Phpml/Preprocessing/Imputer/Strategy.php
lib/mlbackend/php/phpml/src/Phpml/Preprocessing/Imputer/Strategy/MeanStrategy.php
lib/mlbackend/php/phpml/src/Phpml/Preprocessing/Imputer/Strategy/MedianStrategy.php
lib/mlbackend/php/phpml/src/Phpml/Preprocessing/Imputer/Strategy/MostFrequentStrategy.php
lib/mlbackend/php/phpml/src/Phpml/Preprocessing/Normalizer.php
lib/mlbackend/php/phpml/src/Phpml/Regression/LeastSquares.php
lib/mlbackend/php/phpml/src/Phpml/Regression/SVR.php
lib/mlbackend/php/phpml/src/Phpml/SupportVectorMachine/DataTransformer.php
lib/mlbackend/php/phpml/src/Phpml/SupportVectorMachine/Kernel.php
lib/mlbackend/php/phpml/src/Phpml/SupportVectorMachine/SupportVectorMachine.php
lib/mlbackend/php/phpml/src/Phpml/SupportVectorMachine/Type.php
lib/mlbackend/php/phpml/src/Phpml/Tokenization/NGramTokenizer.php [new file with mode: 0644]
lib/mlbackend/php/phpml/src/Phpml/Tokenization/Tokenizer.php
lib/mlbackend/php/phpml/src/Phpml/Tokenization/WhitespaceTokenizer.php
lib/mlbackend/php/phpml/src/Phpml/Tokenization/WordTokenizer.php
lib/mlbackend/php/phpml/src/Phpml/Transformer.php
lib/mlbackend/php/thirdpartylibs.xml
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/scssphp/Base/Range.php
lib/scssphp/Block.php
lib/scssphp/Cache.php [new file with mode: 0644]
lib/scssphp/Colors.php
lib/scssphp/Compiler.php
lib/scssphp/Compiler/Environment.php
lib/scssphp/Exception/CompilerException.php
lib/scssphp/Exception/ParserException.php
lib/scssphp/Exception/RangeException.php
lib/scssphp/Exception/ServerException.php
lib/scssphp/Formatter.php
lib/scssphp/Formatter/Compact.php
lib/scssphp/Formatter/Compressed.php
lib/scssphp/Formatter/Crunched.php
lib/scssphp/Formatter/Debug.php
lib/scssphp/Formatter/Expanded.php
lib/scssphp/Formatter/Nested.php
lib/scssphp/Formatter/OutputBlock.php
lib/scssphp/LICENSE.md
lib/scssphp/Node.php
lib/scssphp/Node/Number.php
lib/scssphp/Parser.php
lib/scssphp/SourceMap/Base64.php [new file with mode: 0644]
lib/scssphp/SourceMap/Base64VLQ.php [new file with mode: 0644]
lib/scssphp/SourceMap/Base64VLQEncoder.php
lib/scssphp/SourceMap/SourceMapGenerator.php
lib/scssphp/Type.php
lib/scssphp/Util.php
lib/scssphp/Version.php
lib/scssphp/moodle_readme.txt
lib/simplepie/library/SimplePie.php [changed mode: 0644->0755]
lib/simplepie/library/SimplePie/Author.php
lib/simplepie/library/SimplePie/Cache/File.php
lib/simplepie/library/SimplePie/Cache/Memcached.php [changed mode: 0644->0755]
lib/simplepie/library/SimplePie/Cache/MySQL.php
lib/simplepie/library/SimplePie/Cache/Redis.php
lib/simplepie/library/SimplePie/Caption.php
lib/simplepie/library/SimplePie/Content/Type/Sniffer.php
lib/simplepie/library/SimplePie/Copyright.php
lib/simplepie/library/SimplePie/Credit.php
lib/simplepie/library/SimplePie/Decode/HTML/Entities.php
lib/simplepie/library/SimplePie/Enclosure.php
lib/simplepie/library/SimplePie/File.php
lib/simplepie/library/SimplePie/HTTP/Parser.php
lib/simplepie/library/SimplePie/IRI.php
lib/simplepie/library/SimplePie/Item.php
lib/simplepie/library/SimplePie/Locator.php
lib/simplepie/library/SimplePie/Misc.php
lib/simplepie/library/SimplePie/Net/IPv6.php
lib/simplepie/library/SimplePie/Parse/Date.php
lib/simplepie/library/SimplePie/Parser.php
lib/simplepie/library/SimplePie/Rating.php
lib/simplepie/library/SimplePie/Registry.php [changed mode: 0644->0755]
lib/simplepie/library/SimplePie/Restriction.php
lib/simplepie/library/SimplePie/Sanitize.php
lib/simplepie/library/SimplePie/Source.php
lib/simplepie/library/SimplePie/XML/Declaration/Parser.php
lib/simplepie/library/SimplePie/gzdecode.php
lib/simplepie/readme_moodle.txt
lib/spout/README.md
lib/spout/readme_moodle.txt
lib/spout/src/Spout/Autoloader/Psr4Autoloader.php
lib/spout/src/Spout/Autoloader/autoload.php
lib/spout/src/Spout/Common/Creator/HelperFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Entity/Cell.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Entity/Row.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Entity/Style/Border.php [moved from lib/spout/src/Spout/Writer/Style/Border.php with 79% similarity]
lib/spout/src/Spout/Common/Entity/Style/BorderPart.php [moved from lib/spout/src/Spout/Writer/Style/BorderPart.php with 98% similarity]
lib/spout/src/Spout/Common/Entity/Style/Color.php [moved from lib/spout/src/Spout/Writer/Style/Color.php with 92% similarity]
lib/spout/src/Spout/Common/Entity/Style/Style.php [moved from lib/spout/src/Spout/Writer/Style/Style.php with 57% similarity]
lib/spout/src/Spout/Common/Exception/EncodingConversionException.php
lib/spout/src/Spout/Common/Exception/IOException.php
lib/spout/src/Spout/Common/Exception/InvalidArgumentException.php
lib/spout/src/Spout/Common/Exception/InvalidColorException.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Exception/SpoutException.php
lib/spout/src/Spout/Common/Exception/UnsupportedTypeException.php
lib/spout/src/Spout/Common/Helper/CellTypeHelper.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Helper/EncodingHelper.php
lib/spout/src/Spout/Common/Helper/Escaper/CSV.php [moved from lib/spout/src/Spout/Common/Escaper/CSV.php with 90% similarity]
lib/spout/src/Spout/Common/Helper/Escaper/EscaperInterface.php [moved from lib/spout/src/Spout/Common/Escaper/EscaperInterface.php with 87% similarity]
lib/spout/src/Spout/Common/Helper/Escaper/ODS.php [moved from lib/spout/src/Spout/Common/Escaper/ODS.php with 80% similarity]
lib/spout/src/Spout/Common/Helper/Escaper/XLSX.php [moved from lib/spout/src/Spout/Common/Escaper/XLSX.php with 78% similarity]
lib/spout/src/Spout/Common/Helper/FileSystemHelper.php
lib/spout/src/Spout/Common/Helper/FileSystemHelperInterface.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Helper/GlobalFunctionsHelper.php
lib/spout/src/Spout/Common/Helper/StringHelper.php
lib/spout/src/Spout/Common/Manager/OptionsManagerAbstract.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Manager/OptionsManagerInterface.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Singleton.php [deleted file]
lib/spout/src/Spout/Common/Type.php
lib/spout/src/Spout/Reader/CSV/Creator/InternalEntityFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/CSV/Manager/OptionsManager.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/CSV/Reader.php
lib/spout/src/Spout/Reader/CSV/ReaderOptions.php [deleted file]
lib/spout/src/Spout/Reader/CSV/RowIterator.php
lib/spout/src/Spout/Reader/CSV/Sheet.php
lib/spout/src/Spout/Reader/CSV/SheetIterator.php
lib/spout/src/Spout/Reader/Common/Creator/InternalEntityFactoryInterface.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/Common/Creator/ReaderEntityFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/Common/Creator/ReaderFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/Common/Entity/Options.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/Common/Manager/RowManager.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/Common/ReaderOptions.php [deleted file]
lib/spout/src/Spout/Reader/Common/XMLProcessor.php
lib/spout/src/Spout/Reader/Exception/InvalidValueException.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/Exception/IteratorNotRewindableException.php
lib/spout/src/Spout/Reader/Exception/NoSheetsFoundException.php
lib/spout/src/Spout/Reader/Exception/ReaderException.php
lib/spout/src/Spout/Reader/Exception/ReaderNotOpenedException.php
lib/spout/src/Spout/Reader/Exception/SharedStringNotFoundException.php
lib/spout/src/Spout/Reader/Exception/XMLProcessingException.php
lib/spout/src/Spout/Reader/IteratorInterface.php
lib/spout/src/Spout/Reader/ODS/Creator/HelperFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/ODS/Creator/InternalEntityFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/ODS/Creator/ManagerFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/ODS/Helper/CellValueFormatter.php
lib/spout/src/Spout/Reader/ODS/Helper/SettingsHelper.php
lib/spout/src/Spout/Reader/ODS/Manager/OptionsManager.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/ODS/Reader.php
lib/spout/src/Spout/Reader/ODS/ReaderOptions.php [deleted file]
lib/spout/src/Spout/Reader/ODS/RowIterator.php
lib/spout/src/Spout/Reader/ODS/Sheet.php
lib/spout/src/Spout/Reader/ODS/SheetIterator.php
lib/spout/src/Spout/Reader/ReaderAbstract.php [moved from lib/spout/src/Spout/Reader/AbstractReader.php with 82% similarity]
lib/spout/src/Spout/Reader/ReaderFactory.php [deleted file]
lib/spout/src/Spout/Reader/ReaderInterface.php
lib/spout/src/Spout/Reader/SheetInterface.php
lib/spout/src/Spout/Reader/Wrapper/XMLInternalErrorsHelper.php
lib/spout/src/Spout/Reader/Wrapper/XMLReader.php
lib/spout/src/Spout/Reader/XLSX/Creator/HelperFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/Creator/InternalEntityFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/Creator/ManagerFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/Helper/CellHelper.php
lib/spout/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php
lib/spout/src/Spout/Reader/XLSX/Helper/DateFormatHelper.php
lib/spout/src/Spout/Reader/XLSX/Helper/SheetHelper.php [deleted file]
lib/spout/src/Spout/Reader/XLSX/Manager/OptionsManager.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/Manager/SharedStringsCaching/CachingStrategyFactory.php [moved from lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyFactory.php with 80% similarity]
lib/spout/src/Spout/Reader/XLSX/Manager/SharedStringsCaching/CachingStrategyInterface.php [moved from lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyInterface.php with 90% similarity]
lib/spout/src/Spout/Reader/XLSX/Manager/SharedStringsCaching/FileBasedStrategy.php [moved from lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/FileBasedStrategy.php with 90% similarity]
lib/spout/src/Spout/Reader/XLSX/Manager/SharedStringsCaching/InMemoryStrategy.php [moved from lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/InMemoryStrategy.php with 95% similarity]
lib/spout/src/Spout/Reader/XLSX/Manager/SharedStringsManager.php [moved from lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php with 74% similarity]
lib/spout/src/Spout/Reader/XLSX/Manager/SheetManager.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/Manager/StyleManager.php [moved from lib/spout/src/Spout/Reader/XLSX/Helper/StyleHelper.php with 88% similarity]
lib/spout/src/Spout/Reader/XLSX/Manager/WorkbookRelationshipsManager.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/Reader.php
lib/spout/src/Spout/Reader/XLSX/ReaderOptions.php [deleted file]
lib/spout/src/Spout/Reader/XLSX/RowIterator.php
lib/spout/src/Spout/Reader/XLSX/Sheet.php
lib/spout/src/Spout/Reader/XLSX/SheetIterator.php
lib/spout/src/Spout/Writer/AbstractMultiSheetsWriter.php [deleted file]
lib/spout/src/Spout/Writer/AbstractWriter.php [deleted file]
lib/spout/src/Spout/Writer/CSV/Manager/OptionsManager.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/CSV/Writer.php
lib/spout/src/Spout/Writer/Common/Creator/InternalEntityFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Creator/ManagerFactoryInterface.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Creator/Style/BorderBuilder.php [moved from lib/spout/src/Spout/Writer/Style/BorderBuilder.php with 60% similarity]
lib/spout/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php [moved from lib/spout/src/Spout/Writer/Style/StyleBuilder.php with 93% similarity]
lib/spout/src/Spout/Writer/Common/Creator/WriterEntityFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Creator/WriterFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Entity/Options.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Entity/Sheet.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Entity/Workbook.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Entity/Worksheet.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Helper/AbstractStyleHelper.php [deleted file]
lib/spout/src/Spout/Writer/Common/Helper/CellHelper.php
lib/spout/src/Spout/Writer/Common/Helper/FileSystemWithRootFolderHelperInterface.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Helper/ZipHelper.php
lib/spout/src/Spout/Writer/Common/Internal/AbstractWorkbook.php [deleted file]
lib/spout/src/Spout/Writer/Common/Internal/WorkbookInterface.php [deleted file]
lib/spout/src/Spout/Writer/Common/Internal/WorksheetInterface.php [deleted file]
lib/spout/src/Spout/Writer/Common/Manager/CellManager.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Manager/RowManager.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Manager/SheetManager.php [moved from lib/spout/src/Spout/Writer/Common/Sheet.php with 57% similarity]
lib/spout/src/Spout/Writer/Common/Manager/Style/StyleManager.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Manager/Style/StyleManagerInterface.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Manager/Style/StyleMerger.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Manager/Style/StyleRegistry.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Manager/WorkbookManagerInterface.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Manager/WorksheetManagerInterface.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Exception/Border/InvalidNameException.php
lib/spout/src/Spout/Writer/Exception/Border/InvalidStyleException.php
lib/spout/src/Spout/Writer/Exception/Border/InvalidWidthException.php
lib/spout/src/Spout/Writer/Exception/InvalidColorException.php [deleted file]
lib/spout/src/Spout/Writer/Exception/InvalidSheetNameException.php
lib/spout/src/Spout/Writer/Exception/SheetNotFoundException.php
lib/spout/src/Spout/Writer/Exception/WriterAlreadyOpenedException.php
lib/spout/src/Spout/Writer/Exception/WriterException.php
lib/spout/src/Spout/Writer/Exception/WriterNotOpenedException.php
lib/spout/src/Spout/Writer/ODS/Creator/HelperFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/ODS/Creator/ManagerFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/ODS/Helper/BorderHelper.php
lib/spout/src/Spout/Writer/ODS/Helper/FileSystemHelper.php
lib/spout/src/Spout/Writer/ODS/Internal/Workbook.php [deleted file]
lib/spout/src/Spout/Writer/ODS/Internal/Worksheet.php [deleted file]
lib/spout/src/Spout/Writer/ODS/Manager/OptionsManager.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/ODS/Manager/Style/StyleManager.php [moved from lib/spout/src/Spout/Writer/ODS/Helper/StyleHelper.php with 84% similarity]
lib/spout/src/Spout/Writer/ODS/Manager/Style/StyleRegistry.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/ODS/Manager/WorkbookManager.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/ODS/Manager/WorksheetManager.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/ODS/Writer.php
lib/spout/src/Spout/Writer/WriterAbstract.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/WriterFactory.php [deleted file]
lib/spout/src/Spout/Writer/WriterInterface.php
lib/spout/src/Spout/Writer/WriterMultiSheetsAbstract.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/XLSX/Creator/HelperFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/XLSX/Creator/ManagerFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/XLSX/Helper/BorderHelper.php
lib/spout/src/Spout/Writer/XLSX/Helper/FileSystemHelper.php
lib/spout/src/Spout/Writer/XLSX/Internal/Workbook.php [deleted file]
lib/spout/src/Spout/Writer/XLSX/Internal/Worksheet.php [deleted file]
lib/spout/src/Spout/Writer/XLSX/Manager/OptionsManager.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/XLSX/Manager/SharedStringsManager.php [moved from lib/spout/src/Spout/Writer/XLSX/Helper/SharedStringsHelper.php with 86% similarity]
lib/spout/src/Spout/Writer/XLSX/Manager/Style/StyleManager.php [moved from lib/spout/src/Spout/Writer/XLSX/Helper/StyleHelper.php with 53% similarity]
lib/spout/src/Spout/Writer/XLSX/Manager/Style/StyleRegistry.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/XLSX/Manager/WorkbookManager.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/XLSX/Manager/WorksheetManager.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/XLSX/Writer.php
lib/templates/checkbox-toggleall-master-button.mustache [new file with mode: 0644]
lib/templates/checkbox-toggleall-master.mustache [new file with mode: 0644]
lib/templates/checkbox-toggleall-slave.mustache [new file with mode: 0644]
lib/tests/minify_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/annotationstamp.js
mod/assign/feedback/editpdf/yui/src/editor/js/comment.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
mod/assign/lib.php
mod/assign/tests/lib_test.php
mod/chat/lib.php
mod/chat/tests/lib_test.php
mod/choice/amd/build/select_all_choices.min.js [deleted file]
mod/choice/amd/src/select_all_choices.js [deleted file]
mod/choice/lang/en/choice.php
mod/choice/lib.php
mod/choice/renderer.php
mod/choice/tests/lib_test.php
mod/feedback/lib.php
mod/feedback/tests/lib_test.php
mod/lesson/backup/moodle2/backup_lesson_stepslib.php
mod/lesson/backup/moodle2/restore_lesson_stepslib.php
mod/lesson/db/upgrade.php
mod/lesson/lib.php
mod/lesson/tests/lib_test.php
mod/lesson/tests/restore_override_test.php [new file with mode: 0644]
mod/lesson/version.php
mod/quiz/backup/moodle2/backup_quiz_stepslib.php
mod/quiz/backup/moodle2/restore_quiz_stepslib.php
mod/quiz/db/upgrade.php
mod/quiz/lib.php
mod/quiz/tests/lib_test.php
mod/quiz/tests/restore_override_test.php [new file with mode: 0644]
mod/quiz/version.php
mod/scorm/lib.php
mod/scorm/tests/lib_test.php
mod/workshop/lib.php
mod/workshop/tests/lib_test.php
question/engine/questionattempt.php
question/engine/tests/questionusage_autosave_test.php
report/progress/amd/build/completion_override.min.js
report/progress/amd/src/completion_override.js
theme/boost/scss/moodle/course.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css
webservice/upgrade.txt

index 3a5d9f2..e7438a6 100644 (file)
@@ -229,7 +229,12 @@ abstract class base extends \core_analytics\calculable {
      */
     public function get_insights_users(\context $context) {
         if ($context->contextlevel === CONTEXT_USER) {
-            $users = [$context->instanceid => \core_user::get_user($context->instanceid)];
+            if (!has_capability('moodle/analytics:listowninsights', $context, $context->instanceid)) {
+                $users = [];
+            } else {
+                $users = [$context->instanceid => \core_user::get_user($context->instanceid)];
+            }
+
         } else if ($context->contextlevel >= CONTEXT_COURSE) {
             // At course level or below only enrolled users although this is not ideal for
             // teachers assigned at category level.
index 8b93bc1..57f113b 100644 (file)
@@ -70,6 +70,10 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
         $this->u2 = $this->getDataGenerator()->create_user(['firstname' => 'a222222222222', 'lastname' => 'a']);
         $this->u3 = $this->getDataGenerator()->create_user(['firstname' => 'b333333333333', 'lastname' => 'b']);
         $this->u4 = $this->getDataGenerator()->create_user(['firstname' => 'b444444444444', 'lastname' => 'b']);
+        $this->u5 = $this->getdatagenerator()->create_user(['firstname' => 'a555555555555', 'lastname' => 'a']);
+        $this->u6 = $this->getdatagenerator()->create_user(['firstname' => 'a666666666666', 'lastname' => 'a']);
+        $this->u7 = $this->getdatagenerator()->create_user(['firstname' => 'b777777777777', 'lastname' => 'b']);
+        $this->u8 = $this->getDataGenerator()->create_user(['firstname' => 'b888888888888', 'lastname' => 'b']);
 
         $this->c1 = $this->getDataGenerator()->create_course(['visible' => false]);
         $this->c2 = $this->getDataGenerator()->create_course();
@@ -78,10 +82,18 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
         $this->getDataGenerator()->enrol_user($this->u2->id, $this->c1->id, 'student');
         $this->getDataGenerator()->enrol_user($this->u3->id, $this->c1->id, 'student');
         $this->getDataGenerator()->enrol_user($this->u4->id, $this->c1->id, 'student');
+        $this->getDataGenerator()->enrol_user($this->u5->id, $this->c1->id, 'student');
+        $this->getDataGenerator()->enrol_user($this->u6->id, $this->c1->id, 'student');
+        $this->getDataGenerator()->enrol_user($this->u7->id, $this->c1->id, 'student');
+        $this->getDataGenerator()->enrol_user($this->u8->id, $this->c1->id, 'student');
         $this->getDataGenerator()->enrol_user($this->u1->id, $this->c2->id, 'student');
         $this->getDataGenerator()->enrol_user($this->u2->id, $this->c2->id, 'student');
         $this->getDataGenerator()->enrol_user($this->u3->id, $this->c2->id, 'student');
         $this->getDataGenerator()->enrol_user($this->u4->id, $this->c2->id, 'student');
+        $this->getDataGenerator()->enrol_user($this->u5->id, $this->c2->id, 'student');
+        $this->getDataGenerator()->enrol_user($this->u6->id, $this->c2->id, 'student');
+        $this->getDataGenerator()->enrol_user($this->u7->id, $this->c2->id, 'student');
+        $this->getDataGenerator()->enrol_user($this->u8->id, $this->c2->id, 'student');
 
         $this->setAdminUser();
 
@@ -111,7 +123,8 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
         $course1context = \context_course::instance($this->c1->id);
         $course2context = \context_course::instance($this->c2->id);
         $systemcontext = \context_system::instance();
-        $expected = [$this->u1->id, $this->u2->id, $this->u3->id, $this->u4->id];
+        $expected = [$this->u1->id, $this->u2->id, $this->u3->id, $this->u4->id, $this->u5->id, $this->u6->id,
+            $this->u7->id, $this->u8->id];
 
         // Check users exist in the relevant contexts.
         $userlist = new \core_privacy\local\request\userlist($course1context, $component);
@@ -145,9 +158,9 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
     public function test_delete_context_data() {
         global $DB;
 
-        // We have 2 predictions for model1 and 4 predictions for model2.
-        $this->assertEquals(6, $DB->count_records('analytics_predictions'));
-        $this->assertEquals(14, $DB->count_records('analytics_indicator_calc'));
+        // We have 4 predictions for model1 and 8 predictions for model2.
+        $this->assertEquals(12, $DB->count_records('analytics_predictions'));
+        $this->assertEquals(26, $DB->count_records('analytics_indicator_calc'));
 
         // We have 1 prediction action.
         $this->assertEquals(1, $DB->count_records('analytics_prediction_actions'));
@@ -157,8 +170,8 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
         // Delete the course that was used for prediction.
         provider::delete_data_for_all_users_in_context($coursecontext);
 
-        // The course predictions are deleted.
-        $this->assertEquals(4, $DB->count_records('analytics_predictions'));
+        // The course1 predictions are deleted.
+        $this->assertEquals(8, $DB->count_records('analytics_predictions'));
 
         // Calculations related to that context are deleted.
         $this->assertEmpty($DB->count_records('analytics_indicator_calc', ['contextid' => $coursecontext->id]));
@@ -181,7 +194,7 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
         provider::delete_data_for_user($contextlist);
 
         // The site level prediction for u3 was deleted.
-        $this->assertEquals(3, $DB->count_records('analytics_predictions'));
+        $this->assertEquals(9, $DB->count_records('analytics_predictions'));
         $this->assertEquals(0, $DB->count_records('analytics_prediction_actions'));
 
         $usercontexts = provider::get_contexts_for_userid($this->u1->id);
@@ -189,13 +202,13 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
                                                                             $usercontexts->get_contextids());
         provider::delete_data_for_user($contextlist);
         // We have nothing for u1.
-        $this->assertEquals(3, $DB->count_records('analytics_predictions'));
+        $this->assertEquals(9, $DB->count_records('analytics_predictions'));
 
         $usercontexts = provider::get_contexts_for_userid($this->u4->id);
         $contextlist = new \core_privacy\local\request\approved_contextlist($this->u4, 'core_analytics',
                                                                             $usercontexts->get_contextids());
         provider::delete_data_for_user($contextlist);
-        $this->assertEquals(0, $DB->count_records('analytics_predictions'));
+        $this->assertEquals(6, $DB->count_records('analytics_predictions'));
     }
 
     /**
@@ -218,6 +231,10 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
             $this->u2->id => provider::get_contexts_for_userid($this->u2->id)->get_contextids(),
             $this->u3->id => provider::get_contexts_for_userid($this->u3->id)->get_contextids(),
             $this->u4->id => provider::get_contexts_for_userid($this->u4->id)->get_contextids(),
+            $this->u5->id => provider::get_contexts_for_userid($this->u5->id)->get_contextids(),
+            $this->u6->id => provider::get_contexts_for_userid($this->u6->id)->get_contextids(),
+            $this->u7->id => provider::get_contexts_for_userid($this->u7->id)->get_contextids(),
+            $this->u8->id => provider::get_contexts_for_userid($this->u8->id)->get_contextids(),
         ];
 
         foreach ($actualcontexts as $userid => $unused) {
@@ -226,9 +243,9 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
         }
 
         // Test initial record counts are as expected.
-        $this->assertEquals(6, $DB->count_records('analytics_predictions'));
+        $this->assertEquals(12, $DB->count_records('analytics_predictions'));
         $this->assertEquals(1, $DB->count_records('analytics_prediction_actions'));
-        $this->assertEquals(14, $DB->count_records('analytics_indicator_calc'));
+        $this->assertEquals(26, $DB->count_records('analytics_indicator_calc'));
 
         // Delete u1 and u3 from system context.
         $approveduserids = [$this->u1->id, $this->u3->id];
@@ -241,6 +258,10 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
             $this->u2->id => [$systemcontext->id, $course1context->id, $course2context->id],
             $this->u3->id => [$course1context->id, $course2context->id],
             $this->u4->id => [$systemcontext->id, $course1context->id, $course2context->id],
+            $this->u5->id => [$systemcontext->id, $course1context->id, $course2context->id],
+            $this->u6->id => [$systemcontext->id, $course1context->id, $course2context->id],
+            $this->u7->id => [$systemcontext->id, $course1context->id, $course2context->id],
+            $this->u8->id => [$systemcontext->id, $course1context->id, $course2context->id],
         ];
 
         $actualcontexts = [
@@ -248,6 +269,10 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
             $this->u2->id => provider::get_contexts_for_userid($this->u2->id)->get_contextids(),
             $this->u3->id => provider::get_contexts_for_userid($this->u3->id)->get_contextids(),
             $this->u4->id => provider::get_contexts_for_userid($this->u4->id)->get_contextids(),
+            $this->u5->id => provider::get_contexts_for_userid($this->u5->id)->get_contextids(),
+            $this->u6->id => provider::get_contexts_for_userid($this->u6->id)->get_contextids(),
+            $this->u7->id => provider::get_contexts_for_userid($this->u7->id)->get_contextids(),
+            $this->u8->id => provider::get_contexts_for_userid($this->u8->id)->get_contextids(),
         ];
 
         foreach ($actualcontexts as $userid => $unused) {
@@ -257,12 +282,13 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
         }
 
         // Test expected number of records have been deleted.
-        $this->assertEquals(5, $DB->count_records('analytics_predictions'));
+        $this->assertEquals(11, $DB->count_records('analytics_predictions'));
         $this->assertEquals(1, $DB->count_records('analytics_prediction_actions'));
-        $this->assertEquals(12, $DB->count_records('analytics_indicator_calc'));
+        $this->assertEquals(24, $DB->count_records('analytics_indicator_calc'));
 
-        // Delete for all 4 users in course 2 context.
-        $approveduserids = [$this->u1->id, $this->u2->id, $this->u3->id, $this->u4->id];
+        // Delete for all 8 users in course 2 context.
+        $approveduserids = [$this->u1->id, $this->u2->id, $this->u3->id, $this->u4->id, $this->u5->id, $this->u6->id,
+            $this->u7->id, $this->u8->id];
         $approvedlist = new approved_userlist($course2context, $component, $approveduserids);
         provider::delete_data_for_users($approvedlist);
 
@@ -272,6 +298,10 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
             $this->u2->id => [$systemcontext->id, $course1context->id],
             $this->u3->id => [$course1context->id],
             $this->u4->id => [$systemcontext->id, $course1context->id],
+            $this->u5->id => [$systemcontext->id, $course1context->id],
+            $this->u6->id => [$systemcontext->id, $course1context->id],
+            $this->u7->id => [$systemcontext->id, $course1context->id],
+            $this->u8->id => [$systemcontext->id, $course1context->id],
         ];
 
         $actualcontexts = [
@@ -279,6 +309,10 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
             $this->u2->id => provider::get_contexts_for_userid($this->u2->id)->get_contextids(),
             $this->u3->id => provider::get_contexts_for_userid($this->u3->id)->get_contextids(),
             $this->u4->id => provider::get_contexts_for_userid($this->u4->id)->get_contextids(),
+            $this->u5->id => provider::get_contexts_for_userid($this->u5->id)->get_contextids(),
+            $this->u6->id => provider::get_contexts_for_userid($this->u6->id)->get_contextids(),
+            $this->u7->id => provider::get_contexts_for_userid($this->u7->id)->get_contextids(),
+            $this->u8->id => provider::get_contexts_for_userid($this->u8->id)->get_contextids(),
         ];
 
         foreach ($actualcontexts as $userid => $unused) {
@@ -288,9 +322,9 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
         }
 
         // Test expected number of records have been deleted.
-        $this->assertEquals(3, $DB->count_records('analytics_predictions'));
+        $this->assertEquals(7, $DB->count_records('analytics_predictions'));
         $this->assertEquals(1, $DB->count_records('analytics_prediction_actions'));
-        $this->assertEquals(8, $DB->count_records('analytics_indicator_calc'));
+        $this->assertEquals(16, $DB->count_records('analytics_indicator_calc'));
 
         $approveduserids = [$this->u3->id];
         $approvedlist = new approved_userlist($course1context, $component, $approveduserids);
@@ -302,6 +336,10 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
             $this->u2->id => [$systemcontext->id, $course1context->id],
             $this->u3->id => [],
             $this->u4->id => [$systemcontext->id, $course1context->id],
+            $this->u5->id => [$systemcontext->id, $course1context->id],
+            $this->u6->id => [$systemcontext->id, $course1context->id],
+            $this->u7->id => [$systemcontext->id, $course1context->id],
+            $this->u8->id => [$systemcontext->id, $course1context->id],
         ];
 
         $actualcontexts = [
@@ -309,6 +347,10 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
             $this->u2->id => provider::get_contexts_for_userid($this->u2->id)->get_contextids(),
             $this->u3->id => provider::get_contexts_for_userid($this->u3->id)->get_contextids(),
             $this->u4->id => provider::get_contexts_for_userid($this->u4->id)->get_contextids(),
+            $this->u5->id => provider::get_contexts_for_userid($this->u5->id)->get_contextids(),
+            $this->u6->id => provider::get_contexts_for_userid($this->u6->id)->get_contextids(),
+            $this->u7->id => provider::get_contexts_for_userid($this->u7->id)->get_contextids(),
+            $this->u8->id => provider::get_contexts_for_userid($this->u8->id)->get_contextids(),
         ];
         foreach ($actualcontexts as $userid => $unused) {
             sort($actualcontexts[$userid]);
@@ -317,9 +359,9 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
         }
 
         // Test expected number of records have been deleted.
-        $this->assertEquals(2, $DB->count_records('analytics_predictions'));
+        $this->assertEquals(6, $DB->count_records('analytics_predictions'));
         $this->assertEquals(0, $DB->count_records('analytics_prediction_actions'));
-        $this->assertEquals(7, $DB->count_records('analytics_indicator_calc'));
+        $this->assertEquals(15, $DB->count_records('analytics_indicator_calc'));
     }
 
     /**
index 18242b9..8f86698 100644 (file)
@@ -90,6 +90,19 @@ if (empty($acceptedroles)) {
     die();
 }
 
+// Get groupmode and currentgroup before going further.
+$groupmode = groups_get_course_groupmode($COURSE);  // Groups are being used.
+$currentgroup = groups_get_course_group($COURSE, true); // Get active group.
+
+// Check groupmode (SEPARATEGROUPS), currentgroup and capability (or admin).
+if ($groupmode == SEPARATEGROUPS && empty($currentgroup) &&
+    !has_capability('moodle/site:accessallgroups', $context) && !is_siteadmin() ) {
+    echo $OUTPUT->header();
+    echo $OUTPUT->heading(get_string("notingroup"));
+    echo $OUTPUT->footer();
+    die();
+}
+
 if (count($acceptedroles) > 1) {
     // If there is more than one role that can award a badge, prompt user to make a selection.
     // If it is an admin, include all accepted roles, otherwise only the ones that current user has in this context.
@@ -151,8 +164,10 @@ $options = array(
         'badgeid' => $badge->id,
         'context' => $context,
         'issuerid' => $USER->id,
-        'issuerrole' => $issuerrole->roleid
-        );
+        'issuerrole' => $issuerrole->roleid,
+        'currentgroup' => $currentgroup,
+        'url' => $url,
+    );
 $existingselector = new badge_existing_users_selector('existingrecipients', $options);
 $recipientselector = new badge_potential_users_selector('potentialrecipients', $options);
 $recipientselector->set_existing_recipients($existingselector->find_users(''));
@@ -193,6 +208,9 @@ if ($award && data_submitted() && has_capability('moodle/badges:awardbadge', $co
 echo $OUTPUT->header();
 echo $OUTPUT->heading($strrecipients);
 
+// Print group selector/dropdown menu (find out current groups mode).
+groups_print_course_menu($COURSE, $url);
+
 if (count($acceptedroles) > 1) {
     echo $OUTPUT->box($roleselect);
 }
index d9bd795..ebddecf 100644 (file)
@@ -112,7 +112,7 @@ class award_criteria_badge extends award_criteria {
             if ($this->id !== 0) {
                 $selected = array_keys($this->params);
             }
-            $settings = array('multiple' => 'multiple', 'size' => 20, 'class' => 'selectbadge');
+            $settings = array('multiple' => 'multiple', 'size' => 20, 'class' => 'selectbadge', 'required' => 'required');
             $mform->addElement('select', 'badge_badges', get_string('addbadge', 'badges'), $select, $settings);
             $mform->addRule('badge_badges', get_string('requiredbadge', 'badges'), 'required');
             $mform->addHelpButton('badge_badges', 'addbadge', 'badges');
@@ -243,7 +243,6 @@ class award_criteria_badge extends award_criteria {
         if ($this->method == BADGE_CRITERIA_AGGREGATION_ANY) {
             // User has received ANY of the required badges.
             $join = " LEFT JOIN {badge_issued} bi2 ON bi2.userid = u.id";
-            $where = "AND (";
             $i = 0;
             foreach ($this->params as $param) {
                 if ($i == 0) {
@@ -254,7 +253,10 @@ class award_criteria_badge extends award_criteria {
                 $params['badgeid'.$i] = $param['badge'];
                 $i++;
             }
-            $where .= ") ";
+            // MDL-66032 Do not create expression if there are no badges in criteria.
+            if (!empty($where)) {
+                $where = ' AND (' . $where . ') ';
+            }
             return array($join, $where, $params);
         } else {
             // User has received ALL of the required badges.
index 4425540..f085c56 100644 (file)
@@ -52,6 +52,18 @@ abstract class badge_award_selector_base extends user_selector_base {
      */
     protected $issuerid = null;
 
+    /**
+     * The return address. Accepts either a string or a moodle_url.
+     * @var string $url
+     */
+    public $url;
+
+    /**
+     * The current group being displayed.
+     * @var int $currentgroup
+     */
+    public $currentgroup;
+
     /**
      * Constructor method
      * @param string $name
@@ -77,6 +89,15 @@ abstract class badge_award_selector_base extends user_selector_base {
         if (isset($options['issuerrole'])) {
             $this->issuerrole = $options['issuerrole'];
         }
+        if (isset($options['url'])) {
+            $this->url = $options['url'];
+        }
+        if (isset($options['currentgroup'])) {
+            $this->currentgroup = $options['currentgroup'];
+        } else {
+            // Returns group active in course, changes the group by default if 'group' page param present.
+            $this->currentgroup = groups_get_course_group($COURSE, true);
+        }
     }
 
     /**
@@ -92,8 +113,27 @@ abstract class badge_award_selector_base extends user_selector_base {
         $options['badgeid'] = $this->badgeid;
         $options['issuerid'] = $this->issuerid;
         $options['issuerrole'] = $this->issuerrole;
+        // These will be used to filter potential badge recipients when searching.
+        $options['currentgroup'] = $this->currentgroup;
         return $options;
     }
+
+    /**
+     * Restricts the selection of users to display, according to the groups they belong.
+     *
+     * @return array
+     */
+    protected function get_groups_sql() {
+        $groupsql = '';
+        $groupwheresql = '';
+        $groupwheresqlparams = array();
+        if ($this->currentgroup) {
+            $groupsql = ' JOIN {groups_members} gm ON gm.userid = u.id ';
+            $groupwheresql = ' AND gm.groupid = :gr_grpid ';
+            $groupwheresqlparams = array('gr_grpid' => $this->currentgroup);
+        }
+        return array($groupsql, $groupwheresql, $groupwheresqlparams);
+    }
 }
 
 /**
@@ -141,8 +181,10 @@ class badge_potential_users_selector extends badge_award_selector_base {
             $wherecondition = ' WHERE ' . implode(' AND ', $whereconditions);
         }
 
+        list($groupsql, $groupwheresql, $groupwheresqlparams) = $this->get_groups_sql();
+
         list($esql, $eparams) = get_enrolled_sql($this->context, 'moodle/badges:earnbadge', 0, true);
-        $params = array_merge($params, $eparams);
+        $params = array_merge($params, $eparams, $groupwheresqlparams);
 
         $fields      = 'SELECT ' . $this->required_fields_sql('u');
         $countfields = 'SELECT COUNT(u.id)';
@@ -153,7 +195,9 @@ class badge_potential_users_selector extends badge_award_selector_base {
         $sql = " FROM {user} u JOIN ($esql) je ON je.id = u.id
                  LEFT JOIN {badge_manual_award} bm
                      ON (bm.recipientid = u.id AND bm.badgeid = :badgeid AND bm.issuerrole = :issuerrole)
-                 $wherecondition AND bm.id IS NULL";
+                 $groupsql
+                 $wherecondition AND bm.id IS NULL
+                 $groupwheresql";
 
         list($sort, $sortparams) = users_order_by_sql('u', $search, $this->accesscontext);
         $order = ' ORDER BY ' . $sort;
@@ -204,12 +248,16 @@ class badge_existing_users_selector extends badge_award_selector_base {
         $fields = $this->required_fields_sql('u');
         list($sort, $sortparams) = users_order_by_sql('u', $search, $this->accesscontext);
 
-        $params = array_merge($params, $eparams, $sortparams);
+        list($groupsql, $groupwheresql, $groupwheresqlparams) = $this->get_groups_sql();
+
+        $params = array_merge($params, $eparams, $sortparams, $groupwheresqlparams);
         $recipients = $DB->get_records_sql("SELECT $fields
                 FROM {user} u
                 JOIN ($esql) je ON je.id = u.id
                 JOIN {badge_manual_award} s ON s.recipientid = u.id
+                $groupsql
                 WHERE $wherecondition AND s.badgeid = :badgeid AND s.issuerrole = :issuerrole
+                $groupwheresql
                 ORDER BY $sort", $params);
 
         return array(get_string('existingrecipients', 'badges') => $recipients);
diff --git a/badges/tests/behat/award_badge_groups.feature b/badges/tests/behat/award_badge_groups.feature
new file mode 100644 (file)
index 0000000..90683b3
--- /dev/null
@@ -0,0 +1,130 @@
+@core @core_badges @_file_upload
+Feature: Award badges with separate groups
+  In order to award badges to users for their achievements
+  As a teacher
+  I need to award badges only to users in the groups I have access
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | teacher2 | Teacher | 2 | teacher2@example.com |
+      | student1 | Student | 1 | student1@example.com |
+      | student2 | Student | 2 | student2@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | teacher2 | C1 | teacher |
+      | student1 | C1 | student |
+      | student2 | C1 | student |
+    And the following "groups" exist:
+      | name | course | idnumber |
+      | Class A | C1 | CA |
+      | Class B | C1 | CB |
+    And the following "group members" exist:
+      | user | group |
+      | student1 | CB |
+      | teacher1 | CB |
+      | student2 | CA |
+      | teacher2 | CA |
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I navigate to "Edit settings" in current page administration
+    And I expand all fieldsets
+    And I set the field "Group mode" to "Separate groups"
+    And I press "Save and display"
+    And I navigate to "Badges > Add a new badge" in current page administration
+    And I follow "Add a new badge"
+    And I set the following fields to these values:
+      | Name | Course Badge |
+      | Description | Course badge description |
+      | issuername | Tester of course badge |
+    And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
+    And I press "Create badge"
+    And I set the field "type" to "Manual issue by role"
+    And I expand all fieldsets
+    And I set the field "Teacher" to "1"
+    And I set the field "Non-editing teacher" to "1"
+    # Set to ANY of the roles awards badge.
+    And I set the field "Any of the selected roles awards the badge" to "1"
+    And I press "Save"
+    And I press "Enable access"
+    And I press "Continue"
+    And I log out
+
+  @javascript
+  Scenario: Award course badge as non-editing teacher with only one group
+    When I log in as "teacher2"
+    And I am on "Course 1" course homepage
+    And I navigate to "Badges > Manage badges" in current page administration
+    And I follow "Manage badges"
+    And I follow "Course Badge"
+    And I press "Award badge"
+    And I set the field "role" to "Non-editing teacher"
+    # Teacher 2 should see a "Separate groups" label with the group he is in
+    Then I should see "Separate groups: Class A"
+    # Teacher 2 should only see the users who belong to the same group as he does
+    And I should see "Student 2"
+    And I should not see "Student 1"
+    # Non-editing teacher can award the badge
+    And I set the field "potentialrecipients[]" to "Student 2 (student2@example.com)"
+    And I press "Award badge"
+    And I follow "Course Badge"
+    And I should see "Recipients (1)"
+    And I log out
+    And I log in as "student2"
+    And I follow "Profile" in the user menu
+    And I click on "Course 1" "link" in the "region-main" "region"
+    And I should see "Course Badge"
+    And I log out
+
+  @javascript
+  Scenario: Award course badge as non-editing teacher with more than one group
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I navigate to "Users > Groups" in current page administration
+    And I follow "Groups"
+    And I set the field "groups" to "Class B (2)"
+    And I press "Add/remove users"
+    And I set the field "addselect" to "Teacher 2 (teacher2@example.com)"
+    And I press "Add"
+    And I log out
+    When I log in as "teacher2"
+    And I am on "Course 1" course homepage
+    And I navigate to "Badges > Manage badges" in current page administration
+    And I follow "Manage badges"
+    And I follow "Course Badge"
+    And I press "Award badge"
+    And I set the field "role" to "Non-editing teacher"
+    # Teacher 2 should see a "Separate groups" label and a dropdown menu with the groups he belongs to
+    And I set the field "Separate groups" to "Class A"
+    Then I should see "Student 2"
+    And I should not see "Student 1"
+    And I set the field "Separate groups" to "Class B"
+    And I should see "Student 1"
+    And I should not see "Student 2"
+    And I log out
+
+  @javascript
+  Scenario: Award course badge as non-editing teacher without any group
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I navigate to "Users > Groups" in current page administration
+    And I follow "Groups"
+    And I set the field "groups" to "Class A (2)"
+    And I press "Add/remove users"
+    And I set the field "removeselect" to "Teacher 2 (teacher2@example.com)"
+    And I press "Remove"
+    And I press "Back to groups"
+    And I log out
+    When I log in as "teacher2"
+    And I am on "Course 1" course homepage
+    And I navigate to "Badges > Manage badges" in current page administration
+    And I follow "Manage badges"
+    And I follow "Course Badge"
+    And I press "Award badge"
+    # Teacher 2 shouldn't be able to go further
+    Then I should see "Sorry, but you need to be part of a group to see this page."
\ No newline at end of file
index 905e24f..857d89e 100644 (file)
Binary files a/calendar/amd/build/calendar_threemonth.min.js and b/calendar/amd/build/calendar_threemonth.min.js differ
index 66cfb00..68d16be 100644 (file)
@@ -47,7 +47,7 @@ function(
      */
     var registerCalendarEventListeners = function(root) {
         var body = $('body');
-        body.on([CalendarEvents.monthChanged, CalendarEvents.dayChanged], function(e, year, month, courseId, categoryId) {
+        body.on([CalendarEvents.monthChanged, CalendarEvents.dayChanged].join(' '), function(e, year, month, courseId, categoryId) {
             // We have to use a queue here because the calling code is decoupled from these listeners.
             // It's possible for the event to be called multiple times before one call is fully resolved.
             root.queue(function(next) {
index 0559c93..1f204d8 100644 (file)
@@ -89,6 +89,11 @@ class writer extends \core\dataformat\base {
     public function write_record($record, $rownum) {
         $rowheight = 0;
 
+        // If $record is an object convert it to an array.
+        if (is_object($record)) {
+            $record = (array)$record;
+        }
+
         foreach ($record as $cell) {
             $rowheight = max($rowheight, $this->pdf->getStringHeight($this->colwidth, $cell, false, true, '', 1));
         }
@@ -99,12 +104,19 @@ class writer extends \core\dataformat\base {
             $this->print_heading();
         }
 
-        $total = count($record);
-        $counter = 1;
-        foreach ($record as $cell) {
-            $nextposition = ($counter == $total) ? 1 : 0;
+        // Get the last key for this record.
+        end($record);
+        $lastkey = key($record);
+
+        // Reset the record pointer.
+        reset($record);
+
+        // Loop through each element.
+        foreach ($record as $key => $cell) {
+            // Determine whether we're at the last element of the record.
+            $nextposition = ($lastkey === $key) ? 1 : 0;
+            // Write the element.
             $this->pdf->Multicell($this->colwidth, $rowheight, $cell, 1, 'L', false, $nextposition);
-            $counter++;
         }
     }
 
index aeda2ff..f55f8db 100644 (file)
@@ -124,7 +124,7 @@ class converter implements \core_files\converter_interface {
         $uploadurl;
         // Google returns a location header with the location for the upload.
         foreach ($headers as $header) {
-            if (strpos($header, 'Location:') === 0) {
+            if (stripos($header, 'Location:') === 0) {
                 $uploadurl = trim(substr($header, strpos($header, ':') + 1));
             }
         }
index fdb999b..45b9918 100644 (file)
@@ -10861,9 +10861,9 @@ class admin_setting_scsscode extends admin_setting_configtextarea {
         $scss = new core_scss();
         try {
             $scss->compile($data);
-        } catch (Leafo\ScssPhp\Exception\ParserException $e) {
+        } catch (ScssPhp\ScssPhp\Exception\ParserException $e) {
             return get_string('scssinvalid', 'admin', $e->getMessage());
-        } catch (Leafo\ScssPhp\Exception\CompilerException $e) {
+        } catch (ScssPhp\ScssPhp\Exception\CompilerException $e) {
             // Silently ignore this - it could be a scss variable defined from somewhere
             // else which we are not examining here.
             return true;
index 7120ade..e8f8294 100644 (file)
@@ -28,4 +28,6 @@
  */
 
 define('NO_MOODLE_COOKIES', true);
+define('ALLOW_GET_PARAMETERS', true);
+
 require_once('service.php');
index caaf86c..3bcb8e9 100644 (file)
@@ -38,9 +38,24 @@ require_once($CFG->libdir . '/externallib.php');
 
 define('PREFERRED_RENDERER_TARGET', RENDERER_TARGET_GENERAL);
 
-$rawjson = file_get_contents('php://input');
+$arguments = '';
+$cacherequest = false;
+if (defined('ALLOW_GET_PARAMETERS')) {
+    $arguments = optional_param('args', '', PARAM_RAW);
+    $cachekey = optional_param('cachekey', '', PARAM_INT);
+    if ($cachekey && $cachekey > 0 && $cachekey <= time()) {
+        $cacherequest = true;
+    }
+}
+
+// Either we are not allowing GET parameters or we didn't use GET because
+// we did not pass a cache key or the URL was too long.
+if (empty($arguments)) {
+    $arguments = file_get_contents('php://input');
+}
+
+$requests = json_decode($arguments, true);
 
-$requests = json_decode($rawjson, true);
 if ($requests === null) {
     $lasterror = json_last_error_msg();
     throw new coding_exception('Invalid json in request: ' . $lasterror);
@@ -54,6 +69,7 @@ $settings->set_fileurl(true);
 $settings->set_filter(true);
 $settings->set_raw(false);
 
+$haserror = false;
 foreach ($requests as $request) {
     $response = array();
     $methodname = clean_param($request['methodname'], PARAM_ALPHANUMEXT);
@@ -64,7 +80,19 @@ foreach ($requests as $request) {
     $responses[$index] = $response;
     if ($response['error']) {
         // Do not process the remaining requests.
+        $haserror = true;
         break;
     }
 }
+
+if ($cacherequest && !$haserror) {
+    // 90 days only - based on Moodle point release cadence being every 3 months.
+    $lifetime = 60 * 60 * 24 * 90;
+
+    header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
+    header('Pragma: ');
+    header('Cache-Control: public, max-age=' . $lifetime . ', immutable');
+    header('Accept-Ranges: none');
+}
+
 echo json_encode($responses);
index adc66eb..e8ad0a8 100644 (file)
Binary files a/lib/amd/build/ajax.min.js and b/lib/amd/build/ajax.min.js differ
index 90e5ebc..b52290f 100644 (file)
Binary files a/lib/amd/build/checkbox-toggleall.min.js and b/lib/amd/build/checkbox-toggleall.min.js differ
index 9b9bcf3..090ae9a 100644 (file)
Binary files a/lib/amd/build/icon_system_fontawesome.min.js and b/lib/amd/build/icon_system_fontawesome.min.js differ
diff --git a/lib/amd/build/modal_confirm.min.js b/lib/amd/build/modal_confirm.min.js
deleted file mode 100644 (file)
index 68df031..0000000
Binary files a/lib/amd/build/modal_confirm.min.js and /dev/null differ
index 5fc8341..e657fbf 100644 (file)
Binary files a/lib/amd/build/modal_events.min.js and b/lib/amd/build/modal_events.min.js differ
index 5494e45..01db748 100644 (file)
Binary files a/lib/amd/build/modal_factory.min.js and b/lib/amd/build/modal_factory.min.js differ
index 9e03a10..b1cc0db 100644 (file)
Binary files a/lib/amd/build/str.min.js and b/lib/amd/build/str.min.js differ
index 1293f1a..113f520 100644 (file)
Binary files a/lib/amd/build/templates.min.js and b/lib/amd/build/templates.min.js differ
index 98e982c..389680f 100644 (file)
@@ -137,9 +137,13 @@ define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Lo
          * @param {Boolean} nosessionupdate Optional, defaults to false.
          *                  If true, the timemodified for the session will not be updated.
          * @param {Integer} timeout number of milliseconds to wait for a response. Defaults to no limit.
+         * @param {Integer} cachekey This is used in order to identify the request. If this id changes then we
+         *                  will be sending a different URL and any caching (eg. browser, proxy) knows that it
+         *                  should perform another request and not use the cache. Note - this variable is only
+         *                  used when we are calling 'service-nologin.php'. See MDL-65794.
          * @return {Promise[]} Array of promises that will be resolved when the ajax call returns.
          */
-        call: function(requests, async, loginrequired, nosessionupdate, timeout) {
+        call: function(requests, async, loginrequired, nosessionupdate, timeout, cachekey) {
             $(window).bind('beforeunload', function() {
                 unloading = true;
             });
@@ -149,6 +153,8 @@ define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Lo
                 methodInfo = [],
                 requestInfo = '';
 
+            var maxUrlLength = 2000;
+
             if (typeof loginrequired === "undefined") {
                 loginrequired = true;
             }
@@ -158,6 +164,16 @@ define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Lo
             if (typeof timeout === 'undefined') {
                 timeout = 0;
             }
+            if (typeof cachekey === 'undefined') {
+                cachekey = null;
+            } else {
+                cachekey = parseInt(cachekey);
+                if (cachekey <= 0) {
+                    cachekey = null;
+                } else if (!cachekey) {
+                    cachekey = null;
+                }
+            }
 
             if (typeof nosessionupdate === "undefined") {
                 nosessionupdate = false;
@@ -193,7 +209,6 @@ define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Lo
             ajaxRequestData = JSON.stringify(ajaxRequestData);
             var settings = {
                 type: 'POST',
-                data: ajaxRequestData,
                 context: requests,
                 dataType: 'json',
                 processData: false,
@@ -203,16 +218,35 @@ define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Lo
             };
 
             var script = 'service.php';
+            var url = config.wwwroot + '/lib/ajax/';
             if (!loginrequired) {
                 script = 'service-nologin.php';
+                url += script + '?info=' + requestInfo;
+                if (cachekey) {
+                    url += '&cachekey=' + cachekey;
+                    settings.type = 'GET';
+                }
+            } else {
+                url += script + '?sesskey=' + config.sesskey + '&info=' + requestInfo;
             }
-            var url = config.wwwroot + '/lib/ajax/' + script +
-                    '?sesskey=' + config.sesskey + '&info=' + requestInfo;
 
             if (nosessionupdate) {
                 url += '&nosessionupdate=true';
             }
 
+            if (settings.type === 'POST') {
+                settings.data = ajaxRequestData;
+            } else {
+                var urlUseGet = url + '&args=' + encodeURIComponent(ajaxRequestData);
+
+                if (urlUseGet.length > maxUrlLength) {
+                    settings.type = 'POST';
+                    settings.data = ajaxRequestData;
+                } else {
+                    url = urlUseGet;
+                }
+            }
+
             // Jquery deprecated done and fail with async=false so we need to do this 2 ways.
             if (async) {
                 $.ajax(url, settings)
index c6d4103..345db8f 100644 (file)
  */
 define(['jquery', 'core/pubsub'], function($, PubSub) {
 
+    /**
+     * Whether event listeners have already been registered.
+     *
+     * @private
+     * @type {boolean}
+     */
     var registered = false;
 
+    /**
+     * List of custom events that this module publishes.
+     *
+     * @private
+     * @type {{checkboxToggled: string}}
+     */
     var events = {
         checkboxToggled: 'core/checkbox-toggleall:checkboxToggled',
     };
 
-    var getAllCheckboxes = function(root, toggleGroup) {
-        return root.find('[data-action="toggle"][data-togglegroup="' + toggleGroup + '"]');
+    /**
+     * Fetches elements that are member of a given toggle group.
+     *
+     * @private
+     * @param {jQuery} root The root jQuery element.
+     * @param {string} toggleGroup The toggle group name that we're searching form.
+     * @param {boolean} exactMatch Whether we want an exact match we just want to match toggle groups that start with the given
+     *                             toggle group name.
+     * @returns {jQuery} The elements matching the given toggle group.
+     */
+    var getToggleGroupElements = function(root, toggleGroup, exactMatch) {
+        if (exactMatch) {
+            return root.find('[data-action="toggle"][data-togglegroup="' + toggleGroup + '"]');
+        } else {
+            return root.find('[data-action="toggle"][data-togglegroup^="' + toggleGroup + '"]');
+        }
     };
 
+    /**
+     * Fetches the slave checkboxes for a given toggle group.
+     *
+     * @private
+     * @param {jQuery} root The root jQuery element.
+     * @param {string} toggleGroup The toggle group name.
+     * @returns {jQuery} The slave checkboxes belonging to the toggle group.
+     */
     var getAllSlaveCheckboxes = function(root, toggleGroup) {
-        return getAllCheckboxes(root, toggleGroup).filter('[data-toggle="slave"]');
+        return getToggleGroupElements(root, toggleGroup, false).filter('[data-toggle="slave"]');
+    };
+
+    /**
+     * Fetches the master elements (checkboxes or buttons) that control the slave checkboxes in a given toggle group.
+     *
+     * @private
+     * @param {jQuery} root The root jQuery element.
+     * @param {string} toggleGroup The toggle group name.
+     * @param {boolean} exactMatch
+     * @returns {jQuery} The control elements belonging to the toggle group.
+     */
+    var getControlCheckboxes = function(root, toggleGroup, exactMatch) {
+        return getToggleGroupElements(root, toggleGroup, exactMatch).filter('[data-toggle="master"]');
     };
 
-    var getControlCheckboxes = function(root, toggleGroup) {
-        return getAllCheckboxes(root, toggleGroup).filter('[data-toggle="master"]');
+    /**
+     * Fetches the action elements that perform actions on the selected checkboxes in a given toggle group.
+     *
+     * @private
+     * @param {jQuery} root The root jQuery element.
+     * @param {string} toggleGroup The toggle group name.
+     * @returns {jQuery} The action elements belonging to the toggle group.
+     */
+    var getActionElements = function(root, toggleGroup) {
+        return getToggleGroupElements(root, toggleGroup, true).filter('[data-toggle="action"]');
     };
 
+    /**
+     * Toggles the slave checkboxes in a given toggle group when a master element in that toggle group is toggled.
+     *
+     * @private
+     * @param {Object} e The event object.
+     */
     var toggleSlavesFromMasters = function(e) {
         var root = e.data.root;
         var target = $(e.target);
 
         var toggleGroupName = target.data('togglegroup');
-        var targetState = target.is(':checked');
+        var targetState;
+        if (target.is(':checkbox')) {
+            targetState = target.is(':checked');
+        } else {
+            targetState = target.data('checkall') === 1;
+        }
 
         var slaves = getAllSlaveCheckboxes(root, toggleGroupName);
         var checkedSlaves = slaves.filter(':checked');
 
-        setMasterStates(root, toggleGroupName, targetState);
+        setMasterStates(root, toggleGroupName, targetState, false);
 
         // Set the slave checkboxes from the masters.
         slaves.prop('checked', targetState);
+        // Trigger 'change' event to toggle other master checkboxes (e.g. parent master checkboxes) and action elements.
+        slaves.trigger('change');
 
         PubSub.publish(events.checkboxToggled, {
             root: root,
@@ -64,55 +132,107 @@ define(['jquery', 'core/pubsub'], function($, PubSub) {
         });
     };
 
+    /**
+     * Toggles the master checkboxes in a given toggle group when all or none of the slave checkboxes in the same toggle group
+     * have been selected.
+     *
+     * @private
+     * @param {Object} e The event object.
+     */
     var toggleMastersFromSlaves = function(e) {
         var root = e.data.root;
         var target = $(e.target);
 
-        var toggleGroupName = target.data('togglegroup');
+        var toggleGroups = target.data('togglegroup').split(' ');
+        var toggleGroupLevels = [];
+        var toggleGroupLevel = '';
+        toggleGroups.forEach(function(toggleGroupName) {
+            toggleGroupLevel += ' ' + toggleGroupName;
+            toggleGroupLevels.push(toggleGroupLevel.trim());
+        });
 
-        var slaves = getAllSlaveCheckboxes(root, toggleGroupName);
-        var checkedSlaves = slaves.filter(':checked');
-        var targetState = (slaves.length === checkedSlaves.length);
+        toggleGroupLevels.forEach(function(toggleGroupName) {
+            var slaves = getAllSlaveCheckboxes(root, toggleGroupName);
+            var checkedSlaves = slaves.filter(':checked');
+            var targetState = (slaves.length === checkedSlaves.length);
 
-        setMasterStates(root, toggleGroupName, targetState);
+            // Make sure to toggle the exact master checkbox.
+            setMasterStates(root, toggleGroupName, targetState, true);
 
-        PubSub.publish(events.checkboxToggled, {
-            root: root,
-            toggleGroupName: toggleGroupName,
-            slaves: slaves,
-            checkedSlaves: checkedSlaves,
-            anyChecked: !!checkedSlaves.length,
+            // Enable action elements when there's at least one checkbox checked. Disable otherwise.
+            setActionElementStates(root, toggleGroupName, !checkedSlaves.length);
+
+            PubSub.publish(events.checkboxToggled, {
+                root: root,
+                toggleGroupName: toggleGroupName,
+                slaves: slaves,
+                checkedSlaves: checkedSlaves,
+                anyChecked: !!checkedSlaves.length,
+            });
         });
     };
 
-    var setMasterStates = function(root, toggleGroupName, targetState) {
+    /**
+     * Enables or disables the action elements.
+     *
+     * @private
+     * @param {jQuery} root The root jQuery element.
+     * @param {string} toggleGroupName The toggle group name of the action element(s).
+     * @param {boolean} disableActionElements Whether to disable or to enable the action elements.
+     */
+    var setActionElementStates = function(root, toggleGroupName, disableActionElements) {
+        getActionElements(root, toggleGroupName).prop('disabled', disableActionElements);
+    };
+
+    /**
+     * Selects or deselects the master elements.
+     *
+     * @private
+     * @param {jQuery} root The root jQuery element.
+     * @param {string} toggleGroupName The toggle group name of the master element(s).
+     * @param {boolean} targetState Whether to select (true) or deselect (false).
+     * @param {boolean} exactMatch Whether to do an exact match for the toggle group name or not.
+     */
+    var setMasterStates = function(root, toggleGroupName, targetState, exactMatch) {
         // Set the master checkboxes value and ARIA labels..
-        var masters = getControlCheckboxes(root, toggleGroupName);
+        var masters = getControlCheckboxes(root, toggleGroupName, exactMatch);
         masters.prop('checked', targetState);
-        masters.each(function(i, masterCheckbox) {
-            masterCheckbox = $(masterCheckbox);
-            var masterLabel = root.find('[for="' + masterCheckbox.attr('id') + '"]');
+        masters.each(function(i, masterElement) {
+            masterElement = $(masterElement);
+
             var targetString;
-            if (masterLabel.length) {
-                if (targetState) {
-                    targetString = masterCheckbox.data('toggle-deselectall');
-                } else {
-                    targetString = masterCheckbox.data('toggle-selectall');
-                }
+            if (targetState) {
+                targetString = masterElement.data('toggle-deselectall');
+            } else {
+                targetString = masterElement.data('toggle-selectall');
+            }
 
-                if (masterLabel.html() !== targetString) {
-                    masterLabel.html(targetString);
+            if (masterElement.is(':checkbox')) {
+                var masterLabel = root.find('[for="' + masterElement.attr('id') + '"]');
+                if (masterLabel.length) {
+                    if (masterLabel.html() !== targetString) {
+                        masterLabel.html(targetString);
+                    }
                 }
+            } else {
+                masterElement.text(targetString);
+                // Set the checkall data attribute.
+                masterElement.data('checkall', targetState ? 0 : 1);
             }
         });
     };
 
+    /**
+     * Registers the event listeners.
+     *
+     * @private
+     */
     var registerListeners = function() {
         if (!registered) {
             registered = true;
 
             var root = $(document.body);
-            root.on('change', '[data-action="toggle"][data-toggle="master"]', {root: root}, toggleSlavesFromMasters);
+            root.on('click', '[data-action="toggle"][data-toggle="master"]', {root: root}, toggleSlavesFromMasters);
             root.on('change', '[data-action="toggle"][data-toggle="slave"]', {root: root}, toggleMastersFromSlaves);
         }
     };
index 5ef205a..b2f8cee 100644 (file)
@@ -60,7 +60,7 @@ define(['core/icon_system', 'jquery', 'core/ajax', 'core/mustache', 'core/locals
             fetchMap = Ajax.call([{
                 methodname: 'core_output_load_fontawesome_icon_map',
                 args: []
-            }], true, false)[0];
+            }], true, false, false, 0, M.cfg.themerev)[0];
         }
 
         return fetchMap.then(function(map) {
diff --git a/lib/amd/src/modal_confirm.js b/lib/amd/src/modal_confirm.js
deleted file mode 100644 (file)
index f396c8e..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-// 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 <http://www.gnu.org/licenses/>.
-
-/**
- * Contain the logic for the yes/no confirmation modal.
- * This has been deprecated and should not be used anymore. Please use core/modal_save_cancel instead.
- * See MDL-59759.
- *
- * @deprecated Since Moodle 3.4
- * @module     core/modal_confirm
- * @class      modal_confirm
- * @package    core
- * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-define(['jquery', 'core/custom_interaction_events', 'core/modal_events', 'core/modal_save_cancel', 'core/log'],
-        function($, CustomEvents, ModalEvents, ModalSaveCancel, Log) {
-
-    /**
-     * Constructor for the Modal.
-     *
-     * @param {object} root The root jQuery element for the modal
-     */
-    var ModalConfirm = function(root) {
-        Log.warn("The CONFIRM modal type has been deprecated and should not be used anymore." +
-            " Please use the SAVE_CANCEL modal type instead.");
-        ModalSaveCancel.call(this, root);
-    };
-
-    ModalConfirm.prototype = Object.create(ModalSaveCancel.prototype);
-    ModalConfirm.prototype.constructor = ModalConfirm;
-
-    return ModalConfirm;
-});
index 9c26179..234d3d7 100644 (file)
@@ -32,9 +32,5 @@ define([], function() {
         // ModalSaveCancel events.
         save: 'modal-save-cancel:save',
         cancel: 'modal-save-cancel:cancel',
-        // ModalConfirm events. Deprecated since Moodle 3.4. See MDL-59759.
-        // Point core/modal_confirm events to save/cancel events of core/modal_save_cancel.
-        yes: 'modal-save-cancel:save',
-        no: 'modal-save-cancel:cancel',
     };
 });
index b946390..4e9381e 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
-        'core/modal_save_cancel', 'core/modal_confirm', 'core/modal_cancel',
+        'core/modal_save_cancel', 'core/modal_cancel',
         'core/templates', 'core/notification', 'core/custom_interaction_events'],
-    function($, ModalEvents, ModalRegistry, Modal, ModalSaveCancel, ModalConfirm,
+    function($, ModalEvents, ModalRegistry, Modal, ModalSaveCancel,
         ModalCancel, Templates, Notification, CustomEvents) {
 
     // The templates for each type of modal.
     var TEMPLATES = {
         DEFAULT: 'core/modal',
         SAVE_CANCEL: 'core/modal_save_cancel',
-        // Deprecated since Moodle 3.4. Point template to core/modal_save_cancel instead. See MDL-59759.
-        CONFIRM: 'core/modal_save_cancel',
         CANCEL: 'core/modal_cancel',
     };
 
@@ -41,16 +39,12 @@ define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
     var TYPES = {
         DEFAULT: 'DEFAULT',
         SAVE_CANCEL: 'SAVE_CANCEL',
-        // Deprecated since Moodle 3.4. See MDL-59759.
-        CONFIRM: 'CONFIRM',
         CANCEL: 'CANCEL',
     };
 
     // Register the common set of modals.
     ModalRegistry.register(TYPES.DEFAULT, Modal, TEMPLATES.DEFAULT);
     ModalRegistry.register(TYPES.SAVE_CANCEL, ModalSaveCancel, TEMPLATES.SAVE_CANCEL);
-    // Deprecated since Moodle 3.4. See MDL-59759.
-    ModalRegistry.register(TYPES.CONFIRM, ModalConfirm, TEMPLATES.CONFIRM);
     ModalRegistry.register(TYPES.CANCEL, ModalCancel, TEMPLATES.CANCEL);
 
     /**
index 286fb48..93ef0a1 100644 (file)
@@ -148,7 +148,7 @@ define(['jquery', 'core/ajax', 'core/localstorage'], function($, ajax, storage)
 
                 // Everything might already be queued so we need to check if we have real ajax requests to run.
                 if (ajaxrequests.length > 0) {
-                    ajax.call(ajaxrequests, true, false);
+                    ajax.call(ajaxrequests, true, false, false, 0, M.cfg.langrev);
                 }
 
                 $.when.apply(null, fetchpromises).done(
index 70140ee..6bb626f 100644 (file)
@@ -220,7 +220,7 @@ define([
 
         if (requests.length) {
             // We have requests to send so resolve the deferred with the promises.
-            serverRequestsDeferred.resolve(ajax.call(requests, true, false));
+            serverRequestsDeferred.resolve(ajax.call(requests, true, false, false, 0, M.cfg.themerev));
         } else {
             // Nothing to load so we can resolve our deferred.
             serverRequestsDeferred.resolve();
index 187fde8..22edb6b 100644 (file)
@@ -80,7 +80,7 @@ class core_component {
         'GeoIp2' => 'lib/maxmind/GeoIp2',
         'Sabberworm\\CSS' => 'lib/php-css-parser',
         'MoodleHQ\\RTLCSS' => 'lib/rtlcss',
-        'Leafo\\ScssPhp' => 'lib/scssphp',
+        'ScssPhp\\ScssPhp' => 'lib/scssphp',
         'Box\\Spout' => 'lib/spout/src/Spout',
         'MatthiasMullie\\Minify' => 'lib/minify/matthiasmullie-minify/src/',
         'MatthiasMullie\\PathConverter' => 'lib/minify/matthiasmullie-pathconverter/src/',
index 6b0c10b..f7d8a74 100644 (file)
@@ -51,7 +51,7 @@ abstract class spout_base extends \core\dataformat\base {
      * Output file headers to initialise the download of the file.
      */
     public function send_http_headers() {
-        $this->writer = \Box\Spout\Writer\WriterFactory::create($this->spouttype);
+        $this->writer = \Box\Spout\Writer\Common\Creator\WriterEntityFactory::createWriter($this->spouttype);
         if (method_exists($this->writer, 'setTempFolder')) {
             $this->writer->setTempFolder(make_request_directory());
         }
@@ -79,7 +79,7 @@ abstract class spout_base extends \core\dataformat\base {
      * @param array $columns
      */
     public function start_sheet($columns) {
-        if ($this->sheettitle && $this->writer instanceof \Box\Spout\Writer\AbstractMultiSheetsWriter) {
+        if ($this->sheettitle && $this->writer instanceof \Box\Spout\Writer\WriterMultiSheetsAbstract) {
             if ($this->renamecurrentsheet) {
                 $sheet = $this->writer->getCurrentSheet();
                 $this->renamecurrentsheet = false;
@@ -88,7 +88,8 @@ abstract class spout_base extends \core\dataformat\base {
             }
             $sheet->setName($this->sheettitle);
         }
-        $this->writer->addRow(array_values((array)$columns));
+        $row = \Box\Spout\Writer\Common\Creator\WriterEntityFactory::createRowFromArray((array)$columns);
+        $this->writer->addRow($row);
     }
 
     /**
@@ -98,7 +99,8 @@ abstract class spout_base extends \core\dataformat\base {
      * @param int $rownum
      */
     public function write_record($record, $rownum) {
-        $this->writer->addRow(array_values((array)$record));
+        $row = \Box\Spout\Writer\Common\Creator\WriterEntityFactory::createRowFromArray((array)$record);
+        $this->writer->addRow($row);
     }
 
     /**
diff --git a/lib/classes/output/checkbox_toggleall.php b/lib/classes/output/checkbox_toggleall.php
new file mode 100644 (file)
index 0000000..be81f80
--- /dev/null
@@ -0,0 +1,123 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * The renderable for core/checkbox-toggleall.
+ *
+ * @package    core
+ * @copyright  2019 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\output;
+defined('MOODLE_INTERNAL') || die();
+
+use renderable;
+use renderer_base;
+use stdClass;
+use templatable;
+
+/**
+ * The checkbox-toggleall renderable class.
+ *
+ * @package    core
+ * @copyright  2019 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class checkbox_toggleall implements renderable, templatable {
+
+    /** @var string The name of the group of checkboxes to be toggled. */
+    protected $togglegroup;
+
+    /** @var bool $ismaster Whether we're rendering for a master checkbox or a slave checkbox. */
+    protected $ismaster;
+
+    /** @var array $options The options for the checkbox. */
+    protected $options;
+
+    /** @var bool $isbutton Whether to render this as a button. Applies to master checkboxes only. */
+    protected $isbutton;
+
+    /**
+     * Constructor.
+     *
+     * @param string $togglegroup The name of the group of checkboxes to be toggled.
+     * @param bool $ismaster Whether we're rendering for a master checkbox or a slave checkbox.
+     * @param array $options The options for the checkbox. Valid options are:
+     *     <ul>
+     *         <li><b>id          </b> string - The element ID.</li>
+     *         <li><b>name        </b> string - The element name.</li>
+     *         <li><b>classes     </b> string - CSS classes that you want to add for your checkbox.</li>
+     *         <li><b>value       </b> string|int - The element's value.</li>
+     *         <li><b>checked     </b> boolean - Whether to render this initially as checked.</li>
+     *         <li><b>label       </b> string - The label for the checkbox element.</li>
+     *         <li><b>labelclasses</b> string - CSS classes that you want to add for your label.</li>
+     *         <li><b>selectall   </b> string - Master only. The language string that will be used to indicate that clicking on
+     *                                 the master will select all of the slave checkboxes. Defaults to "Select all".</li>
+     *         <li><b>deselectall </b> string - Master only. The language string that will be used to indicate that clicking on
+     *                                 the master will select all of the slave checkboxes. Defaults to "Deselect all".</li>
+     *     </ul>
+     * @param bool $isbutton Whether to render this as a button. Applies to master only.
+     */
+    public function __construct(string $togglegroup, bool $ismaster, $options = [], $isbutton = false) {
+        $this->togglegroup = $togglegroup;
+        $this->ismaster = $ismaster;
+        $this->options = $options;
+        $this->isbutton = $ismaster && $isbutton;
+    }
+
+    /**
+     * Export for template.
+     *
+     * @param renderer_base $output The renderer.
+     * @return stdClass
+     */
+    public function export_for_template(renderer_base $output) {
+        $data = (object)[
+            'togglegroup' => $this->togglegroup,
+            'id' => $this->options['id'] ?? null,
+            'name' => $this->options['name'] ?? null,
+            'value' => $this->options['value'] ?? null,
+            'classes' => $this->options['classes'] ?? null,
+            'label' => $this->options['label'] ?? null,
+            'labelclasses' => $this->options['labelclasses'] ?? null,
+            'checked' => $this->options['checked'] ?? false,
+        ];
+
+        if ($this->ismaster) {
+            $data->selectall = $this->options['selectall'] ?? get_string('selectall');
+            $data->deselectall = $this->options['deselectall'] ?? get_string('deselectall');
+        }
+
+        return $data;
+    }
+
+    /**
+     * Fetches the appropriate template for the checkbox toggle all element.
+     *
+     * @return string
+     */
+    public function get_template() {
+        if ($this->ismaster) {
+            if ($this->isbutton) {
+                return 'core/checkbox-toggleall-master-button';
+            } else {
+                return 'core/checkbox-toggleall-master';
+            }
+        }
+        return 'core/checkbox-toggleall-slave';
+    }
+}
index 2281080..a196105 100644 (file)
@@ -31,7 +31,7 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright  2016 Frédéric Massart
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class core_scss extends \Leafo\ScssPhp\Compiler {
+class core_scss extends \ScssPhp\ScssPhp\Compiler {
 
     /** @var string The path to the SCSS file. */
     protected $scssfile;
@@ -150,14 +150,14 @@ class core_scss extends \Leafo\ScssPhp\Compiler {
      * Compile child; returns a value to halt execution
      *
      * @param array $child
-     * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
+     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
      *
      * @return array|null
      */
-    protected function compileChild($child, \Leafo\ScssPhp\Formatter\OutputBlock $out) {
+    protected function compileChild($child, \ScssPhp\ScssPhp\Formatter\OutputBlock $out) {
         switch($child[0]) {
-            case \Leafo\ScssPhp\Type::T_SCSSPHP_IMPORT_ONCE:
-            case \Leafo\ScssPhp\Type::T_IMPORT:
+            case \ScssPhp\ScssPhp\Type::T_SCSSPHP_IMPORT_ONCE:
+            case \ScssPhp\ScssPhp\Type::T_IMPORT:
                 list(, $rawpath) = $child;
                 $rawpath = $this->reduce($rawpath);
                 $path = $this->compileStringContent($rawpath);
index 5b3b0d2..477bc1d 100644 (file)
@@ -11,7 +11,10 @@ namespace GeoIp2\Record;
  * confidence that the country is correct. This attribute is only available
  * from the Insights service and the GeoIP2 Enterprise database.
  * @property-read int|null $geonameId The GeoName ID for the country. This
- * attribute is returned by location services and databases.
+ * attribute is returned by all location services and databases.
+ * @property-read bool $isInEuropeanUnion This is true if the country is a
+ * member state of the European Union. This attribute is returned by all
+ * location services and databases.
  * @property-read string|null $isoCode The
  * {@link * http://en.wikipedia.org/wiki/ISO_3166-1 two-character ISO 3166-1 alpha
  * code} for the country. This attribute is returned by all location services
@@ -31,6 +34,7 @@ class Country extends AbstractPlaceRecord
     protected $validAttributes = [
         'confidence',
         'geonameId',
+        'isInEuropeanUnion',
         'isoCode',
         'names',
     ];
index c37102e..13082dd 100644 (file)
@@ -13,6 +13,9 @@ namespace GeoIp2\Record;
  * confidence that the country is correct. This attribute is only available
  * from the Insights service and the GeoIP2 Enterprise database.
  * @property-read int|null $geonameId The GeoName ID for the country.
+ * @property-read bool $isInEuropeanUnion This is true if the country is a
+ * member state of the European Union. This attribute is returned by all
+ * location services and databases.
  * @property-read string|null $isoCode The {@link http://en.wikipedia.org/wiki/ISO_3166-1
  * two-character ISO 3166-1 alpha code} for the country.
  * @property-read string|null $name The name of the country based on the locales list
@@ -28,6 +31,7 @@ class RepresentedCountry extends Country
     protected $validAttributes = [
         'confidence',
         'geonameId',
+        'isInEuropeanUnion',
         'isoCode',
         'names',
         'type',
index 582eea6..bc5f07d 100644 (file)
@@ -32,18 +32,32 @@ namespace GeoIp2\Record;
  * running on. If the system is behind a NAT, this may differ from the IP
  * address locally assigned to it. This attribute is returned by all end
  * points.
+ * @property-read bool $isAnonymous This is true if the IP address belongs to
+ * any sort of anonymous network. This property is only available from GeoIP2
+ * Precision Insights.
  * @property-read bool $isAnonymousProxy *Deprecated.* Please see our
  * {@link * https://www.maxmind.com/en/geoip2-anonymous-ip-database GeoIP2
  * Anonymous IP database} to determine whether the IP address is used by an
  * anonymizing service.
+ * @property-read bool $isAnonymousVpn This is true if the IP address belongs to
+ * an anonymous VPN system. This property is only available from GeoIP2
+ * Precision Insights.
+ * @property-read bool $isHostingProvider This is true if the IP address belongs
+ * to a hosting provider. This property is only available from GeoIP2
+ * Precision Insights.
  * @property-read bool $isLegitimateProxy This attribute is true if MaxMind
  * believes this IP address to be a legitimate proxy, such as an internal
  * VPN used by a corporation. This attribute is only available in the GeoIP2
  * Enterprise database.
+ * @property-read bool $isPublicProxy This is true if the IP address belongs to
+ * a public proxy. This property is only available from GeoIP2 Precision
+ * Insights.
  * @property-read bool $isSatelliteProvider *Deprecated.* Due to the
  * increased coverage by mobile carriers, very few satellite providers now
  * serve multiple countries. As a result, the output does not provide
  * sufficiently relevant data for us to maintain it.
+ * @property-read bool $isTorExitNode This is true if the IP address is a Tor
+ * exit node. This property is only available from GeoIP2 Precision Insights.
  * @property-read string|null $isp The name of the ISP associated with the IP
  * address. This attribute is only available from the City and Insights web
  * services and the GeoIP2 Enterprise database.
@@ -84,11 +98,16 @@ class Traits extends AbstractRecord
         'autonomousSystemOrganization',
         'connectionType',
         'domain',
+        'ipAddress',
+        'isAnonymous',
         'isAnonymousProxy',
+        'isAnonymousVpn',
+        'isHostingProvider',
         'isLegitimateProxy',
-        'isSatelliteProvider',
         'isp',
-        'ipAddress',
+        'isPublicProxy',
+        'isSatelliteProvider',
+        'isTorExitNode',
         'organization',
         'userType',
     ];
index fbc9a5f..1a1ccf1 100644 (file)
@@ -30,7 +30,7 @@ use MaxMind\WebService\Client as WsClient;
  * ## Usage ##
  *
  * The basic API for this class is the same for all of the web service end
- * points. First you create a web service object with your MaxMind `$userId`
+ * points. First you create a web service object with your MaxMind `$accountId`
  * and `$licenseKey`, then you call the method corresponding to a specific end
  * point, passing it the IP address you want to look up.
  *
@@ -47,12 +47,12 @@ class Client implements ProviderInterface
     private $client;
     private static $basePath = '/geoip/v2.1';
 
-    const VERSION = 'v2.6.0';
+    const VERSION = 'v2.9.0';
 
     /**
      * Constructor.
      *
-     * @param int    $userId     your MaxMind user ID
+     * @param int    $accountId  your MaxMind account ID
      * @param string $licenseKey your MaxMind license key
      * @param array  $locales    list of locale codes to use in name property
      *                           from most preferred to least preferred
@@ -65,7 +65,7 @@ class Client implements ProviderInterface
      *                           `http://username:password@127.0.0.1:10`.
      */
     public function __construct(
-        $userId,
+        $accountId,
         $licenseKey,
         $locales = ['en'],
         $options = []
@@ -84,7 +84,7 @@ class Client implements ProviderInterface
 
         $options['userAgent'] = $this->userAgent();
 
-        $this->client = new WsClient($userId, $licenseKey, $options);
+        $this->client = new WsClient($accountId, $licenseKey, $options);
     }
 
     private function userAgent()
@@ -102,7 +102,7 @@ class Client implements ProviderInterface
      * @throws \GeoIp2\Exception\AddressNotFoundException if the address you
      *                                                    provided is not in our database (e.g., a private address).
      * @throws \GeoIp2\Exception\AuthenticationException  if there is a problem
-     *                                                    with the user ID or license key that you provided
+     *                                                    with the account ID or license key that you provided
      * @throws \GeoIp2\Exception\OutOfQueriesException    if your account is out
      *                                                    of queries
      * @throws \GeoIp2\Exception\InvalidRequestException} if your request was received by the web service but is
@@ -133,7 +133,7 @@ class Client implements ProviderInterface
      * @throws \GeoIp2\Exception\AddressNotFoundException if the address you provided is not in our database (e.g.,
      *                                                    a private address).
      * @throws \GeoIp2\Exception\AuthenticationException  if there is a problem
-     *                                                    with the user ID or license key that you provided
+     *                                                    with the account ID or license key that you provided
      * @throws \GeoIp2\Exception\OutOfQueriesException    if your account is out of queries
      * @throws \GeoIp2\Exception\InvalidRequestException} if your request was received by the web service but is
      *                                                    invalid for some other reason.  This may indicate an
@@ -164,7 +164,7 @@ class Client implements ProviderInterface
      * @throws \GeoIp2\Exception\AddressNotFoundException if the address you
      *                                                    provided is not in our database (e.g., a private address).
      * @throws \GeoIp2\Exception\AuthenticationException  if there is a problem
-     *                                                    with the user ID or license key that you provided
+     *                                                    with the account ID or license key that you provided
      * @throws \GeoIp2\Exception\OutOfQueriesException    if your account is out
      *                                                    of queries
      * @throws \GeoIp2\Exception\InvalidRequestException} if your request was received by the web service but is
index 129bf6c..745f7bb 100644 (file)
@@ -29,15 +29,16 @@ class Reader
      * be a valid MaxMind DB file such as a GeoIp2 database file.
      *
      * @param string $database
-     *            the MaxMind DB file to use.
-     * @throws \InvalidArgumentException for invalid database path or unknown arguments
+     *                         the MaxMind DB file to use
+     *
+     * @throws \InvalidArgumentException                   for invalid database path or unknown arguments
      * @throws \MaxMind\Db\Reader\InvalidDatabaseException
-     *             if the database is invalid or there is an error reading
-     *             from it.
+     *                                                     if the database is invalid or there is an error reading
+     *                                                     from it
      */
     public function __construct($database)
     {
-        if (func_num_args() != 1) {
+        if (\func_num_args() !== 1) {
             throw new \InvalidArgumentException(
                 'The constructor takes exactly one argument.'
             );
@@ -75,23 +76,25 @@ class Reader
      * Looks up the <code>address</code> in the MaxMind DB.
      *
      * @param string $ipAddress
-     *            the IP address to look up.
-     * @return array the record for the IP address.
-     * @throws \BadMethodCallException if this method is called on a closed database.
-     * @throws \InvalidArgumentException if something other than a single IP address is passed to the method.
+     *                          the IP address to look up
+     *
+     * @throws \BadMethodCallException   if this method is called on a closed database
+     * @throws \InvalidArgumentException if something other than a single IP address is passed to the method
      * @throws InvalidDatabaseException
-     *             if the database is invalid or there is an error reading
-     *             from it.
+     *                                   if the database is invalid or there is an error reading
+     *                                   from it
+     *
+     * @return array the record for the IP address
      */
     public function get($ipAddress)
     {
-        if (func_num_args() != 1) {
+        if (\func_num_args() !== 1) {
             throw new \InvalidArgumentException(
                 'Method takes exactly one argument.'
             );
         }
 
-        if (!is_resource($this->fileHandle)) {
+        if (!\is_resource($this->fileHandle)) {
             throw new \BadMethodCallException(
                 'Attempt to read from a closed MaxMind DB.'
             );
@@ -103,16 +106,17 @@ class Reader
             );
         }
 
-        if ($this->metadata->ipVersion == 4 && strrpos($ipAddress, ':')) {
+        if ($this->metadata->ipVersion === 4 && strrpos($ipAddress, ':')) {
             throw new \InvalidArgumentException(
                 "Error looking up $ipAddress. You attempted to look up an"
-                . " IPv6 address in an IPv4-only database."
+                . ' IPv6 address in an IPv4-only database.'
             );
         }
         $pointer = $this->findAddressInTree($ipAddress);
-        if ($pointer == 0) {
+        if ($pointer === 0) {
             return null;
         }
+
         return $this->resolveDataPointer($pointer);
     }
 
@@ -121,13 +125,13 @@ class Reader
         // XXX - could simplify. Done as a byte array to ease porting
         $rawAddress = array_merge(unpack('C*', inet_pton($ipAddress)));
 
-        $bitCount = count($rawAddress) * 8;
+        $bitCount = \count($rawAddress) * 8;
 
         // The first node of the tree is always node 0, at the beginning of the
         // value
         $node = $this->startNode($bitCount);
 
-        for ($i = 0; $i < $bitCount; $i++) {
+        for ($i = 0; $i < $bitCount; ++$i) {
             if ($node >= $this->metadata->nodeCount) {
                 break;
             }
@@ -136,22 +140,21 @@ class Reader
 
             $node = $this->readNode($node, $bit);
         }
-        if ($node == $this->metadata->nodeCount) {
+        if ($node === $this->metadata->nodeCount) {
             // Record is empty
             return 0;
         } elseif ($node > $this->metadata->nodeCount) {
             // Record is a data pointer
             return $node;
         }
-        throw new InvalidDatabaseException("Something bad happened");
+        throw new InvalidDatabaseException('Something bad happened');
     }
 
-
     private function startNode($length)
     {
         // Check if we are looking up an IPv4 address in an IPv6 tree. If this
         // is the case, we can skip over the first 96 nodes.
-        if ($this->metadata->ipVersion == 6 && $length == 32) {
+        if ($this->metadata->ipVersion === 6 && $length === 32) {
             return $this->ipV4StartNode();
         }
         // The first node of the tree is always node 0, at the beginning of the
@@ -163,19 +166,20 @@ class Reader
     {
         // This is a defensive check. There is no reason to call this when you
         // have an IPv4 tree.
-        if ($this->metadata->ipVersion == 4) {
+        if ($this->metadata->ipVersion === 4) {
             return 0;
         }
 
-        if ($this->ipV4Start != 0) {
+        if ($this->ipV4Start) {
             return $this->ipV4Start;
         }
         $node = 0;
 
-        for ($i = 0; $i < 96 && $node < $this->metadata->nodeCount; $i++) {
+        for ($i = 0; $i < 96 && $node < $this->metadata->nodeCount; ++$i) {
             $node = $this->readNode($node, 0);
         }
         $this->ipV4Start = $node;
+
         return $node;
     }
 
@@ -188,21 +192,24 @@ class Reader
             case 24:
                 $bytes = Util::read($this->fileHandle, $baseOffset + $index * 3, 3);
                 list(, $node) = unpack('N', "\x00" . $bytes);
+
                 return $node;
             case 28:
                 $middleByte = Util::read($this->fileHandle, $baseOffset + 3, 1);
                 list(, $middle) = unpack('C', $middleByte);
-                if ($index == 0) {
+                if ($index === 0) {
                     $middle = (0xF0 & $middle) >> 4;
                 } else {
                     $middle = 0x0F & $middle;
                 }
                 $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 3);
-                list(, $node) = unpack('N', chr($middle) . $bytes);
+                list(, $node) = unpack('N', \chr($middle) . $bytes);
+
                 return $node;
             case 32:
                 $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 4);
                 list(, $node) = unpack('N', $bytes);
+
                 return $node;
             default:
                 throw new InvalidDatabaseException(
@@ -223,6 +230,7 @@ class Reader
         }
 
         list($data) = $this->decoder->decode($resolved);
+
         return $data;
     }
 
@@ -241,14 +249,15 @@ class Reader
         $metadataMaxLengthExcludingMarker
             = min(self::$METADATA_MAX_SIZE, $fileSize) - $markerLength;
 
-        for ($i = 0; $i <= $metadataMaxLengthExcludingMarker; $i++) {
-            for ($j = 0; $j < $markerLength; $j++) {
+        for ($i = 0; $i <= $metadataMaxLengthExcludingMarker; ++$i) {
+            for ($j = 0; $j < $markerLength; ++$j) {
                 fseek($handle, $fileSize - $i - $j - 1);
                 $matchBit = fgetc($handle);
-                if ($matchBit != $marker[$markerLength - $j - 1]) {
+                if ($matchBit !== $marker[$markerLength - $j - 1]) {
                     continue 2;
                 }
             }
+
             return $fileSize - $i;
         }
         throw new InvalidDatabaseException(
@@ -258,13 +267,14 @@ class Reader
     }
 
     /**
-     * @throws \InvalidArgumentException if arguments are passed to the method.
-     * @throws \BadMethodCallException if the database has been closed.
-     * @return Metadata object for the database.
+     * @throws \InvalidArgumentException if arguments are passed to the method
+     * @throws \BadMethodCallException   if the database has been closed
+     *
+     * @return Metadata object for the database
      */
     public function metadata()
     {
-        if (func_num_args()) {
+        if (\func_num_args()) {
             throw new \InvalidArgumentException(
                 'Method takes no arguments.'
             );
@@ -272,7 +282,7 @@ class Reader
 
         // Not technically required, but this makes it consistent with
         // C extension and it allows us to change our implementation later.
-        if (!is_resource($this->fileHandle)) {
+        if (!\is_resource($this->fileHandle)) {
             throw new \BadMethodCallException(
                 'Attempt to read from a closed MaxMind DB.'
             );
@@ -285,11 +295,11 @@ class Reader
      * Closes the MaxMind DB and returns resources to the system.
      *
      * @throws \Exception
-     *             if an I/O error occurs.
+     *                    if an I/O error occurs
      */
     public function close()
     {
-        if (!is_resource($this->fileHandle)) {
+        if (!\is_resource($this->fileHandle)) {
             throw new \BadMethodCallException(
                 'Attempt to close a closed MaxMind DB.'
             );
index 4575b27..a71b3de 100644 (file)
@@ -2,36 +2,35 @@
 
 namespace MaxMind\Db\Reader;
 
-use MaxMind\Db\Reader\InvalidDatabaseException;
-use MaxMind\Db\Reader\Util;
+// @codingStandardsIgnoreLine
+// We subtract 1 from the log to protect against precision loss.
+\define(__NAMESPACE__ . '\_MM_MAX_INT_BYTES', (log(PHP_INT_MAX, 2) - 1) / 8);
 
 class Decoder
 {
-
     private $fileStream;
     private $pointerBase;
+    private $pointerBaseByteSize;
     // This is only used for unit testing
     private $pointerTestHack;
     private $switchByteOrder;
 
-    private $types = array(
-        0 => 'extended',
-        1 => 'pointer',
-        2 => 'utf8_string',
-        3 => 'double',
-        4 => 'bytes',
-        5 => 'uint16',
-        6 => 'uint32',
-        7 => 'map',
-        8 => 'int32',
-        9 => 'uint64',
-        10 => 'uint128',
-        11 => 'array',
-        12 => 'container',
-        13 => 'end_marker',
-        14 => 'boolean',
-        15 => 'float',
-    );
+    const _EXTENDED = 0;
+    const _POINTER = 1;
+    const _UTF8_STRING = 2;
+    const _DOUBLE = 3;
+    const _BYTES = 4;
+    const _UINT16 = 5;
+    const _UINT32 = 6;
+    const _MAP = 7;
+    const _INT32 = 8;
+    const _UINT64 = 9;
+    const _UINT128 = 10;
+    const _ARRAY = 11;
+    const _CONTAINER = 12;
+    const _END_MARKER = 13;
+    const _BOOLEAN = 14;
+    const _FLOAT = 15;
 
     public function __construct(
         $fileStream,
@@ -40,57 +39,57 @@ class Decoder
     ) {
         $this->fileStream = $fileStream;
         $this->pointerBase = $pointerBase;
+
+        $this->pointerBaseByteSize = $pointerBase > 0 ? log($pointerBase, 2) / 8 : 0;
         $this->pointerTestHack = $pointerTestHack;
 
         $this->switchByteOrder = $this->isPlatformLittleEndian();
     }
 
-
     public function decode($offset)
     {
         list(, $ctrlByte) = unpack(
             'C',
             Util::read($this->fileStream, $offset, 1)
         );
-        $offset++;
+        ++$offset;
 
-        $type = $this->types[$ctrlByte >> 5];
+        $type = $ctrlByte >> 5;
 
         // Pointers are a special case, we don't read the next $size bytes, we
         // use the size to determine the length of the pointer and then follow
         // it.
-        if ($type == 'pointer') {
+        if ($type === self::_POINTER) {
             list($pointer, $offset) = $this->decodePointer($ctrlByte, $offset);
 
             // for unit testing
             if ($this->pointerTestHack) {
-                return array($pointer);
+                return [$pointer];
             }
 
             list($result) = $this->decode($pointer);
 
-            return array($result, $offset);
+            return [$result, $offset];
         }
 
-        if ($type == 'extended') {
+        if ($type === self::_EXTENDED) {
             list(, $nextByte) = unpack(
                 'C',
                 Util::read($this->fileStream, $offset, 1)
             );
 
-            $typeNum = $nextByte + 7;
+            $type = $nextByte + 7;
 
-            if ($typeNum < 8) {
+            if ($type < 8) {
                 throw new InvalidDatabaseException(
-                    "Something went horribly wrong in the decoder. An extended type "
-                    . "resolved to a type number < 8 ("
-                    . $this->types[$typeNum]
-                    . ")"
+                    'Something went horribly wrong in the decoder. An extended type '
+                    . 'resolved to a type number < 8 ('
+                    . $type
+                    . ')'
                 );
             }
 
-            $type = $this->types[$typeNum];
-            $offset++;
+            ++$offset;
         }
 
         list($size, $offset) = $this->sizeFromCtrlByte($ctrlByte, $offset);
@@ -101,45 +100,45 @@ class Decoder
     private function decodeByType($type, $offset, $size)
     {
         switch ($type) {
-            case 'map':
+            case self::_MAP:
                 return $this->decodeMap($size, $offset);
-            case 'array':
+            case self::_ARRAY:
                 return $this->decodeArray($size, $offset);
-            case 'boolean':
-                return array($this->decodeBoolean($size), $offset);
+            case self::_BOOLEAN:
+                return [$this->decodeBoolean($size), $offset];
         }
 
         $newOffset = $offset + $size;
         $bytes = Util::read($this->fileStream, $offset, $size);
         switch ($type) {
-            case 'utf8_string':
-                return array($this->decodeString($bytes), $newOffset);
-            case 'double':
+            case self::_BYTES:
+            case self::_UTF8_STRING:
+                return [$bytes, $newOffset];
+            case self::_DOUBLE:
                 $this->verifySize(8, $size);
-                return array($this->decodeDouble($bytes), $newOffset);
-            case 'float':
+
+                return [$this->decodeDouble($bytes), $newOffset];
+            case self::_FLOAT:
                 $this->verifySize(4, $size);
-                return array($this->decodeFloat($bytes), $newOffset);
-            case 'bytes':
-                return array($bytes, $newOffset);
-            case 'uint16':
-            case 'uint32':
-                return array($this->decodeUint($bytes), $newOffset);
-            case 'int32':
-                return array($this->decodeInt32($bytes), $newOffset);
-            case 'uint64':
-            case 'uint128':
-                return array($this->decodeBigUint($bytes, $size), $newOffset);
+
+                return [$this->decodeFloat($bytes), $newOffset];
+            case self::_INT32:
+                return [$this->decodeInt32($bytes, $size), $newOffset];
+            case self::_UINT16:
+            case self::_UINT32:
+            case self::_UINT64:
+            case self::_UINT128:
+                return [$this->decodeUint($bytes, $size), $newOffset];
             default:
                 throw new InvalidDatabaseException(
-                    "Unknown or unexpected type: " . $type
+                    'Unknown or unexpected type: ' . $type
                 );
         }
     }
 
     private function verifySize($expected, $actual)
     {
-        if ($expected != $actual) {
+        if ($expected !== $actual) {
             throw new InvalidDatabaseException(
                 "The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)"
             );
@@ -148,63 +147,83 @@ class Decoder
 
     private function decodeArray($size, $offset)
     {
-        $array = array();
+        $array = [];
 
-        for ($i = 0; $i < $size; $i++) {
+        for ($i = 0; $i < $size; ++$i) {
             list($value, $offset) = $this->decode($offset);
             array_push($array, $value);
         }
 
-        return array($array, $offset);
+        return [$array, $offset];
     }
 
     private function decodeBoolean($size)
     {
-        return $size == 0 ? false : true;
+        return $size === 0 ? false : true;
     }
 
     private function decodeDouble($bits)
     {
-        // XXX - Assumes IEEE 754 double on platform
+        // This assumes IEEE 754 doubles, but most (all?) modern platforms
+        // use them.
+        //
+        // We are not using the "E" format as that was only added in
+        // 7.0.15 and 7.1.1. As such, we must switch byte order on
+        // little endian machines.
         list(, $double) = unpack('d', $this->maybeSwitchByteOrder($bits));
+
         return $double;
     }
 
     private function decodeFloat($bits)
     {
-        // XXX - Assumes IEEE 754 floats on platform
+        // This assumes IEEE 754 floats, but most (all?) modern platforms
+        // use them.
+        //
+        // We are not using the "G" format as that was only added in
+        // 7.0.15 and 7.1.1. As such, we must switch byte order on
+        // little endian machines.
         list(, $float) = unpack('f', $this->maybeSwitchByteOrder($bits));
+
         return $float;
     }
 
-    private function decodeInt32($bytes)
+    private function decodeInt32($bytes, $size)
     {
-        $bytes = $this->zeroPadLeft($bytes, 4);
+        switch ($size) {
+            case 0:
+                return 0;
+            case 1:
+            case 2:
+            case 3:
+                $bytes = str_pad($bytes, 4, "\x00", STR_PAD_LEFT);
+                break;
+            case 4:
+                break;
+            default:
+                throw new InvalidDatabaseException(
+                    "The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)"
+                );
+        }
+
         list(, $int) = unpack('l', $this->maybeSwitchByteOrder($bytes));
+
         return $int;
     }
 
     private function decodeMap($size, $offset)
     {
+        $map = [];
 
-        $map = array();
-
-        for ($i = 0; $i < $size; $i++) {
+        for ($i = 0; $i < $size; ++$i) {
             list($key, $offset) = $this->decode($offset);
             list($value, $offset) = $this->decode($offset);
             $map[$key] = $value;
         }
 
-        return array($map, $offset);
+        return [$map, $offset];
     }
 
-    private $pointerValueOffset = array(
-        1 => 0,
-        2 => 2048,
-        3 => 526336,
-        4 => 0,
-    );
-
     private function decodePointer($ctrlByte, $offset)
     {
         $pointerSize = (($ctrlByte >> 3) & 0x3) + 1;
@@ -212,87 +231,99 @@ class Decoder
         $buffer = Util::read($this->fileStream, $offset, $pointerSize);
         $offset = $offset + $pointerSize;
 
-        $packed = $pointerSize == 4
-            ? $buffer
-            : (pack('C', $ctrlByte & 0x7)) . $buffer;
-
-        $unpacked = $this->decodeUint($packed);
-        $pointer = $unpacked + $this->pointerBase
-            + $this->pointerValueOffset[$pointerSize];
-
-        return array($pointer, $offset);
-    }
+        switch ($pointerSize) {
+            case 1:
+                $packed = (pack('C', $ctrlByte & 0x7)) . $buffer;
+                list(, $pointer) = unpack('n', $packed);
+                $pointer += $this->pointerBase;
+                break;
+            case 2:
+                $packed = "\x00" . (pack('C', $ctrlByte & 0x7)) . $buffer;
+                list(, $pointer) = unpack('N', $packed);
+                $pointer += $this->pointerBase + 2048;
+                break;
+            case 3:
+                $packed = (pack('C', $ctrlByte & 0x7)) . $buffer;
+
+                // It is safe to use 'N' here, even on 32 bit machines as the
+                // first bit is 0.
+                list(, $pointer) = unpack('N', $packed);
+                $pointer += $this->pointerBase + 526336;
+                break;
+            case 4:
+                // We cannot use unpack here as we might overflow on 32 bit
+                // machines
+                $pointerOffset = $this->decodeUint($buffer, $pointerSize);
+
+                $byteLength = $pointerSize + $this->pointerBaseByteSize;
+
+                if ($byteLength <= _MM_MAX_INT_BYTES) {
+                    $pointer = $pointerOffset + $this->pointerBase;
+                } elseif (\extension_loaded('gmp')) {
+                    $pointer = gmp_strval(gmp_add($pointerOffset, $this->pointerBase));
+                } elseif (\extension_loaded('bcmath')) {
+                    $pointer = bcadd($pointerOffset, $this->pointerBase);
+                } else {
+                    throw new \RuntimeException(
+                        'The gmp or bcmath extension must be installed to read this database.'
+                    );
+                }
+        }
 
-    private function decodeUint($bytes)
-    {
-        list(, $int) = unpack('N', $this->zeroPadLeft($bytes, 4));
-        return $int;
+        return [$pointer, $offset];
     }
 
-    private function decodeBigUint($bytes, $byteLength)
+    private function decodeUint($bytes, $byteLength)
     {
-        $maxUintBytes = log(PHP_INT_MAX, 2) / 8;
-
-        if ($byteLength == 0) {
+        if ($byteLength === 0) {
             return 0;
         }
 
-        $numberOfLongs = ceil($byteLength / 4);
-        $paddedLength = $numberOfLongs * 4;
-        $paddedBytes = $this->zeroPadLeft($bytes, $paddedLength);
-        $unpacked = array_merge(unpack("N$numberOfLongs", $paddedBytes));
-
         $integer = 0;
 
-        // 2^32
-        $twoTo32 = '4294967296';
+        for ($i = 0; $i < $byteLength; ++$i) {
+            $part = \ord($bytes[$i]);
 
-        foreach ($unpacked as $part) {
             // We only use gmp or bcmath if the final value is too big
-            if ($byteLength <= $maxUintBytes) {
-                $integer = ($integer << 32) + $part;
-            } elseif (extension_loaded('gmp')) {
-                $integer = gmp_strval(gmp_add(gmp_mul($integer, $twoTo32), $part));
-            } elseif (extension_loaded('bcmath')) {
-                $integer = bcadd(bcmul($integer, $twoTo32), $part);
+            if ($byteLength <= _MM_MAX_INT_BYTES) {
+                $integer = ($integer << 8) + $part;
+            } elseif (\extension_loaded('gmp')) {
+                $integer = gmp_strval(gmp_add(gmp_mul($integer, 256), $part));
+            } elseif (\extension_loaded('bcmath')) {
+                $integer = bcadd(bcmul($integer, 256), $part);
             } else {
                 throw new \RuntimeException(
                     'The gmp or bcmath extension must be installed to read this database.'
                 );
             }
         }
-        return $integer;
-    }
 
-    private function decodeString($bytes)
-    {
-        // XXX - NOOP. As far as I know, the end user has to explicitly set the
-        // encoding in PHP. Strings are just bytes.
-        return $bytes;
+        return $integer;
     }
 
     private function sizeFromCtrlByte($ctrlByte, $offset)
     {
         $size = $ctrlByte & 0x1f;
-        $bytesToRead = $size < 29 ? 0 : $size - 28;
+
+        if ($size < 29) {
+            return [$size, $offset];
+        }
+
+        $bytesToRead = $size - 28;
         $bytes = Util::read($this->fileStream, $offset, $bytesToRead);
-        $decoded = $this->decodeUint($bytes);
 
-        if ($size == 29) {
-            $size = 29 + $decoded;
-        } elseif ($size == 30) {
-            $size = 285 + $decoded;
+        if ($size === 29) {
+            $size = 29 + \ord($bytes);
+        } elseif ($size === 30) {
+            list(, $adjust) = unpack('n', $bytes);
+            $size = 285 + $adjust;
         } elseif ($size > 30) {
-            $size = ($decoded & (0x0FFFFFFF >> (32 - (8 * $bytesToRead))))
+            list(, $adjust) = unpack('N', "\x00" . $bytes);
+            $size = ($adjust & (0x0FFFFFFF >> (32 - (8 * $bytesToRead))))
                 + 65821;
         }
 
-        return array($size, $offset + $bytesToRead);
-    }
-
-    private function zeroPadLeft($content, $desiredLength)
-    {
-        return str_pad($content, $desiredLength, "\x00", STR_PAD_LEFT);
+        return [$size, $offset + $bytesToRead];
     }
 
     private function maybeSwitchByteOrder($bytes)
@@ -304,6 +335,7 @@ class Decoder
     {
         $testint = 0x00FF;
         $packed = pack('S', $testint);
+
         return $testint === current(unpack('v', $packed));
     }
 }
index 3ac908b..4efdd3d 100644 (file)
@@ -5,35 +5,27 @@ namespace MaxMind\Db\Reader;
 /**
  * This class provides the metadata for the MaxMind DB file.
  *
- * @property integer nodeCount This is an unsigned 32-bit integer indicating
+ * @property int nodeCount This is an unsigned 32-bit integer indicating
  * the number of nodes in the search tree.
- *
- * @property integer recordSize This is an unsigned 16-bit integer. It
+ * @property int recordSize This is an unsigned 16-bit integer. It
  * indicates the number of bits in a record in the search tree. Note that each
  * node consists of two records.
- *
- * @property integer ipVersion This is an unsigned 16-bit integer which is
+ * @property int ipVersion This is an unsigned 16-bit integer which is
  * always 4 or 6. It indicates whether the database contains IPv4 or IPv6
  * address data.
- *
  * @property string databaseType This is a string that indicates the structure
  * of each data record associated with an IP address. The actual definition of
  * these structures is left up to the database creator.
- *
  * @property array languages An array of strings, each of which is a language
  * code. A given record may contain data items that have been localized to
  * some or all of these languages. This may be undefined.
- *
- * @property integer binaryFormatMajorVersion This is an unsigned 16-bit
+ * @property int binaryFormatMajorVersion This is an unsigned 16-bit
  * integer indicating the major version number for the database's binary
  * format.
- *
- * @property integer binaryFormatMinorVersion This is an unsigned 16-bit
+ * @property int binaryFormatMinorVersion This is an unsigned 16-bit
  * integer indicating the minor version number for the database's binary format.
- *
- * @property integer buildEpoch This is an unsigned 64-bit integer that
+ * @property int buildEpoch This is an unsigned 64-bit integer that
  * contains the database build timestamp as a Unix epoch value.
- *
  * @property array description This key will always point to a map
  * (associative array). The keys of that map will be language codes, and the
  * values will be a description in that language as a UTF-8 string. May be
index dc8ec80..87ebbf1 100644 (file)
@@ -2,16 +2,14 @@
 
 namespace MaxMind\Db\Reader;
 
-use MaxMind\Db\Reader\InvalidDatabaseException;
-
 class Util
 {
     public static function read($stream, $offset, $numberOfBytes)
     {
-        if ($numberOfBytes == 0) {
+        if ($numberOfBytes === 0) {
             return '';
         }
-        if (fseek($stream, $offset) == 0) {
+        if (fseek($stream, $offset) === 0) {
             $value = fread($stream, $numberOfBytes);
 
             // We check that the number of bytes read is equal to the number
@@ -22,7 +20,7 @@ class Util
             }
         }
         throw new InvalidDatabaseException(
-            "The MaxMind DB file contains bad data"
+            'The MaxMind DB file contains bad data'
         );
     }
 }
index e960210..8b90c90 100644 (file)
@@ -16,15 +16,15 @@ Installation
 ------------
 
 1) Download the latest versions of GeoIP2-php and MaxMind-DB-Reader-php
-wget https://github.com/maxmind/GeoIP2-php/archive/v2.6.0.zip
-wget https://github.com/maxmind/MaxMind-DB-Reader-php/archive/v1.1.3.zip
+wget https://github.com/maxmind/GeoIP2-php/archive/v2.9.0.zip
+wget https://github.com/maxmind/MaxMind-DB-Reader-php/archive/v1.4.1.zip
 
 2) Unzip the archives
-unzip v2.6.0.zip
-unzip v1.1.3.zip
+unzip v2.9.0.zip
+unzip v1.4.1.zip
 
 3) Move the source code directories into place
-mv GeoIP2-php-2.6.0/src/ /path/to/moodle/lib/maxmind/GeoIp2/
-mv MaxMind-DB-Reader-php-1.1.3/src/MaxMind/ /path/to/moodle/lib/maxmind/MaxMind/
+mv GeoIP2-php-2.9.0/src/ /path/to/moodle/lib/maxmind/GeoIp2/
+mv MaxMind-DB-Reader-php-1.4.1/src/MaxMind/ /path/to/moodle/lib/maxmind/MaxMind/
 
 4) Run unit tests on iplookup/tests/geoip_test.php with PHPUNIT_LONGTEST defined.
diff --git a/lib/minify/matthiasmullie-minify/LICENSE b/lib/minify/matthiasmullie-minify/LICENSE
new file mode 100755 (executable)
index 0000000..0c0d08a
--- /dev/null
@@ -0,0 +1,18 @@
+Copyright (c) 2012 Matthias Mullie
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
index ec96ebc..e5a4669 100644 (file)
@@ -1,4 +1,13 @@
 <?php
+/**
+ * CSS Minifier
+ *
+ * Please report bugs on https://github.com/matthiasmullie/minify/issues
+ *
+ * @author Matthias Mullie <minify@mullie.eu>
+ * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
+ * @license MIT License
+ */
 
 namespace MatthiasMullie\Minify;
 
@@ -7,10 +16,11 @@ use MatthiasMullie\PathConverter\ConverterInterface;
 use MatthiasMullie\PathConverter\Converter;
 
 /**
- * CSS minifier.
+ * CSS minifier
  *
  * Please report bugs on https://github.com/matthiasmullie/minify/issues
  *
+ * @package Minify
  * @author Matthias Mullie <minify@mullie.eu>
  * @author Tijs Verkoyen <minify@verkoyen.eu>
  * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
@@ -19,12 +29,12 @@ use MatthiasMullie\PathConverter\Converter;
 class CSS extends Minify
 {
     /**
-     * @var int
+     * @var int maximum inport size in kB
      */
     protected $maxImportSize = 5;
 
     /**
-     * @var string[]
+     * @var string[] valid import extensions
      */
     protected $importExtensions = array(
         'gif' => 'data:image/gif',
@@ -76,14 +86,14 @@ class CSS extends Minify
      */
     protected function moveImportsToTop($content)
     {
-        if (preg_match_all('/@import[^;]+;/', $content, $matches)) {
+        if (preg_match_all('/(;?)(@import (?<url>url\()?(?P<quotes>["\']?).+?(?P=quotes)(?(url)\)));?/', $content, $matches)) {
             // remove from content
             foreach ($matches[0] as $import) {
                 $content = str_replace($import, '', $content);
             }
 
             // add to top
-            $content = implode('', $matches[0]).$content;
+            $content = implode(';', $matches[2]).';'.trim($content, ';');
         }
 
         return $content;
@@ -207,6 +217,8 @@ class CSS extends Minify
             // grab referenced file & minify it (which may include importing
             // yet other @import statements recursively)
             $minifier = new static($importPath);
+            $minifier->setMaxImportSize($this->maxImportSize);
+            $minifier->setImportExtensions($this->importExtensions);
             $importContent = $minifier->execute($source, $parents);
 
             // check if this is only valid for certain media
@@ -295,10 +307,11 @@ class CSS extends Minify
              */
             $this->extractStrings();
             $this->stripComments();
+            $this->extractCalcs();
             $css = $this->replace($css);
 
             $css = $this->stripWhitespace($css);
-            $css = $this->shortenHex($css);
+            $css = $this->shortenColors($css);
             $css = $this->shortenZeroes($css);
             $css = $this->shortenFontWeights($css);
             $css = $this->stripEmptyTags($css);
@@ -469,12 +482,16 @@ class CSS extends Minify
      *
      * @return string
      */
-    protected function shortenHex($content)
+    protected function shortenColors($content)
     {
-        $content = preg_replace('/(?<=[: ])#([0-9a-z])\\1([0-9a-z])\\2([0-9a-z])\\3(?=[; }])/i', '#$1$2$3', $content);
+        $content = preg_replace('/(?<=[: ])#([0-9a-z])\\1([0-9a-z])\\2([0-9a-z])\\3(?:([0-9a-z])\\4)?(?=[; }])/i', '#$1$2$3$4', $content);
+
+        // remove alpha channel if it's pointless...
+        $content = preg_replace('/(?<=[: ])#([0-9a-z]{6})ff?(?=[; }])/i', '#$1', $content);
+        $content = preg_replace('/(?<=[: ])#([0-9a-z]{3})f?(?=[; }])/i', '#$1', $content);
 
-        // we can shorten some even more by replacing them with their color name
         $colors = array(
+            // we can shorten some even more by replacing them with their color name
             '#F0FFFF' => 'azure',
             '#F5F5DC' => 'beige',
             '#A52A2A' => 'brown',
@@ -502,6 +519,9 @@ class CSS extends Minify
             '#FF6347' => 'tomato',
             '#EE82EE' => 'violet',
             '#F5DEB3' => 'wheat',
+            // or the other way around
+            'WHITE' => '#fff',
+            'BLACK' => '#000',
         );
 
         return preg_replace_callback(
@@ -543,6 +563,12 @@ class CSS extends Minify
      */
     protected function shortenZeroes($content)
     {
+        // we don't want to strip units in `calc()` expressions:
+        // `5px - 0px` is valid, but `5px - 0` is not
+        // `10px * 0` is valid (equates to 0), and so is `10 * 0px`, but
+        // `10 * 0` is invalid
+        // we've extracted calcs earlier, so we don't need to worry about this
+
         // reusable bits of code throughout these regexes:
         // before & after are used to make sure we don't match lose unintended
         // 0-like values (e.g. in #000, or in http://url/1.0)
@@ -571,28 +597,11 @@ class CSS extends Minify
         // strip negative zeroes (-0 -> 0) & truncate zeroes (00 -> 0)
         $content = preg_replace('/'.$before.'-?0+'.$units.'?'.$after.'/', '0\\1', $content);
 
-        // remove zeroes where they make no sense in calc: e.g. calc(100px - 0)
-        // the 0 doesn't have any effect, and this isn't even valid without unit
-        // strip all `+ 0` or `- 0` occurrences: calc(10% + 0) -> calc(10%)
-        // looped because there may be multiple 0s inside 1 group of parentheses
-        do {
-            $previous = $content;
-            $content = preg_replace('/\(([^\(\)]+) [\+\-] 0( [^\(\)]+)?\)/', '(\\1\\2)', $content);
-        } while ($content !== $previous);
-        // strip all `0 +` occurrences: calc(0 + 10%) -> calc(10%)
-        $content = preg_replace('/\(0 \+ ([^\(\)]+)\)/', '(\\1)', $content);
-        // strip all `0 -` occurrences: calc(0 - 10%) -> calc(-10%)
-        $content = preg_replace('/\(0 \- ([^\(\)]+)\)/', '(-\\1)', $content);
-        // I'm not going to attempt to optimize away `x * 0` instances:
-        // it's dumb enough code already that it likely won't occur, and it's
-        // too complex to do right (order of operations would have to be
-        // respected etc)
-        // what I cared about most here was fixing incorrectly truncated units
-
-        // IE doesn't seem to understand a unitless flex-basis value, so let's
-        // add it in again (make it `%`, which is only 1 char: 0%, 0px, 0
-        // anything, it's all just the same)
-        $content = preg_replace('/flex:([^ ]+ [^ ]+ )0([;\}])/', 'flex:${1}0%${2}', $content);
+        // IE doesn't seem to understand a unitless flex-basis value (correct -
+        // it goes against the spec), so let's add it in again (make it `%`,
+        // which is only 1 char: 0%, 0px, 0 anything, it's all just the same)
+        // @see https://developer.mozilla.org/nl/docs/Web/CSS/flex
+        $content = preg_replace('/flex:([0-9]+\s[0-9]+\s)0([;\}])/', 'flex:${1}0%${2}', $content);
         $content = preg_replace('/flex-basis:0([;\}])/', 'flex-basis:0%${1}', $content);
 
         return $content;
@@ -607,7 +616,10 @@ class CSS extends Minify
      */
     protected function stripEmptyTags($content)
     {
-        return preg_replace('/(^|\}|;)[^\{\};]+\{\s*\}/', '\\1', $content);
+        $content = preg_replace('/(?<=^)[^\{\};]+\{\s*\}/', '', $content);
+        $content = preg_replace('/(?<=(\}|;))[^\{\};]+\{\s*\}/', '', $content);
+
+        return $content;
     }
 
     /**
@@ -615,6 +627,17 @@ class CSS extends Minify
      */
     protected function stripComments()
     {
+        // PHP only supports $this inside anonymous functions since 5.4
+        $minifier = $this;
+        $callback = function ($match) use ($minifier) {
+            $count = count($minifier->extracted);
+            $placeholder = '/*'.$count.'*/';
+            $minifier->extracted[$placeholder] = $match[0];
+
+            return $placeholder;
+        };
+        $this->registerPattern('/\n?\/\*(!|.*?@license|.*?@preserve).*?\*\/\n?/s', $callback);
+
         $this->registerPattern('/\/\*.*?\*\//s', '');
     }
 
@@ -637,8 +660,8 @@ class CSS extends Minify
         // remove whitespace around meta characters
         // inspired by stackoverflow.com/questions/15195750/minify-compress-css-with-regex
         $content = preg_replace('/\s*([\*$~^|]?+=|[{};,>~]|!important\b)\s*/', '$1', $content);
-        $content = preg_replace('/([\[(:])\s+/', '$1', $content);
-        $content = preg_replace('/\s+([\]\)])/', '$1', $content);
+        $content = preg_replace('/([\[(:>\+])\s+/', '$1', $content);
+        $content = preg_replace('/\s+([\]\)>\+])/', '$1', $content);
         $content = preg_replace('/\s+(:)(?![^\}]*\{)/', '$1', $content);
 
         // whitespace around + and - can only be stripped inside some pseudo-
@@ -654,6 +677,40 @@ class CSS extends Minify
         return trim($content);
     }
 
+    /**
+     * Replace all `calc()` occurrences.
+     */
+    protected function extractCalcs()
+    {
+        // PHP only supports $this inside anonymous functions since 5.4
+        $minifier = $this;
+        $callback = function ($match) use ($minifier) {
+            $length = strlen($match[1]);
+            $expr = '';
+            $opened = 0;
+
+            for ($i = 0; $i < $length; $i++) {
+                $char = $match[1][$i];
+                $expr .= $char;
+                if ($char === '(') {
+                    $opened++;
+                } elseif ($char === ')' && --$opened === 0) {
+                    break;
+                }
+            }
+            $rest = str_replace($expr, '', $match[1]);
+            $expr = trim(substr($expr, 1, -1));
+
+            $count = count($minifier->extracted);
+            $placeholder = 'calc('.$count.')';
+            $minifier->extracted[$placeholder] = 'calc('.$expr.')';
+
+            return $placeholder.$rest;
+        };
+
+        $this->registerPattern('/calc(\(.+?)(?=$|;|calc\()/', $callback);
+    }
+
     /**
      * Check if file is small enough to be imported.
      *
index 0cb3fab..d03898f 100644 (file)
@@ -1,10 +1,18 @@
 <?php
-
+/**
+ * Base Exception
+ *
+ * @deprecated Use Exceptions\BasicException instead
+ *
+ * @author Matthias Mullie <minify@mullie.eu>
+ */
 namespace MatthiasMullie\Minify;
 
 /**
+ * Base Exception Class
  * @deprecated Use Exceptions\BasicException instead
  *
+ * @package Minify
  * @author Matthias Mullie <minify@mullie.eu>
  */
 abstract class Exception extends \Exception
index a6328f4..af5e81b 100644 (file)
@@ -1,10 +1,21 @@
 <?php
-
+/**
+ * Basic exception
+ *
+ * Please report bugs on https://github.com/matthiasmullie/minify/issues
+ *
+ * @author Matthias Mullie <minify@mullie.eu>
+ * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
+ * @license MIT License
+ */
 namespace MatthiasMullie\Minify\Exceptions;
 
 use MatthiasMullie\Minify\Exception;
 
 /**
+ * Basic Exception Class
+ *
+ * @package Minify\Exception
  * @author Matthias Mullie <minify@mullie.eu>
  */
 abstract class BasicException extends Exception
index 98fc341..912a2c9 100644 (file)
@@ -1,8 +1,19 @@
 <?php
-
+/**
+ * File Import Exception
+ *
+ * Please report bugs on https://github.com/matthiasmullie/minify/issues
+ *
+ * @author Matthias Mullie <minify@mullie.eu>
+ * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
+ * @license MIT License
+ */
 namespace MatthiasMullie\Minify\Exceptions;
 
 /**
+ * File Import Exception Class
+ *
+ * @package Minify\Exception
  * @author Matthias Mullie <minify@mullie.eu>
  */
 class FileImportException extends BasicException
index 9b59074..b172eb4 100644 (file)
@@ -1,8 +1,19 @@
 <?php
-
+/**
+ * IO Exception
+ *
+ * Please report bugs on https://github.com/matthiasmullie/minify/issues
+ *
+ * @author Matthias Mullie <minify@mullie.eu>
+ * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
+ * @license MIT License
+ */
 namespace MatthiasMullie\Minify\Exceptions;
 
 /**
+ * IO Exception Class
+ *
+ * @package Minify\Exception
  * @author Matthias Mullie <minify@mullie.eu>
  */
 class IOException extends BasicException
index 042f542..92389cd 100644 (file)
@@ -1,12 +1,21 @@
 <?php
-
+/**
+ * JavaScript minifier
+ *
+ * Please report bugs on https://github.com/matthiasmullie/minify/issues
+ *
+ * @author Matthias Mullie <minify@mullie.eu>
+ * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
+ * @license MIT License
+ */
 namespace MatthiasMullie\Minify;
 
 /**
- * JavaScript minifier.
+ * JavaScript Minifier Class
  *
  * Please report bugs on https://github.com/matthiasmullie/minify/issues
  *
+ * @package Minify
  * @author Matthias Mullie <minify@mullie.eu>
  * @author Tijs Verkoyen <minify@verkoyen.eu>
  * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
@@ -110,11 +119,6 @@ class JS extends Minify
      */
     protected $operatorsAfter = array();
 
-    /**
-     * @var array
-     */
-    protected $nestedExtracted = array();
-
     /**
      * {@inheritdoc}
      */
@@ -144,19 +148,6 @@ class JS extends Minify
     {
         $content = '';
 
-        // loop files
-        foreach ($this->data as $source => $js) {
-            /*
-             * Combine js: separating the scripts by a ;
-             * I'm also adding a newline: it will be eaten when whitespace is
-             * stripped, but we need to make sure we're not just appending
-             * a new script right after a previous script that ended with a
-             * singe-line comment on the last line (in which case it would also
-             * be seen as part of that comment)
-             */
-            $content .= $js."\n;";
-        }
-
         /*
          * Let's first take out strings, comments and regular expressions.
          * All of these can contain JS code-like characters, and we should make
@@ -171,11 +162,24 @@ class JS extends Minify
         $this->extractStrings('\'"`');
         $this->stripComments();
         $this->extractRegex();
-        $content = $this->replace($content);
 
-        $content = $this->propertyNotation($content);
-        $content = $this->shortenBools($content);
-        $content = $this->stripWhitespace($content);
+        // loop files
+        foreach ($this->data as $source => $js) {
+            // take out strings, comments & regex (for which we've registered
+            // the regexes just a few lines earlier)
+            $js = $this->replace($js);
+
+            $js = $this->propertyNotation($js);
+            $js = $this->shortenBools($js);
+            $js = $this->stripWhitespace($js);
+
+            // combine js: separating the scripts by a ;
+            $content .= $js.";";
+        }
+
+        // clean up leftover `;`s from the combination of multiple scripts
+        $content = ltrim($content, ';');
+        $content = (string) substr($content, 0, -1);
 
         /*
          * Earlier, we extracted strings & regular expressions and replaced them
@@ -191,11 +195,21 @@ class JS extends Minify
      */
     protected function stripComments()
     {
-        // single-line comments
-        $this->registerPattern('/\/\/.*$/m', '');
+        // PHP only supports $this inside anonymous functions since 5.4
+        $minifier = $this;
+        $callback = function ($match) use ($minifier) {
+            $count = count($minifier->extracted);
+            $placeholder = '/*'.$count.'*/';
+            $minifier->extracted[$placeholder] = $match[0];
 
+            return $placeholder;
+        };
         // multi-line comments
+        $this->registerPattern('/\n?\/\*(!|.*?@license|.*?@preserve).*?\*\/\n?/s', $callback);
         $this->registerPattern('/\/\*.*?\*\//s', '');
+
+        // single-line comments
+        $this->registerPattern('/\/\/.*$/m', '');
     }
 
     /**
@@ -222,38 +236,26 @@ class JS extends Minify
         $callback = function ($match) use ($minifier) {
             $count = count($minifier->extracted);
             $placeholder = '"'.$count.'"';
-            $minifier->extracted[$placeholder] = $match['regex'];
-
-            // because we're also trying to find regular expressions that follow
-            // if/when/for statements, we should also make sure that the content
-            // within these statements is also minified...
-            // e.g. `if("some   string"/* or comment */)` should become
-            //      `if("some   string")`
-            if (isset($match['before'])) {
-                $other = new static();
-                $other->extractStrings('\'"`', "$count-");
-                $other->stripComments();
-                $match['before'] = $other->replace($match['before']);
-                $this->nestedExtracted += $other->extracted;
-            }
+            $minifier->extracted[$placeholder] = $match[0];
 
-            return (isset($match['before']) ? $match['before'] : '').
-                $placeholder.
-                (isset($match['after']) ? $match['after'] : '');
+            return $placeholder;
         };
 
-        $pattern = '(?P<regex>\/.+?((?<!\\\\)\\\\\\\\)*\/[gimy]*)(?![0-9a-zA-Z\/])';
+        // match all chars except `/` and `\`
+        // `\` is allowed though, along with whatever char follows (which is the
+        // one being escaped)
+        // this should allow all chars, except for an unescaped `/` (= the one
+        // closing the regex)
+        // then also ignore bare `/` inside `[]`, where they don't need to be
+        // escaped: anything inside `[]` can be ignored safely
+        $pattern = '\\/(?!\*)(?:[^\\[\\/\\\\\n\r]++|(?:\\\\.)++|(?:\\[(?:[^\\]\\\\\n\r]++|(?:\\\\.)++)++\\])++)++\\/[gimuy]*';
 
         // a regular expression can only be followed by a few operators or some
         // of the RegExp methods (a `\` followed by a variable or value is
         // likely part of a division, not a regex)
         $keywords = array('do', 'in', 'new', 'else', 'throw', 'yield', 'delete', 'return',  'typeof');
-        $before = '(?P<before>[=:,;\}\(\{&\|!]|^|'.implode('|', $keywords).')';
+        $before = '([=:,;\+\-\*\/\}\(\{\[&\|!]|^|'.implode('|', $keywords).')\s*';
         $propertiesAndMethods = array(
-            // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#Properties
-            'prototype',
-            'length',
-            'lastIndex',
             // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#Properties_2
             'constructor',
             'flags',
@@ -267,29 +269,28 @@ class JS extends Minify
             'compile(',
             'exec(',
             'test(',
-            'match',
-            'replace(',
-            'search(',
-            'split(',
             'toSource(',
             'toString(',
         );
         $delimiters = array_fill(0, count($propertiesAndMethods), '/');
         $propertiesAndMethods = array_map('preg_quote', $propertiesAndMethods, $delimiters);
-        $after = '(?P<after>[\.,;\)\}&\|+]|$|\.('.implode('|', $propertiesAndMethods).'))';
-        $this->registerPattern('/'.$before.'\s*'.$pattern.'\s*'.$after.'/', $callback);
-
-        // we didn't check for regular expressions after `)`, because that is
-        // more often than not not a character where a regex can follow (e.g.
-        // (1+2)/3/4 -> /3/ could be considered a regex, but it's not)
-        // however, after single-line if/while/for, there could very well be a
-        // regex after `)` (e.g. if(true)/regex/)
-        // there is one problem, though: it's (near) impossible to check for
-        // when the if/while/for statement is closed (same amount of closing
-        // brackets as there were opened), so I'll ignore single-line statements
-        // with nested brackets followed by a regex for now...
-        $before = '(?P<before>\b(if|while|for)\s*\((?P<code>[^\(]+?)\))';
-        $this->registerPattern('/'.$before.'\s*'.$pattern.'\s*'.$after.'/', $callback);
+        $after = '(?=\s*([\.,;\)\}&\|+]|\/\/|$|\.('.implode('|', $propertiesAndMethods).')))';
+        $this->registerPattern('/'.$before.'\K'.$pattern.$after.'/', $callback);
+
+        // regular expressions following a `)` are rather annoying to detect...
+        // quite often, `/` after `)` is a division operator & if it happens to
+        // be followed by another one (or a comment), it is likely to be
+        // confused for a regular expression
+        // however, it's perfectly possible for a regex to follow a `)`: after
+        // a single-line `if()`, `while()`, ... statement, for example
+        // since, when they occur like that, they're always the start of a
+        // statement, there's only a limited amount of ways they can be useful:
+        // by calling the regex methods directly
+        // if a regex following `)` is not followed by `.<property or method>`,
+        // it's quite likely not a regex
+        $before = '\)\s*';
+        $after = '(?=\s*\.('.implode('|', $propertiesAndMethods).'))';
+        $this->registerPattern('/'.$before.'\K'.$pattern.$after.'/', $callback);
 
         // 1 more edge case: a regex can be followed by a lot more operators or
         // keywords if there's a newline (ASI) in between, where the operator
@@ -297,25 +298,8 @@ class JS extends Minify
         // (https://github.com/matthiasmullie/minify/issues/56)
         $operators = $this->getOperatorsForRegex($this->operatorsBefore, '/');
         $operators += $this->getOperatorsForRegex($this->keywordsReserved, '/');
-        $after = '(?P<after>\n\s*('.implode('|', $operators).'))';
-        $this->registerPattern('/'.$pattern.'\s*'.$after.'/', $callback);
-    }
-
-    /**
-     * In addition to the regular restore routine, we also need to restore a few
-     * more things that have been extracted as part of the regex extraction...
-     *
-     * {@inheritdoc}
-     */
-    protected function restoreExtractedData($content)
-    {
-        // restore regular extracted stuff
-        $content = parent::restoreExtractedData($content);
-
-        // restore nested stuff from within regex extraction
-        $content = strtr($content, $this->nestedExtracted);
-
-        return $content;
+        $after = '(?=\s*\n\s*('.implode('|', $operators).'))';
+        $this->registerPattern('/'.$pattern.$after.'/', $callback);
     }
 
     /**
@@ -361,7 +345,9 @@ class JS extends Minify
             array(
                 '/('.implode('|', $operatorsBefore).')\s+/',
                 '/\s+('.implode('|', $operatorsAfter).')/',
-            ), '\\1', $content
+            ),
+            '\\1',
+            $content
         );
 
         // make sure + and - can't be mistaken for, or joined into ++ and --
@@ -369,7 +355,9 @@ class JS extends Minify
             array(
                 '/(?<![\+\-])\s*([\+\-])(?![\+\-])/',
                 '/(?<![\+\-])([\+\-])\s*(?![\+\-])/',
-            ), '\\1', $content
+            ),
+            '\\1',
+            $content
         );
 
         // collapse whitespace around reserved words into single space
@@ -387,6 +375,16 @@ class JS extends Minify
         $content = preg_replace('/('.implode('|', $operatorsDiffBefore).')[^\S\n]+/', '\\1', $content);
         $content = preg_replace('/[^\S\n]+('.implode('|', $operatorsDiffAfter).')/', '\\1', $content);
 
+        /*
+         * Whitespace after `return` can be omitted in a few occasions
+         * (such as when followed by a string or regex)
+         * Same for whitespace in between `)` and `{`, or between `{` and some
+         * keywords.
+         */
+        $content = preg_replace('/\breturn\s+(["\'\/\+\-])/', 'return$1', $content);
+        $content = preg_replace('/\)\s+\{/', '){', $content);
+        $content = preg_replace('/}\n(else|catch|finally)\b/', '}$1', $content);
+
         /*
          * Get rid of double semicolons, except where they can be used like:
          * "for(v=1,_=b;;)", "for(v=1;;v++)" or "for(;;ja||(ja=true))".
@@ -404,12 +402,19 @@ class JS extends Minify
          * semicolon), like: `for(i=1;i<3;i++);`, of `for(i in list);`
          * Here, nothing happens during the loop; it's just used to keep
          * increasing `i`. With that ; omitted, the next line would be expected
-         * to be the for-loop's body...
+         * to be the for-loop's body... Same goes for while loops.
          * I'm going to double that semicolon (if any) so after the next line,
          * which strips semicolons here & there, we're still left with this one.
          */
         $content = preg_replace('/(for\([^;\{]*;[^;\{]*;[^;\{]*\));(\}|$)/s', '\\1;;\\2', $content);
         $content = preg_replace('/(for\([^;\{]+\s+in\s+[^;\{]+\));(\}|$)/s', '\\1;;\\2', $content);
+        /*
+         * Below will also keep `;` after a `do{}while();` along with `while();`
+         * While these could be stripped after do-while, detecting this
+         * distinction is cumbersome, so I'll play it safe and make sure `;`
+         * after any kind of `while` is kept.
+         */
+        $content = preg_replace('/(while\([^;\{]+\));(\}|$)/s', '\\1;;\\2', $content);
 
         /*
          * We also can't strip empty else-statements. Even though they're
index 29905b2..e5fefe6 100644 (file)
@@ -1,5 +1,13 @@
 <?php
-
+/**
+ * Abstract minifier class
+ *
+ * Please report bugs on https://github.com/matthiasmullie/minify/issues
+ *
+ * @author Matthias Mullie <minify@mullie.eu>
+ * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
+ * @license MIT License
+ */
 namespace MatthiasMullie\Minify;
 
 use MatthiasMullie\Minify\Exceptions\IOException;
@@ -10,6 +18,7 @@ use Psr\Cache\CacheItemInterface;
  *
  * Please report bugs on https://github.com/matthiasmullie/minify/issues
  *
+ * @package Minify
  * @author Matthias Mullie <minify@mullie.eu>
  * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
  * @license MIT License
@@ -230,6 +239,12 @@ abstract class Minify
             foreach ($this->patterns as $i => $pattern) {
                 list($pattern, $replacement) = $pattern;
 
+                // we can safely ignore patterns for positions we've unset earlier,
+                // because we know these won't show up anymore
+                if (array_key_exists($i, $positions) == false) {
+                    continue;
+                }
+
                 // no need to re-run matches that are still in the part of the
                 // content that hasn't been processed
                 if ($positions[$i] >= 0) {
@@ -237,19 +252,18 @@ abstract class Minify
                 }
 
                 $match = null;
-                if (preg_match($pattern, $content, $match)) {
+                if (preg_match($pattern, $content, $match, PREG_OFFSET_CAPTURE)) {
                     $matches[$i] = $match;
 
                     // we'll store the match position as well; that way, we
                     // don't have to redo all preg_matches after changing only
                     // the first (we'll still know where those others are)
-                    $positions[$i] = strpos($content, $match[0]);
+                    $positions[$i] = $match[0][1];
                 } else {
                     // if the pattern couldn't be matched, there's no point in
                     // executing it again in later runs on this same content;
                     // ignore this one until we reach end of content
-                    unset($matches[$i]);
-                    $positions[$i] = strlen($content);
+                    unset($matches[$i], $positions[$i]);
                 }
             }
 
@@ -264,7 +278,7 @@ abstract class Minify
             // other found was not inside what the first found)
             $discardLength = min($positions);
             $firstPattern = array_search($discardLength, $positions);
-            $match = $matches[$firstPattern][0];
+            $match = $matches[$firstPattern][0][0];
 
             // execute the pattern that matches earliest in the content string
             list($pattern, $replacement) = $this->patterns[$firstPattern];
@@ -272,7 +286,7 @@ abstract class Minify
 
             // figure out which part of the string was unmatched; that's the
             // part we'll execute the patterns on again next
-            $content = substr($content, $discardLength);
+            $content = (string) substr($content, $discardLength);
             $unmatched = (string) substr($content, strpos($content, $match) + strlen($match));
 
             // move the replaced part to $processed and prepare $content to
@@ -396,6 +410,16 @@ abstract class Minify
      */
     protected function canImportFile($path)
     {
+        $parsed = parse_url($path);
+        if (
+            // file is elsewhere
+            isset($parsed['host']) ||
+            // file responds to queries (may change, or need to bypass cache)
+            isset($parsed['query'])
+        ) {
+            return false;
+        }
+
         return strlen($path) < PHP_MAXPATHLEN && @is_file($path) && is_readable($path);
     }
 
diff --git a/lib/minify/matthiasmullie-pathconverter/LICENSE b/lib/minify/matthiasmullie-pathconverter/LICENSE
new file mode 100644 (file)
index 0000000..491295a
--- /dev/null
@@ -0,0 +1,18 @@
+Copyright (c) 2015 Matthias Mullie
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
index c8b0c69..519d3c8 100644 (file)
@@ -31,21 +31,22 @@ class Converter implements ConverterInterface
     /**
      * @param string $from The original base path (directory, not file!)
      * @param string $to   The new base path (directory, not file!)
+     * @param string $root Root directory (defaults to `getcwd`)
      */
-    public function __construct($from, $to)
+    public function __construct($from, $to, $root = '')
     {
         $shared = $this->shared($from, $to);
         if ($shared === '') {
             // when both paths have nothing in common, one of them is probably
             // absolute while the other is relative
-            $cwd = getcwd();
-            $from = strpos($from, $cwd) === 0 ? $from : $cwd.'/'.$from;
-            $to = strpos($to, $cwd) === 0 ? $to : $cwd.'/'.$to;
+            $root = $root ?: getcwd();
+            $from = strpos($from, $root) === 0 ? $from : preg_replace('/\/+/', '/', $root.'/'.$from);
+            $to = strpos($to, $root) === 0 ? $to : preg_replace('/\/+/', '/', $root.'/'.$to);
 
             // or traveling the tree via `..`
             // attempt to resolve path, or assume it's fine if it doesn't exist
-            $from = realpath($from) ?: $from;
-            $to = realpath($to) ?: $to;
+            $from = @realpath($from) ?: $from;
+            $to = @realpath($to) ?: $to;
         }
 
         $from = $this->dirname($from);
@@ -155,7 +156,7 @@ class Converter implements ConverterInterface
         $to = mb_substr($this->to, mb_strlen($shared));
 
         // add .. for every directory that needs to be traversed to new path
-        $to = str_repeat('../', mb_substr_count($to, '/'));
+        $to = str_repeat('../', count(array_filter(explode('/', $to))));
 
         return $to.ltrim($path, '/');
     }
@@ -169,11 +170,11 @@ class Converter implements ConverterInterface
      */
     protected function dirname($path)
     {
-        if (is_file($path)) {
+        if (@is_file($path)) {
             return dirname($path);
         }
 
-        if (is_dir($path)) {
+        if (@is_dir($path)) {
             return rtrim($path, '/');
         }
 
index 063c44d..b9b122c 100644 (file)
@@ -30,6 +30,7 @@ use Phpml\Preprocessing\Normalizer;
 use Phpml\CrossValidation\RandomSplit;
 use Phpml\Dataset\ArrayDataset;
 use Phpml\ModelManager;
+use Phpml\Classification\Linear\LogisticRegression;
 
 /**
  * PHP predictions processor.
@@ -110,7 +111,7 @@ class processor implements \core_analytics\classifier, \core_analytics\regressor
         if (file_exists($modelfilepath)) {
             $classifier = $modelmanager->restoreFromFile($modelfilepath);
         } else {
-            $classifier = new \Phpml\Classification\Linear\LogisticRegression(self::TRAIN_ITERATIONS, Normalizer::NORM_L2);
+            $classifier = $this->instantiate_algorithm();
         }
 
         $fh = $dataset->get_content_file_handle();
@@ -314,7 +315,7 @@ class processor implements \core_analytics\classifier, \core_analytics\regressor
         for ($i = 0; $i < $niterations; $i++) {
 
             if (!$trainedmodeldir) {
-                $classifier = new \Phpml\Classification\Linear\LogisticRegression(self::TRAIN_ITERATIONS, Normalizer::NORM_L2);
+                $classifier = $this->instantiate_algorithm();
 
                 // Split up the dataset in classifier and testing.
                 $data = new RandomSplit(new ArrayDataset($samples, $targets), 0.2);
@@ -561,4 +562,14 @@ class processor implements \core_analytics\classifier, \core_analytics\regressor
         $metadata = fgetcsv($fh);
         return array_combine($metadata, fgetcsv($fh));
     }
+
+    /**
+     * Instantiates the ML algorithm.
+     *
+     * @return \Phpml\Classification\Linear\LogisticRegression
+     */
+    protected function instantiate_algorithm(): \Phpml\Classification\Linear\LogisticRegression {
+        return new LogisticRegression(self::TRAIN_ITERATIONS, true,
+            LogisticRegression::CONJUGATE_GRAD_TRAINING, 'log');
+    }
 }
index bd5cb2f..c90077c 100644 (file)
@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2016 Arkadiusz Kondas <arkadiusz.kondas[at]gmail>
+Copyright (c) 2016-2018 Arkadiusz Kondas <arkadiusz.kondas[at]gmail>
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
index 4ffb5de..5728311 100644 (file)
@@ -1,4 +1,4 @@
-Current version is 12b8b11
+Current version is 0.8.0
 
 # Download latest stable version from https://github.com/php-ai/php-ml
 # Remove all files but:
index 362f25a..201bfbf 100644 (file)
@@ -9,15 +9,16 @@ use Phpml\Helper\Trainable;
 
 class Apriori implements Associator
 {
-    use Trainable, Predictable;
+    use Trainable;
+    use Predictable;
 
-    const ARRAY_KEY_ANTECEDENT = 'antecedent';
+    public const ARRAY_KEY_ANTECEDENT = 'antecedent';
 
-    const ARRAY_KEY_CONFIDENCE = 'confidence';
+    public const ARRAY_KEY_CONFIDENCE = 'confidence';
 
-    const ARRAY_KEY_CONSEQUENT = 'consequent';
+    public const ARRAY_KEY_CONSEQUENT = 'consequent';
 
-    const ARRAY_KEY_SUPPORT = 'support';
+    public const ARRAY_KEY_SUPPORT = 'support';
 
     /**
      * Minimum relative probability of frequent transactions.
@@ -31,7 +32,7 @@ class Apriori implements Associator
      *
      * @var mixed[][][]
      */
-    private $large;
+    private $large = [];
 
     /**
      * Minimum relative frequency of transactions.
@@ -45,13 +46,10 @@ class Apriori implements Associator
      *
      * @var mixed[][]
      */
-    private $rules;
+    private $rules = [];
 
     /**
      * Apriori constructor.
-     *
-     * @param float $support
-     * @param float $confidence
      */
     public function __construct(float $support = 0.0, float $confidence = 0.0)
     {
@@ -64,13 +62,13 @@ class Apriori implements Associator
      *
      * @return mixed[][]
      */
-    public function getRules() : array
+    public function getRules(): array
     {
-        if (!$this->large) {
+        if (count($this->large) === 0) {
             $this->large = $this->apriori();
         }
 
-        if ($this->rules) {
+        if (count($this->rules) > 0) {
             return $this->rules;
         }
 
@@ -86,15 +84,14 @@ class Apriori implements Associator
      *
      * @return mixed[][][]
      */
-    public function apriori() : array
+    public function apriori(): array
     {
         $L = [];
-        $L[1] = $this->items();
-        $L[1] = $this->frequent($L[1]);
 
-        for ($k = 2; !empty($L[$k - 1]); ++$k) {
-            $L[$k] = $this->candidates($L[$k - 1]);
-            $L[$k] = $this->frequent($L[$k]);
+        $items = $this->frequent($this->items());
+        for ($k = 1; isset($items[0]); ++$k) {
+            $L[$k] = $items;
+            $items = $this->frequent($this->candidates($items));
         }
 
         return $L;
@@ -105,7 +102,7 @@ class Apriori implements Associator
      *
      * @return mixed[][]
      */
-    protected function predictSample(array $sample) : array
+    protected function predictSample(array $sample): array
     {
         $predicts = array_values(array_filter($this->getRules(), function ($rule) use ($sample) {
             return $this->equals($rule[self::ARRAY_KEY_ANTECEDENT], $sample);
@@ -119,9 +116,9 @@ class Apriori implements Associator
     /**
      * Generate rules for each k-length frequent item set.
      */
-    private function generateAllRules()
+    private function generateAllRules(): void
     {
-        for ($k = 2; !empty($this->large[$k]); ++$k) {
+        for ($k = 2; isset($this->large[$k]); ++$k) {
             foreach ($this->large[$k] as $frequent) {
                 $this->generateRules($frequent);
             }
@@ -133,15 +130,16 @@ class Apriori implements Associator
      *
      * @param mixed[] $frequent
      */
-    private function generateRules(array $frequent)
+    private function generateRules(array $frequent): void
     {
         foreach ($this->antecedents($frequent) as $antecedent) {
-            if ($this->confidence <= ($confidence = $this->confidence($frequent, $antecedent))) {
+            $confidence = $this->confidence($frequent, $antecedent);
+            if ($this->confidence <= $confidence) {
                 $consequent = array_values(array_diff($frequent, $antecedent));
                 $this->rules[] = [
                     self::ARRAY_KEY_ANTECEDENT => $antecedent,
                     self::ARRAY_KEY_CONSEQUENT => $consequent,
-                    self::ARRAY_KEY_SUPPORT => $this->support($consequent),
+                    self::ARRAY_KEY_SUPPORT => $this->support($frequent),
                     self::ARRAY_KEY_CONFIDENCE => $confidence,
                 ];
             }
@@ -155,7 +153,7 @@ class Apriori implements Associator
      *
      * @return mixed[][]
      */
-    private function powerSet(array $sample) : array
+    private function powerSet(array $sample): array
     {
         $results = [[]];
         foreach ($sample as $item) {
@@ -174,7 +172,7 @@ class Apriori implements Associator
      *
      * @return mixed[][]
      */
-    private function antecedents(array $sample) : array
+    private function antecedents(array $sample): array
     {
         $cardinality = count($sample);
         $antecedents = $this->powerSet($sample);
@@ -189,7 +187,7 @@ class Apriori implements Associator
      *
      * @return mixed[][]
      */
-    private function items() : array
+    private function items(): array
     {
         $items = [];
 
@@ -213,11 +211,11 @@ class Apriori implements Associator
      *
      * @return mixed[][]
      */
-    private function frequent(array $samples) : array
+    private function frequent(array $samples): array
     {
-        return array_filter($samples, function ($entry) {
+        return array_values(array_filter($samples, function ($entry) {
             return $this->support($entry) >= $this->support;
-        });
+        }));
     }
 
     /**
@@ -227,7 +225,7 @@ class Apriori implements Associator
      *
      * @return mixed[][]
      */
-    private function candidates(array $samples) : array
+    private function candidates(array $samples): array
     {
         $candidates = [];
 
@@ -237,15 +235,16 @@ class Apriori implements Associator
                     continue;
                 }
 
-                $candidate = array_unique(array_merge($p, $q));
+                $candidate = array_values(array_unique(array_merge($p, $q)));
 
                 if ($this->contains($candidates, $candidate)) {
                     continue;
                 }
 
-                foreach ((array) $this->samples as $sample) {
+                foreach ($this->samples as $sample) {
                     if ($this->subset($sample, $candidate)) {
                         $candidates[] = $candidate;
+
                         continue 2;
                     }
                 }
@@ -261,10 +260,8 @@ class Apriori implements Associator
      *
      * @param mixed[] $set
      * @param mixed[] $subset
-     *
-     * @return float
      */
-    private function confidence(array $set, array $subset) : float
+    private function confidence(array $set, array $subset): float
     {
         return $this->support($set) / $this->support($subset);
     }
@@ -276,10 +273,8 @@ class Apriori implements Associator
      * @see \Phpml\Association\Apriori::samples
      *
      * @param mixed[] $sample
-     *
-     * @return float
      */
-    private function support(array $sample) : float
+    private function support(array $sample): float
     {
         return $this->frequency($sample) / count($this->samples);
     }
@@ -290,10 +285,8 @@ class Apriori implements Associator
      * @see \Phpml\Association\Apriori::samples
      *
      * @param mixed[] $sample
-     *
-     * @return int
      */
-    private function frequency(array $sample) : int
+    private function frequency(array $sample): int
     {
         return count(array_filter($this->samples, function ($entry) use ($sample) {
             return $this->subset($entry, $sample);
@@ -307,10 +300,8 @@ class Apriori implements Associator
      *
      * @param mixed[][] $system
      * @param mixed[]   $set
-     *
-     * @return bool
      */
-    private function contains(array $system, array $set) : bool
+    private function contains(array $system, array $set): bool
     {
         return (bool) array_filter($system, function ($entry) use ($set) {
             return $this->equals($entry, $set);
@@ -322,12 +313,10 @@ class Apriori implements Associator
      *
      * @param mixed[] $set
      * @param mixed[] $subset
-     *
-     * @return bool
      */
-    private function subset(array $set, array $subset) : bool
+    private function subset(array $set, array $subset): bool
     {
-        return !array_diff($subset, array_intersect($subset, $set));
+        return count(array_diff($subset, array_intersect($subset, $set))) === 0;
     }
 
     /**
@@ -335,10 +324,8 @@ class Apriori implements Associator
      *
      * @param mixed[] $set1
      * @param mixed[] $set2
-     *
-     * @return bool
      */
-    private function equals(array $set1, array $set2) : bool
+    private function equals(array $set1, array $set2): bool
     {
         return array_diff($set1, $set2) == array_diff($set2, $set1);
     }
index da8b81b..04cde56 100644 (file)
@@ -4,48 +4,50 @@ declare(strict_types=1);
 
 namespace Phpml\Classification;
 
+use Phpml\Classification\DecisionTree\DecisionTreeLeaf;
 use Phpml\Exception\InvalidArgumentException;
 use Phpml\Helper\Predictable;
 use Phpml\Helper\Trainable;
 use Phpml\Math\Statistic\Mean;
-use Phpml\Classification\DecisionTree\DecisionTreeLeaf;
 
 class DecisionTree implements Classifier
 {
-    use Trainable, Predictable;
+    use Trainable;
+    use Predictable;
 
-    const CONTINUOUS = 1;
-    const NOMINAL = 2;
+    public const CONTINUOUS = 1;
 
-    /**
-     * @var array
-     */
-    protected $columnTypes;
+    public const NOMINAL = 2;
 
     /**
-     * @var array
+     * @var int
      */
-    private $labels = [];
+    public $actualDepth = 0;
 
     /**
-     * @var int
+     * @var array
      */
-    private $featureCount = 0;
+    protected $columnTypes = [];
 
     /**
      * @var DecisionTreeLeaf
      */
-    protected $tree = null;
+    protected $tree;
 
     /**
      * @var int
      */
     protected $maxDepth;
 
+    /**
+     * @var array
+     */
+    private $labels = [];
+
     /**
      * @var int
      */
-    public $actualDepth = 0;
+    private $featureCount = 0;
 
     /**
      * @var int
@@ -55,32 +57,24 @@ class DecisionTree implements Classifier
     /**
      * @var array
      */
-    private $selectedFeatures;
+    private $selectedFeatures = [];
 
     /**
-     * @var array
+     * @var array|null
      */
-    private $featureImportances = null;
+    private $featureImportances;
 
     /**
-     *
      * @var array
      */
-    private $columnNames = null;
+    private $columnNames = [];
 
-    /**
-     * @param int $maxDepth
-     */
     public function __construct(int $maxDepth = 10)
     {
         $this->maxDepth = $maxDepth;
     }
 
-    /**
-     * @param array $samples
-     * @param array $targets
-     */
-    public function train(array $samples, array $targets)
+    public function train(array $samples, array $targets): void
     {
         $this->samples = array_merge($this->samples, $samples);
         $this->targets = array_merge($this->targets, $targets);
@@ -96,23 +90,19 @@ class DecisionTree implements Classifier
 
         // If column names are given or computed before, then there is no
         // need to init it and accidentally remove the previous given names
-        if ($this->columnNames === null) {
+        if ($this->columnNames === []) {
             $this->columnNames = range(0, $this->featureCount - 1);
         } elseif (count($this->columnNames) > $this->featureCount) {
             $this->columnNames = array_slice($this->columnNames, 0, $this->featureCount);
         } elseif (count($this->columnNames) < $this->featureCount) {
-            $this->columnNames = array_merge($this->columnNames,
+            $this->columnNames = array_merge(
+                $this->columnNames,
                 range(count($this->columnNames), $this->featureCount - 1)
             );
         }
     }
 
-    /**
-     * @param array $samples
-     *
-     * @return array
-     */
-    public static function getColumnTypes(array $samples) : array
+    public static function getColumnTypes(array $samples): array
     {
         $types = [];
         $featureCount = count($samples[0]);
@@ -126,12 +116,120 @@ class DecisionTree implements Classifier
     }
 
     /**
-     * @param array $records
-     * @param int   $depth
+     * @param mixed $baseValue
+     */
+    public function getGiniIndex($baseValue, array $colValues, array $targets): float
+    {
+        $countMatrix = [];
+        foreach ($this->labels as $label) {
+            $countMatrix[$label] = [0, 0];
+        }
+
+        foreach ($colValues as $index => $value) {
+            $label = $targets[$index];
+            $rowIndex = $value === $baseValue ? 0 : 1;
+            ++$countMatrix[$label][$rowIndex];
+        }
+
+        $giniParts = [0, 0];
+        for ($i = 0; $i <= 1; ++$i) {
+            $part = 0;
+            $sum = array_sum(array_column($countMatrix, $i));
+            if ($sum > 0) {
+                foreach ($this->labels as $label) {
+                    $part += ($countMatrix[$label][$i] / (float) $sum) ** 2;
+                }
+            }
+
+            $giniParts[$i] = (1 - $part) * $sum;
+        }
+
+        return array_sum($giniParts) / count($colValues);
+    }
+
+    /**
+     * This method is used to set number of columns to be used
+     * when deciding a split at an internal node of the tree.  <br>
+     * If the value is given 0, then all features are used (default behaviour),
+     * otherwise the given value will be used as a maximum for number of columns
+     * randomly selected for each split operation.
+     *
+     * @return $this
+     *
+     * @throws InvalidArgumentException
+     */
+    public function setNumFeatures(int $numFeatures)
+    {
+        if ($numFeatures < 0) {
+            throw new InvalidArgumentException('Selected column count should be greater or equal to zero');
+        }
+
+        $this->numUsableFeatures = $numFeatures;
+
+        return $this;
+    }
+
+    /**
+     * A string array to represent columns. Useful when HTML output or
+     * column importances are desired to be inspected.
+     *
+     * @return $this
      *
-     * @return DecisionTreeLeaf
+     * @throws InvalidArgumentException
+     */
+    public function setColumnNames(array $names)
+    {
+        if ($this->featureCount !== 0 && count($names) !== $this->featureCount) {
+            throw new InvalidArgumentException(sprintf('Length of the given array should be equal to feature count %s', $this->featureCount));
+        }
+
+        $this->columnNames = $names;
+
+        return $this;
+    }
+
+    public function getHtml(): string
+    {
+        return $this->tree->getHTML($this->columnNames);
+    }
+
+    /**
+     * This will return an array including an importance value for
+     * each column in the given dataset. The importance values are
+     * normalized and their total makes 1.<br/>
      */
-    protected function getSplitLeaf(array $records, int $depth = 0) : DecisionTreeLeaf
+    public function getFeatureImportances(): array
+    {
+        if ($this->featureImportances !== null) {
+            return $this->featureImportances;
+        }
+
+        $sampleCount = count($this->samples);
+        $this->featureImportances = [];
+        foreach ($this->columnNames as $column => $columnName) {
+            $nodes = $this->getSplitNodesByColumn($column, $this->tree);
+
+            $importance = 0;
+            foreach ($nodes as $node) {
+                $importance += $node->getNodeImpurityDecrease($sampleCount);
+            }
+
+            $this->featureImportances[$columnName] = $importance;
+        }
+
+        // Normalize & sort the importances
+        $total = array_sum($this->featureImportances);
+        if ($total > 0) {
+            array_walk($this->featureImportances, function (&$importance) use ($total): void {
+                $importance /= $total;
+            });
+            arsort($this->featureImportances);
+        }
+
+        return $this->featureImportances;
+    }
+
+    protected function getSplitLeaf(array $records, int $depth = 0): DecisionTreeLeaf
     {
         $split = $this->getBestSplit($records);
         $split->level = $depth;
@@ -143,7 +241,7 @@ class DecisionTree implements Classifier
         // otherwise group the records so that we can classify the leaf
         // in case maximum depth is reached
         $leftRecords = [];
-        $rightRecords= [];
+        $rightRecords = [];
         $remainingTargets = [];
         $prevRecord = null;
         $allSame = true;
@@ -151,9 +249,10 @@ class DecisionTree implements Classifier
         foreach ($records as $recordNo) {
             // Check if the previous record is the same with the current one
             $record = $this->samples[$recordNo];
-            if ($prevRecord && $prevRecord != $record) {
+            if ($prevRecord !== null && $prevRecord != $record) {
                 $allSame = false;
             }
+
             $prevRecord = $record;
 
             // According to the split criteron, this record will
@@ -161,7 +260,7 @@ class DecisionTree implements Classifier
             if ($split->evaluate($record)) {
                 $leftRecords[] = $recordNo;
             } else {
-                $rightRecords[]= $recordNo;
+                $rightRecords[] = $recordNo;
             }
 
             // Group remaining targets
@@ -174,31 +273,29 @@ class DecisionTree implements Classifier
         }
 
         if ($allSame || $depth >= $this->maxDepth || count($remainingTargets) === 1) {
-            $split->isTerminal = 1;
+            $split->isTerminal = true;
             arsort($remainingTargets);
-            $split->classValue = key($remainingTargets);
+            $split->classValue = (string) key($remainingTargets);
         } else {
-            if ($leftRecords) {
+            if (isset($leftRecords[0])) {
                 $split->leftLeaf = $this->getSplitLeaf($leftRecords, $depth + 1);
             }
-            if ($rightRecords) {
-                $split->rightLeaf= $this->getSplitLeaf($rightRecords, $depth + 1);
+
+            if (isset($rightRecords[0])) {
+                $split->rightLeaf = $this->getSplitLeaf($rightRecords, $depth + 1);
             }
         }
 
         return $split;
     }
 
-    /**
-     * @param array $records
-     *
-     * @return DecisionTreeLeaf
-     */
-    protected function getBestSplit(array $records) : DecisionTreeLeaf
+    protected function getBestSplit(array $records): DecisionTreeLeaf
     {
         $targets = array_intersect_key($this->targets, array_flip($records));
-        $samples = array_intersect_key($this->samples, array_flip($records));
-        $samples = array_combine($records, $this->preprocess($samples));
+        $samples = (array) array_combine(
+            $records,
+            $this->preprocess(array_intersect_key($this->samples, array_flip($records)))
+        );
         $bestGiniVal = 1;
         $bestSplit = null;
         $features = $this->getSelectedFeatures();
@@ -207,26 +304,31 @@ class DecisionTree implements Classifier
             foreach ($samples as $index => $row) {
                 $colValues[$index] = $row[$i];
             }
+
             $counts = array_count_values($colValues);
             arsort($counts);
             $baseValue = key($counts);
+            if ($baseValue === null) {
+                continue;
+            }
+
             $gini = $this->getGiniIndex($baseValue, $colValues, $targets);
             if ($bestSplit === null || $bestGiniVal > $gini) {
                 $split = new DecisionTreeLeaf();
                 $split->value = $baseValue;
                 $split->giniIndex = $gini;
                 $split->columnIndex = $i;
-                $split->isContinuous = $this->columnTypes[$i] == self::CONTINUOUS;
+                $split->isContinuous = $this->columnTypes[$i] === self::CONTINUOUS;
                 $split->records = $records;
 
                 // If a numeric column is to be selected, then
                 // the original numeric value and the selected operator
                 // will also be saved into the leaf for future access
-                if ($this->columnTypes[$i] == self::CONTINUOUS) {
+                if ($this->columnTypes[$i] === self::CONTINUOUS) {
                     $matches = [];
-                    preg_match("/^([<>=]{1,2})\s*(.*)/", strval($split->value), $matches);
+                    preg_match("/^([<>=]{1,2})\s*(.*)/", (string) $split->value, $matches);
                     $split->operator = $matches[1];
-                    $split->numericValue = floatval($matches[2]);
+                    $split->numericValue = (float) $matches[2];
                 }
 
                 $bestSplit = $split;
@@ -249,17 +351,15 @@ cla