diff --git a/composer.json b/composer.json index b2525a3db3..d8f1326275 100644 --- a/composer.json +++ b/composer.json @@ -60,7 +60,7 @@ "xibosignage/xibo-xmr": "0.3", "tedivm/stash": "^0.14.1", "akrabat/rka-slim-controller": "^2.0", - "phenx/php-font-lib": "^0.4.0", + "phenx/php-font-lib": "^0.5.0", "abraham/twitteroauth": "^0.6.4", "symfony/event-dispatcher": "^3.1", "illuminate/database": "5.2.*", diff --git a/composer.lock b/composer.lock index 0836a37ba4..5acad987fb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "fc4c5cf71a441d59ce60f8ae295e7149", + "hash": "1eb2f59696226ea2189ea106f68de44c", + "content-hash": "9590ac7ce261f2ab4bc8c532bffd505d", "packages": [ { "name": "abraham/twitteroauth", @@ -58,7 +59,7 @@ "social", "twitter" ], - "time": "2016-10-10T14:20:30+00:00" + "time": "2016-10-10 14:20:30" }, { "name": "akrabat/rka-slim-controller", @@ -101,7 +102,7 @@ "controller", "slim" ], - "time": "2015-01-07T09:54:54+00:00" + "time": "2015-01-07 09:54:54" }, { "name": "doctrine/inflector", @@ -168,7 +169,7 @@ "singularize", "string" ], - "time": "2015-11-06T14:35:42+00:00" + "time": "2015-11-06 14:35:42" }, { "name": "emojione/emojione", @@ -212,7 +213,7 @@ "smilies", "unicode" ], - "time": "2015-10-30T04:41:37+00:00" + "time": "2015-10-30 04:41:37" }, { "name": "erusev/parsedown", @@ -254,7 +255,7 @@ "markdown", "parser" ], - "time": "2016-11-02T15:56:58+00:00" + "time": "2016-11-02 15:56:58" }, { "name": "evenement/evenement", @@ -300,7 +301,7 @@ "event-dispatcher", "event-emitter" ], - "time": "2012-11-02T14:49:47+00:00" + "time": "2012-11-02 14:49:47" }, { "name": "ezyang/htmlpurifier", @@ -400,7 +401,7 @@ ], "description": "Modern library to handle RSS/Atom feeds", "homepage": "https://github.com/fguillot/picoFeed", - "time": "2016-12-12T01:50:11+00:00" + "time": "2016-12-12 01:50:11" }, { "name": "flynsarmy/slim-monolog", @@ -446,7 +447,7 @@ "logging", "middleware" ], - "time": "2015-07-15T22:14:44+00:00" + "time": "2015-07-15 22:14:44" }, { "name": "gettext/gettext", @@ -506,7 +507,7 @@ "po", "translation" ], - "time": "2016-08-01T18:09:57+00:00" + "time": "2016-08-01 18:09:57" }, { "name": "gettext/languages", @@ -560,7 +561,7 @@ "translations", "unicode" ], - "time": "2016-12-06T08:00:42+00:00" + "time": "2016-12-06 08:00:42" }, { "name": "guzzlehttp/guzzle", @@ -622,7 +623,7 @@ "rest", "web service" ], - "time": "2016-10-08T15:01:37+00:00" + "time": "2016-10-08 15:01:37" }, { "name": "guzzlehttp/promises", @@ -673,7 +674,7 @@ "keywords": [ "promise" ], - "time": "2016-11-18T17:47:58+00:00" + "time": "2016-11-18 17:47:58" }, { "name": "guzzlehttp/psr7", @@ -731,7 +732,7 @@ "stream", "uri" ], - "time": "2016-06-24T23:00:38+00:00" + "time": "2016-06-24 23:00:38" }, { "name": "illuminate/cache", @@ -781,7 +782,7 @@ ], "description": "The Illuminate Cache package.", "homepage": "http://laravel.com", - "time": "2016-08-18T14:17:46+00:00" + "time": "2016-08-18 14:17:46" }, { "name": "illuminate/container", @@ -824,7 +825,7 @@ ], "description": "The Illuminate Container package.", "homepage": "http://laravel.com", - "time": "2016-08-01T13:49:14+00:00" + "time": "2016-08-01 13:49:14" }, { "name": "illuminate/contracts", @@ -866,7 +867,7 @@ ], "description": "The Illuminate Contracts package.", "homepage": "http://laravel.com", - "time": "2016-08-08T11:46:08+00:00" + "time": "2016-08-08 11:46:08" }, { "name": "illuminate/database", @@ -926,7 +927,7 @@ "orm", "sql" ], - "time": "2016-08-25T07:01:20+00:00" + "time": "2016-08-25 07:01:20" }, { "name": "illuminate/filesystem", @@ -976,7 +977,7 @@ ], "description": "The Illuminate Filesystem package.", "homepage": "http://laravel.com", - "time": "2016-08-17T17:21:29+00:00" + "time": "2016-08-17 17:21:29" }, { "name": "illuminate/support", @@ -1035,7 +1036,7 @@ ], "description": "The Illuminate Support package.", "homepage": "http://laravel.com", - "time": "2016-08-05T14:49:58+00:00" + "time": "2016-08-05 14:49:58" }, { "name": "intervention/image", @@ -1097,7 +1098,7 @@ "thumbnail", "watermark" ], - "time": "2016-09-01T17:04:03+00:00" + "time": "2016-09-01 17:04:03" }, { "name": "intervention/imagecache", @@ -1150,7 +1151,7 @@ "imagick", "laravel" ], - "time": "2015-09-22T15:22:47+00:00" + "time": "2015-09-22 15:22:47" }, { "name": "ircmaxell/random-lib", @@ -1205,7 +1206,7 @@ "random-numbers", "random-strings" ], - "time": "2016-09-07T15:52:06+00:00" + "time": "2016-09-07 15:52:06" }, { "name": "ircmaxell/security-lib", @@ -1251,7 +1252,7 @@ ], "description": "A Base Security Library", "homepage": "https://github.com/ircmaxell/SecurityLib", - "time": "2015-03-20T14:31:23+00:00" + "time": "2015-03-20 14:31:23" }, { "name": "james-heinrich/getid3", @@ -1287,7 +1288,7 @@ "php", "tags" ], - "time": "2016-12-14T14:29:29+00:00" + "time": "2016-12-14 14:29:29" }, { "name": "jenssegers/date", @@ -1344,7 +1345,7 @@ "time", "translation" ], - "time": "2016-10-21T20:32:13+00:00" + "time": "2016-10-21 20:32:13" }, { "name": "jeremeamia/SuperClosure", @@ -1402,7 +1403,7 @@ "serialize", "tokenizer" ], - "time": "2016-12-07T09:37:55+00:00" + "time": "2016-12-07 09:37:55" }, { "name": "league/event", @@ -1452,7 +1453,7 @@ "event", "listener" ], - "time": "2015-05-21T12:24:47+00:00" + "time": "2015-05-21 12:24:47" }, { "name": "league/oauth2-client", @@ -1515,7 +1516,7 @@ "oauth2", "single sign on" ], - "time": "2016-07-28T13:20:43+00:00" + "time": "2016-07-28 13:20:43" }, { "name": "league/oauth2-server", @@ -1579,7 +1580,7 @@ "secure", "server" ], - "time": "2016-09-13T13:42:53+00:00" + "time": "2016-09-13 13:42:53" }, { "name": "monolog/monolog", @@ -1657,7 +1658,7 @@ "logging", "psr-3" ], - "time": "2016-11-26T00:15:39+00:00" + "time": "2016-11-26 00:15:39" }, { "name": "mtdowling/cron-expression", @@ -1701,7 +1702,7 @@ "cron", "schedule" ], - "time": "2016-01-26T21:23:30+00:00" + "time": "2016-01-26 21:23:30" }, { "name": "nesbot/carbon", @@ -1748,7 +1749,7 @@ "datetime", "time" ], - "time": "2015-11-04T20:07:17+00:00" + "time": "2015-11-04 20:07:17" }, { "name": "nikic/php-parser", @@ -1799,7 +1800,7 @@ "parser", "php" ], - "time": "2016-12-06T11:30:35+00:00" + "time": "2016-12-06 11:30:35" }, { "name": "onelogin/php-saml", @@ -1853,7 +1854,7 @@ "onelogin", "saml" ], - "time": "2016-11-15T15:34:53+00:00" + "time": "2016-11-15 15:34:53" }, { "name": "paragonie/random_compat", @@ -1901,26 +1902,29 @@ "pseudorandom", "random" ], - "time": "2016-03-18T20:34:03+00:00" + "time": "2016-03-18 20:34:03" }, { "name": "phenx/php-font-lib", - "version": "0.4", + "version": "0.5.1", "source": { "type": "git", "url": "https://github.com/PhenX/php-font-lib.git", - "reference": "b8af0cacdc3cbf1e41a586fcb78f506f4121a088" + "reference": "760148820110a1ae0936e5cc35851e25a938bc97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PhenX/php-font-lib/zipball/b8af0cacdc3cbf1e41a586fcb78f506f4121a088", - "reference": "b8af0cacdc3cbf1e41a586fcb78f506f4121a088", + "url": "https://api.github.com/repos/PhenX/php-font-lib/zipball/760148820110a1ae0936e5cc35851e25a938bc97", + "reference": "760148820110a1ae0936e5cc35851e25a938bc97", "shasum": "" }, + "require-dev": { + "phpunit/phpunit": "^4.8" + }, "type": "library", "autoload": { - "psr-0": { - "FontLib\\": "src/" + "psr-4": { + "FontLib\\": "src/FontLib" } }, "notification-url": "https://packagist.org/downloads/", @@ -1935,7 +1939,7 @@ ], "description": "A library to read, parse, export and make subsets of different types of font files.", "homepage": "https://github.com/PhenX/php-font-lib", - "time": "2015-05-06T20:02:39+00:00" + "time": "2017-09-13 16:14:37" }, { "name": "phpmailer/phpmailer", @@ -2041,7 +2045,7 @@ "psr", "psr-6" ], - "time": "2016-08-06T20:24:11+00:00" + "time": "2016-08-06 20:24:11" }, { "name": "psr/http-message", @@ -2091,7 +2095,7 @@ "request", "response" ], - "time": "2016-08-06T14:39:51+00:00" + "time": "2016-08-06 14:39:51" }, { "name": "psr/log", @@ -2138,7 +2142,7 @@ "psr", "psr-3" ], - "time": "2016-10-10T12:19:37+00:00" + "time": "2016-10-10 12:19:37" }, { "name": "react/event-loop", @@ -2182,7 +2186,7 @@ "asynchronous", "event-loop" ], - "time": "2016-03-08T02:09:32+00:00" + "time": "2016-03-08 02:09:32" }, { "name": "react/zmq", @@ -2227,7 +2231,7 @@ "zeromq", "zmq" ], - "time": "2014-05-25T17:54:51+00:00" + "time": "2014-05-25 17:54:51" }, { "name": "respect/validation", @@ -2289,7 +2293,7 @@ "validation", "validator" ], - "time": "2016-03-31T17:26:10+00:00" + "time": "2016-03-31 17:26:10" }, { "name": "sallar/jdatetime", @@ -2389,7 +2393,7 @@ "rest", "router" ], - "time": "2015-03-08T18:41:17+00:00" + "time": "2015-03-08 18:41:17" }, { "name": "slim/views", @@ -2442,7 +2446,7 @@ "slimphp", "templating" ], - "time": "2014-12-09T23:48:51+00:00" + "time": "2014-12-09 23:48:51" }, { "name": "symfony/event-dispatcher", @@ -2502,7 +2506,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2016-10-13T06:29:04+00:00" + "time": "2016-10-13 06:29:04" }, { "name": "symfony/finder", @@ -2551,7 +2555,7 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2016-06-29T05:40:00+00:00" + "time": "2016-06-29 05:40:00" }, { "name": "symfony/http-foundation", @@ -2604,7 +2608,7 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2016-11-27T04:21:38+00:00" + "time": "2016-11-27 04:21:38" }, { "name": "symfony/polyfill-mbstring", @@ -2663,7 +2667,7 @@ "portable", "shim" ], - "time": "2016-11-14T01:06:16+00:00" + "time": "2016-11-14 01:06:16" }, { "name": "symfony/polyfill-php56", @@ -2719,7 +2723,7 @@ "portable", "shim" ], - "time": "2016-11-14T01:06:16+00:00" + "time": "2016-11-14 01:06:16" }, { "name": "symfony/polyfill-util", @@ -2771,7 +2775,7 @@ "polyfill", "shim" ], - "time": "2016-11-14T01:06:16+00:00" + "time": "2016-11-14 01:06:16" }, { "name": "symfony/translation", @@ -2835,7 +2839,7 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2016-11-30T14:40:17+00:00" + "time": "2016-11-30 14:40:17" }, { "name": "tedivm/stash", @@ -2895,7 +2899,7 @@ "redis", "sessions" ], - "time": "2016-02-10T22:23:16+00:00" + "time": "2016-02-10 22:23:16" }, { "name": "twig/twig", @@ -2956,7 +2960,7 @@ "keywords": [ "templating" ], - "time": "2016-12-13T17:28:18+00:00" + "time": "2016-12-13 17:28:18" }, { "name": "xibosignage/oauth2-xibo-cms", @@ -2964,12 +2968,12 @@ "source": { "type": "git", "url": "https://github.com/PeterMis/oauth2-xibo-cms.git", - "reference": "6a8d2ca2f4dbc191e7b377c9c9a691ff887ae6f6" + "reference": "61439c0511d8d1b97ff8685c2c115d7aa1586572" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PeterMis/oauth2-xibo-cms/zipball/6a8d2ca2f4dbc191e7b377c9c9a691ff887ae6f6", - "reference": "6a8d2ca2f4dbc191e7b377c9c9a691ff887ae6f6", + "url": "https://api.github.com/repos/PeterMis/oauth2-xibo-cms/zipball/61439c0511d8d1b97ff8685c2c115d7aa1586572", + "reference": "61439c0511d8d1b97ff8685c2c115d7aa1586572", "shasum": "" }, "require": { @@ -3009,7 +3013,7 @@ "support": { "source": "https://github.com/PeterMis/oauth2-xibo-cms/tree/master" }, - "time": "2017-05-03 15:07:16" + "time": "2017-07-27 14:38:35" }, { "name": "xibosignage/xibo-xmr", @@ -3059,7 +3063,7 @@ "spring signage", "xibo" ], - "time": "2017-01-13T15:11:16+00:00" + "time": "2017-01-13 15:11:16" }, { "name": "zendframework/zendxml", @@ -3104,7 +3108,7 @@ "xml", "zf2" ], - "time": "2016-02-04T21:02:08+00:00" + "time": "2016-02-04 21:02:08" } ], "packages-dev": [ @@ -3174,7 +3178,7 @@ "docblock", "parser" ], - "time": "2016-10-24T11:45:47+00:00" + "time": "2016-10-24 11:45:47" }, { "name": "doctrine/instantiator", @@ -3228,7 +3232,7 @@ "constructor", "instantiate" ], - "time": "2015-06-14T21:17:01+00:00" + "time": "2015-06-14 21:17:01" }, { "name": "doctrine/lexer", @@ -3282,7 +3286,7 @@ "lexer", "parser" ], - "time": "2014-09-09T13:34:57+00:00" + "time": "2014-09-09 13:34:57" }, { "name": "phpdocumentor/reflection-common", @@ -3336,7 +3340,7 @@ "reflection", "static analysis" ], - "time": "2015-12-27T11:43:31+00:00" + "time": "2015-12-27 11:43:31" }, { "name": "phpdocumentor/reflection-docblock", @@ -3381,7 +3385,7 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2016-09-30T07:12:33+00:00" + "time": "2016-09-30 07:12:33" }, { "name": "phpdocumentor/type-resolver", @@ -3428,7 +3432,7 @@ "email": "me@mikevanriel.com" } ], - "time": "2016-11-25T06:54:22+00:00" + "time": "2016-11-25 06:54:22" }, { "name": "phpspec/prophecy", @@ -3491,7 +3495,7 @@ "spy", "stub" ], - "time": "2016-11-21T14:58:47+00:00" + "time": "2016-11-21 14:58:47" }, { "name": "phpunit/dbunit", @@ -3546,7 +3550,7 @@ "testing", "xunit" ], - "time": "2016-12-02T14:39:14+00:00" + "time": "2016-12-02 14:39:14" }, { "name": "phpunit/php-code-coverage", @@ -3608,7 +3612,7 @@ "testing", "xunit" ], - "time": "2015-10-06T15:47:00+00:00" + "time": "2015-10-06 15:47:00" }, { "name": "phpunit/php-file-iterator", @@ -3655,7 +3659,7 @@ "filesystem", "iterator" ], - "time": "2016-10-03T07:40:28+00:00" + "time": "2016-10-03 07:40:28" }, { "name": "phpunit/php-text-template", @@ -3696,7 +3700,7 @@ "keywords": [ "template" ], - "time": "2015-06-21T13:50:34+00:00" + "time": "2015-06-21 13:50:34" }, { "name": "phpunit/php-timer", @@ -3740,7 +3744,7 @@ "keywords": [ "timer" ], - "time": "2016-05-12T18:03:57+00:00" + "time": "2016-05-12 18:03:57" }, { "name": "phpunit/php-token-stream", @@ -3789,7 +3793,7 @@ "keywords": [ "tokenizer" ], - "time": "2016-11-15T14:06:22+00:00" + "time": "2016-11-15 14:06:22" }, { "name": "phpunit/phpunit", @@ -3861,7 +3865,7 @@ "testing", "xunit" ], - "time": "2016-12-09T02:45:31+00:00" + "time": "2016-12-09 02:45:31" }, { "name": "phpunit/phpunit-mock-objects", @@ -3917,7 +3921,7 @@ "mock", "xunit" ], - "time": "2015-10-02T06:51:40+00:00" + "time": "2015-10-02 06:51:40" }, { "name": "sebastian/comparator", @@ -3981,7 +3985,7 @@ "compare", "equality" ], - "time": "2016-11-19T09:18:40+00:00" + "time": "2016-11-19 09:18:40" }, { "name": "sebastian/diff", @@ -4033,7 +4037,7 @@ "keywords": [ "diff" ], - "time": "2015-12-08T07:14:41+00:00" + "time": "2015-12-08 07:14:41" }, { "name": "sebastian/environment", @@ -4083,7 +4087,7 @@ "environment", "hhvm" ], - "time": "2016-08-18T05:49:44+00:00" + "time": "2016-08-18 05:49:44" }, { "name": "sebastian/exporter", @@ -4150,7 +4154,7 @@ "export", "exporter" ], - "time": "2016-06-17T09:04:28+00:00" + "time": "2016-06-17 09:04:28" }, { "name": "sebastian/global-state", @@ -4201,7 +4205,7 @@ "keywords": [ "global state" ], - "time": "2015-10-12T03:26:01+00:00" + "time": "2015-10-12 03:26:01" }, { "name": "sebastian/recursion-context", @@ -4254,7 +4258,7 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2015-11-11T19:50:13+00:00" + "time": "2015-11-11 19:50:13" }, { "name": "sebastian/version", @@ -4289,7 +4293,7 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", - "time": "2015-06-21T13:59:46+00:00" + "time": "2015-06-21 13:59:46" }, { "name": "symfony/filesystem", @@ -4338,7 +4342,7 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2016-11-24T00:46:43+00:00" + "time": "2016-11-24 00:46:43" }, { "name": "symfony/form", @@ -4413,7 +4417,7 @@ ], "description": "Symfony Form Component", "homepage": "https://symfony.com", - "time": "2016-12-13T08:00:09+00:00" + "time": "2016-12-13 08:00:09" }, { "name": "symfony/inflector", @@ -4470,7 +4474,7 @@ "symfony", "words" ], - "time": "2016-06-14T11:18:32+00:00" + "time": "2016-06-14 11:18:32" }, { "name": "symfony/intl", @@ -4545,7 +4549,7 @@ "l10n", "localization" ], - "time": "2016-11-18T21:17:59+00:00" + "time": "2016-11-18 21:17:59" }, { "name": "symfony/options-resolver", @@ -4599,7 +4603,7 @@ "configuration", "options" ], - "time": "2016-05-13T18:13:23+00:00" + "time": "2016-05-13 18:13:23" }, { "name": "symfony/polyfill-intl-icu", @@ -4657,7 +4661,7 @@ "portable", "shim" ], - "time": "2016-11-14T01:06:16+00:00" + "time": "2016-11-14 01:06:16" }, { "name": "symfony/polyfill-php70", @@ -4716,7 +4720,7 @@ "portable", "shim" ], - "time": "2016-11-14T01:06:16+00:00" + "time": "2016-11-14 01:06:16" }, { "name": "symfony/property-access", @@ -4784,7 +4788,7 @@ "property path", "reflection" ], - "time": "2016-12-08T15:18:22+00:00" + "time": "2016-12-08 15:18:22" }, { "name": "symfony/routing", @@ -4859,7 +4863,7 @@ "uri", "url" ], - "time": "2016-11-25T12:32:42+00:00" + "time": "2016-11-25 12:32:42" }, { "name": "symfony/twig-bridge", @@ -4940,7 +4944,7 @@ ], "description": "Symfony Twig Bridge", "homepage": "https://symfony.com", - "time": "2016-12-12T19:31:24+00:00" + "time": "2016-12-12 19:31:24" }, { "name": "symfony/yaml", @@ -4995,7 +4999,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2016-12-10T10:07:06+00:00" + "time": "2016-12-10 10:07:06" }, { "name": "there4/slim-test-helpers", @@ -5046,7 +5050,7 @@ "keywords": [ "slim" ], - "time": "2016-08-07T13:07:45+00:00" + "time": "2016-08-07 13:07:45" }, { "name": "twig/extensions", @@ -5098,7 +5102,7 @@ "i18n", "text" ], - "time": "2016-10-25T17:34:14+00:00" + "time": "2016-10-25 17:34:14" }, { "name": "umpirsky/twig-gettext-extractor", @@ -5200,7 +5204,7 @@ "check", "validate" ], - "time": "2016-11-23T20:04:58+00:00" + "time": "2016-11-23 20:04:58" }, { "name": "zircote/swagger-php", @@ -5262,7 +5266,7 @@ "rest", "service discovery" ], - "time": "2016-12-16T12:39:03+00:00" + "time": "2016-12-16 12:39:03" } ], "aliases": [], diff --git a/gulpfile.js b/gulpfile.js index 939dbdbcda..798962afde 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,5 +1,5 @@ // Include gulp -var version = '1.8.2'; +var version = '1.8.3'; var gulp = require("gulp"); var del = require("del"); var composer = require("gulp-composer"); diff --git a/install/master/data.sql b/install/master/data.sql index bb310911ec..10a495eeac 100644 --- a/install/master/data.sql +++ b/install/master/data.sql @@ -1,5 +1,5 @@ INSERT INTO `version` (`app_ver`, `XmdsVersion`, `XlfVersion`, `DBVersion`) VALUES -('1.8.2', 5, 2, 133); +('1.8.3', 5, 2, 134); INSERT INTO `group` (`groupID`, `group`, `IsUserSpecific`, `IsEveryone`, `isSystemNotification`) VALUES (1, 'Users', 0, 0, 0), @@ -102,7 +102,7 @@ INSERT INTO `module` (`ModuleID`, `Module`, `Name`, `Enabled`, `RegionSpecific`, (11, 'datasetview', 'Data Set', 1, 1, 'A view on a DataSet', 'forms/datasetview.gif', 1, NULL, 1, 1, NULL, NULL, '../modules', 'Xibo\\Widget\\DataSetView', 60), (12, 'shellcommand', 'Shell Command', 1, 1, 'Execute a shell command on the client', 'forms/shellcommand.gif', 1, NULL, 1, 1, NULL, NULL, '../modules', 'Xibo\\Widget\\ShellCommand', 3), (13, 'localvideo', 'Local Video', 1, 1, 'Play a video locally stored on the client', 'forms/video.gif', 1, NULL, 0, 1, NULL, NULL, '../modules', 'Xibo\\Widget\\LocalVideo', 60), - (14, 'genericfile', 'Generic File', 1, 0, 'A generic file to be stored in the library', 'forms/library.gif', 1, 'apk,js,html,htm', 0, 0, NULL, NULL, '../modules', 'Xibo\\Widget\\GenericFile', 10), + (14, 'genericfile', 'Generic File', 1, 0, 'A generic file to be stored in the library', 'forms/library.gif', 1, 'apk,ipk,js,html,htm', 0, 0, NULL, NULL, '../modules', 'Xibo\\Widget\\GenericFile', 10), (15, 'clock', 'Clock', 1, 1, '', 'forms/library.gif', 1, NULL, 1, 1, 'html', '[]', '../modules', 'Xibo\\Widget\\Clock', 5), (16, 'font', 'Font', 1, 0, 'A font to use in other Modules', 'forms/library.gif', 1, 'ttf,otf,eot,svg,woff', 0, 0, NULL, NULL, '../modules', 'Xibo\\Widget\\Font', 10), (17, 'audio', 'Audio', 1, 0, 'Audio - support varies depending on the client hardware', 'forms/video.gif', 1, 'mp3,wav', 0, 1, NULL, NULL, '../modules', 'Xibo\\Widget\\Audio', 0), @@ -216,7 +216,6 @@ INSERT INTO `setting` (`settingid`, `setting`, `value`, `fieldType`, `helptext`, (77, 'FORCE_HTTPS', '0', 'checkbox', 'Force the portal into HTTPS?', NULL, 'network', 1, 'Force HTTPS?', '', 70, '0', 1, 'checkbox'), (78, 'ISSUE_STS', '0', 'checkbox', 'Add STS to the response headers? Make sure you fully understand STS before turning it on as it will prevent access via HTTP after the first successful HTTPS connection.', NULL, 'network', 1, 'Enable STS?', '', 80, '0', 1, 'checkbox'), (79, 'STS_TTL', '600', 'text', 'The Time to Live (maxage) of the STS header expressed in seconds.', NULL, 'network', 1, 'STS Time out', '', 90, '600', 1, 'int'), -(80, 'MAINTENANCE_ALERTS_FOR_VIEW_USERS', '0', 'checkbox', 'Email maintenance alerts for users with view permissions to effected Displays.', NULL, 'displays', 1, 'Maintenance Alerts for Users', '', 60, '0', 1, 'checkbox'), (81, 'CALENDAR_TYPE', 'Gregorian', 'dropdown', 'Which Calendar Type should the CMS use?', 'Gregorian|Jalali', 'regional', 1, 'Calendar Type', '', 50, 'Gregorian', 1, 'string'), (82, 'DASHBOARD_LATEST_NEWS_ENABLED', '1', 'checkbox', 'Should the Dashboard show latest news? The address is provided by the theme.', '', 'general', 1, 'Enable Latest News?', '', 110, '1', 1, 'checkbox'), (83, 'LIBRARY_MEDIA_DELETEOLDVER_CHECKB','Checked','dropdown','Default the checkbox for Deleting Old Version of media when a new file is being uploaded to the library.','Checked|Unchecked','defaults',1,'Default for "Delete old version of Media" checkbox. Shown when Editing Library Media.', '', 50, 'Unchecked', 1, 'dropdown'), @@ -285,7 +284,7 @@ INSERT INTO `tag` (`tagId`, `tag`) VALUES (3, 'thumbnail'); INSERT INTO `displayprofile` (`name`, `type`, `config`, `isdefault`, `userid`) -VALUES ('Windows', 'windows', '[]', '1', '1'), ('Android', 'android', '[]', '1', '1'); +VALUES ('Windows', 'windows', '[]', '1', '1'), ('Android', 'android', '[]', '1', '1'), ('webOS', 'lg', '[]', '1', '1'); INSERT INTO `permissionentity` (`entityId`, `entity`) VALUES (1, 'Xibo\\Entity\\Page'), diff --git a/install/master/structure.sql b/install/master/structure.sql index fcde792d42..2497c969d8 100644 --- a/install/master/structure.sql +++ b/install/master/structure.sql @@ -228,6 +228,7 @@ CREATE TABLE IF NOT EXISTS `group` ( `IsEveryone` tinyint(4) NOT NULL DEFAULT '0', `libraryQuota` int(11) DEFAULT NULL, `isSystemNotification` tinyint(4) NOT NULL DEFAULT '0', + `isDisplayNotification` tinyint(4) NOT NULL DEFAULT '0', PRIMARY KEY (`groupID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Groups' AUTO_INCREMENT=2 ; @@ -397,6 +398,15 @@ CREATE TABLE IF NOT EXISTS `lktagcampaign` ( -- -------------------------------------------------------- +create table lktagdisplaygroup +( + lkTagDisplayGroupId int auto_increment primary key, + tagId int not null, + displayGroupId int not null, + constraint tagId + unique (tagId, displayGroupId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ; + -- -- Table structure for table `lkusergroup` -- @@ -468,6 +478,8 @@ CREATE TABLE IF NOT EXISTS `media` ( `expires` int(11) DEFAULT NULL, `released` tinyint(4) NOT NULL DEFAULT '1', `apiRef` varchar(254) NULL, + `createdDt` DATETIME NULL, + `modifiedDt` DATETIME NULL, PRIMARY KEY (`mediaID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ; diff --git a/install/steps/134.json b/install/steps/134.json new file mode 100644 index 0000000000..13316fdbd6 --- /dev/null +++ b/install/steps/134.json @@ -0,0 +1,42 @@ +{ + "dbVersion": 134, + "appVersion": "1.8.3", + "steps": [ + { + "step": "Add Xibo for webOS display profile", + "action": "INSERT INTO displayprofile (name, type, config, isdefault, userId) VALUES ('webOS', 'lg', '{}', 1, 1)" + }, + { + "step": "Add Notification Module", + "action": "INSERT INTO module (Module, Name, Enabled, RegionSpecific, Description, ImageUri, SchemaVersion, ValidExtensions, PreviewEnabled, assignable, render_as, settings, viewPath, class, defaultDuration) VALUES ('notificationview', 'Notification', 1, 1, 'Display Notifications from the Notification Centre', 'forms/library.gif', 1, null, 1, 1, 'html', null, '../modules', 'Xibo\\\\Widget\\\\NotificationView', 10);" + }, + { + "step": "Add isDisplayNotification setting to Groups", + "action": "ALTER TABLE `group` ADD isDisplayNotification TINYINT DEFAULT 0 NULL;" + }, + { + "step": "Remove MAINTENANCE_ALERTS_FOR_VIEW_USERS setting", + "action": "DELETE FROM `setting` WHERE setting = 'MAINTENANCE_ALERTS_FOR_VIEW_USERS';" + }, + { + "step": "Create Tag Display Group Link Table", + "action": "create table lktagdisplaygroup (lkTagDisplayGroupId int auto_increment primary key,tagId int not null,displayGroupId int not null, constraint tagId unique (tagId, displayGroupId)) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;" + }, + { + "step": "CreatedDt/ModifiedDt for Library Media", + "action": "ALTER TABLE media ADD createdDt DATETIME NULL, ADD modifiedDt DATETIME NULL;" + }, + { + "step": "Add IPK to generic file module", + "action": "UPDATE `module` SET validextensions = CONCAT(validextensions, ',ipk') WHERE module = 'genericfile' LIMIT 1;" + }, + { + "step": "Update the Currencies Module description", + "action": "UPDATE `module` SET description = 'A module for showing Currency pairs and exchange rates' WHERE module = 'currencies';" + }, + { + "step": "Update the Stocks Module description", + "action": "UPDATE `module` SET description = 'A module for showing Stock quotes' WHERE module = 'stocks';" + } + ] +} \ No newline at end of file diff --git a/lib/Controller/Applications.php b/lib/Controller/Applications.php index 731bccc06f..9e87d79788 100644 --- a/lib/Controller/Applications.php +++ b/lib/Controller/Applications.php @@ -26,9 +26,11 @@ use Xibo\Entity\Application; use Xibo\Entity\ApplicationScope; use Xibo\Exception\AccessDeniedException; +use Xibo\Exception\InvalidArgumentException; use Xibo\Factory\ApplicationFactory; use Xibo\Factory\ApplicationRedirectUriFactory; use Xibo\Factory\ApplicationScopeFactory; +use Xibo\Factory\UserFactory; use Xibo\Helper\Session; use Xibo\Service\ConfigServiceInterface; use Xibo\Service\DateServiceInterface; @@ -68,6 +70,9 @@ class Applications extends Base /** @var ApplicationScopeFactory */ private $applicationScopeFactory; + /** @var UserFactory */ + private $userFactory; + /** * Set common dependencies. * @param LogServiceInterface $log @@ -81,8 +86,9 @@ class Applications extends Base * @param StorageServiceInterface $store * @param ApplicationFactory $applicationFactory * @param ApplicationRedirectUriFactory $applicationRedirectUriFactory + * @param UserFactory $userFactory */ - public function __construct($log, $sanitizerService, $state, $user, $help, $date, $config, $session, $store, $applicationFactory, $applicationRedirectUriFactory, $applicationScopeFactory) + public function __construct($log, $sanitizerService, $state, $user, $help, $date, $config, $session, $store, $applicationFactory, $applicationRedirectUriFactory, $applicationScopeFactory, $userFactory) { $this->setCommonDependencies($log, $sanitizerService, $state, $user, $help, $date, $config); @@ -91,6 +97,7 @@ public function __construct($log, $sanitizerService, $state, $user, $help, $date $this->applicationFactory = $applicationFactory; $this->applicationRedirectUriFactory = $applicationRedirectUriFactory; $this->applicationScopeFactory = $applicationScopeFactory; + $this->userFactory = $userFactory; } /** @@ -251,6 +258,7 @@ public function editForm($clientId) $this->getState()->setData([ 'client' => $client, 'scopes' => $scopes, + 'users' => $this->userFactory->query(), 'help' => $this->getHelp()->link('Services', 'Register') ]); } @@ -294,10 +302,12 @@ public function add() /** * Edit Application * @param $clientId - * @throws \Xibo\Exception\NotFoundException + * @throws \Xibo\Exception\XiboException */ public function edit($clientId) { + $this->getLog()->debug('Editing ' . $clientId); + // Get the client $client = $this->applicationFactory->getById($clientId); @@ -358,6 +368,18 @@ public function edit($clientId) $client->unassignScope($scope); } + // Change the ownership? + if ($this->getSanitizer()->getInt('userId') !== null) { + // Check we have permissions to view this user + $user = $this->userFactory->getById($this->getSanitizer()->getInt('userId')); + + $this->getLog()->debug('Attempting to change ownership to ' . $user->userId . ' - ' . $user->userName); + + if (!$this->getUser()->checkViewable($user)) + throw new InvalidArgumentException('You do not have permission to assign this user', 'userId'); + + $client->userId = $user->userId; + } $client->save(); diff --git a/lib/Controller/Base.php b/lib/Controller/Base.php index d8f1481f1c..d3638484d8 100644 --- a/lib/Controller/Base.php +++ b/lib/Controller/Base.php @@ -454,4 +454,15 @@ public function renderTwigAjaxReturn($data, $app, $state) $state->fieldActions = json_decode($view['fieldActions']); } } + + /** + * Render a template to string + * @param string $template + * @param array $data + * @return string + */ + public function renderTemplateToString($template, $data) + { + return $this->getApp()->view()->render($template . '.twig', $data); + } } \ No newline at end of file diff --git a/lib/Controller/Campaign.php b/lib/Controller/Campaign.php index 28dc63794b..9f4b6d7a86 100644 --- a/lib/Controller/Campaign.php +++ b/lib/Controller/Campaign.php @@ -663,7 +663,7 @@ public function preview($campaignId) $extendedLayouts = []; foreach($layouts as $layout) { - $duration += $layout->duration ; + $duration += $layout->duration; $extendedLayouts[] = ['layout' => $layout, 'duration' => $layout->duration, 'previewOptions' => [ diff --git a/lib/Controller/DataSet.php b/lib/Controller/DataSet.php index 928fbd4596..375b058eda 100644 --- a/lib/Controller/DataSet.php +++ b/lib/Controller/DataSet.php @@ -117,6 +117,13 @@ public function displayPage() * type="string", * required=false * ), + * @SWG\Parameter( + * name="embed", + * in="formData", + * description="Embed related data such as columns", + * type="string", + * required=false + * ), * @SWG\Response( * response=200, * description="successful operation", @@ -130,7 +137,10 @@ public function displayPage() public function grid() { $user = $this->getUser(); - + + // Embed? + $embed = ($this->getSanitizer()->getString('embed') != null) ? explode(',', $this->getSanitizer()->getString('embed')) : []; + $filter = [ 'dataSetId' => $this->getSanitizer()->getInt('dataSetId'), 'dataSet' => $this->getSanitizer()->getString('dataSet'), @@ -141,7 +151,9 @@ public function grid() foreach ($dataSets as $dataSet) { /* @var \Xibo\Entity\DataSet $dataSet */ - + if (in_array('columns', $embed)) { + $dataSet->load(); + } if ($this->isApi()) break; @@ -294,7 +306,9 @@ public function add() $dataSetColumn->dataTypeId = 1; // Add Column - $dataSet->assignColumn($dataSetColumn); + // only when we are not routing through the API + if (!$this->isApi()) + $dataSet->assignColumn($dataSetColumn); // Save $dataSet->save(); @@ -444,7 +458,9 @@ public function delete($dataSetId) if (!$this->getUser()->checkDeleteable($dataSet)) throw new AccessDeniedException(); + $this->getLog()->debug('Delete data flag = ' . $this->getSanitizer()->getCheckbox('deleteData') . '. Params = ' . var_export($this->getApp()->request()->params(), true)); + // Is there existing data? if ($this->getSanitizer()->getCheckbox('deleteData') == 0 && $dataSet->hasData()) throw new \InvalidArgumentException(__('There is data assigned to this data set, cannot delete.')); @@ -568,12 +584,33 @@ public function copy($dataSetId) * required=true * ), * @SWG\Parameter( - * name="file", + * name="files", * in="formData", * description="The file", * type="file", * required=true * ), + * @SWG\Parameter( + * name="csvImport_{dataSetColumnId}", + * in="formData", + * description="You need to provide dataSetColumnId after csvImport_, to know your dataSet columns Ids, you will need to use the GET /dataset/{dataSetId}/column call first. The value of this parameter is the index of the column in your csv file, where the first column is 1", + * type="integer", + * required=true + * ), + * @SWG\Parameter( + * name="overwrite", + * in="formData", + * description="flag (0,1) Set to 1 to erase all content in the dataSet and overwrite it with new content in this import", + * type="integer", + * required=false + * ), + * @SWG\Parameter( + * name="ignorefirstrow", + * in="formData", + * description="flag (0,1), Set to 1 to Ignore first row, useful if the CSV file has headings", + * type="integer", + * required=false + * ), * @SWG\Response( * response=200, * description="successful operation" @@ -721,7 +758,7 @@ public function importJson($dataSetId) $filter = ''; foreach ($data['uniqueKeys'] as $uniqueKey) { if (isset($sanitizedRow[$uniqueKey])) { - $filter .= 'AND ' . $uniqueKey . '= \'' . $sanitizedRow[$uniqueKey] . '\' '; + $filter .= 'AND `' . $uniqueKey . '` = \'' . $sanitizedRow[$uniqueKey] . '\' '; } } $filter = trim($filter, 'AND'); diff --git a/lib/Controller/Display.php b/lib/Controller/Display.php index 3b127426a9..68d4b4a533 100644 --- a/lib/Controller/Display.php +++ b/lib/Controller/Display.php @@ -34,6 +34,7 @@ use Xibo\Factory\MediaFactory; use Xibo\Factory\RequiredFileFactory; use Xibo\Factory\ScheduleFactory; +use Xibo\Factory\TagFactory; use Xibo\Helper\ByteFormatter; use Xibo\Helper\WakeOnLan; use Xibo\Service\ConfigServiceInterface; @@ -107,6 +108,9 @@ class Display extends Base /** @var RequiredFileFactory */ private $requiredFileFactory; + /** @var TagFactory */ + private $tagFactory; + /** * Set common dependencies. * @param LogServiceInterface $log @@ -128,8 +132,9 @@ class Display extends Base * @param ScheduleFactory $scheduleFactory * @param DisplayEventFactory $displayEventFactory * @param RequiredFileFactory $requiredFileFactory + * @param TagFactory $tagFactory */ - public function __construct($log, $sanitizerService, $state, $user, $help, $date, $config, $store, $pool, $playerAction, $displayFactory, $displayGroupFactory, $logFactory, $layoutFactory, $displayProfileFactory, $mediaFactory, $scheduleFactory, $displayEventFactory, $requiredFileFactory) + public function __construct($log, $sanitizerService, $state, $user, $help, $date, $config, $store, $pool, $playerAction, $displayFactory, $displayGroupFactory, $logFactory, $layoutFactory, $displayProfileFactory, $mediaFactory, $scheduleFactory, $displayEventFactory, $requiredFileFactory, $tagFactory) { $this->setCommonDependencies($log, $sanitizerService, $state, $user, $help, $date, $config); @@ -145,6 +150,7 @@ public function __construct($log, $sanitizerService, $state, $user, $help, $date $this->scheduleFactory = $scheduleFactory; $this->displayEventFactory = $displayEventFactory; $this->requiredFileFactory = $requiredFileFactory; + $this->tagFactory = $tagFactory; } /** @@ -176,14 +182,6 @@ function displayManage($displayId) if (!$this->getUser()->checkViewable($display)) throw new AccessDeniedException(); - // Errors in the last 24 hours - $errors = $this->logFactory->query(null, [ - 'displayId' => $display->displayId, - 'type' => 'ERROR', - 'fromDt' => $this->getDate()->getLocalDate($this->getDate()->parse()->subHours(24), 'U'), - 'toDt' => $this->getDate()->getLocalDate(null, 'U') - ]); - // Zero out some variables $layouts = []; $widgets = []; @@ -302,7 +300,12 @@ function displayManage($displayId) 'requiredFiles' => [], 'display' => $display, 'timeAgo' => $this->getDate()->parse($display->lastAccessed, 'U')->diffForHumans(), - 'errors' => $errors, + 'errorSearch' => http_build_query([ + 'displayId' => $display->displayId, + 'type' => 'ERROR', + 'fromDt' => $this->getDate()->getLocalDate($this->getDate()->parse()->subHours(12)), + 'toDt' => $this->getDate()->getLocalDate() + ]), 'inventory' => [ 'layouts' => $layouts, 'media' => $media, @@ -419,6 +422,9 @@ function grid() 'clientVersion' => $this->getSanitizer()->getString('clientVersion'), 'authorised' => $this->getSanitizer()->getInt('authorised'), 'displayProfileId' => $this->getSanitizer()->getInt('displayProfileId'), + 'tags' => $this->getSanitizer()->getString('tags'), + 'exactTags' => $this->getSanitizer()->getCheckbox('exactTags'), + 'showTags' => true, ]; // Get a list of displays @@ -630,7 +636,7 @@ function grid() */ function editForm($displayId) { - $display = $this->displayFactory->getById($displayId); + $display = $this->displayFactory->getById($displayId, true); if (!$this->getUser()->checkEditable($display)) throw new AccessDeniedException(); @@ -664,7 +670,12 @@ function editForm($displayId) } else { // A format has been set $format = (strlen($profile[$i]['value']) == 5) ? 'H:i' : 'H:i:s'; - $profile[$i]['valueString'] = $this->getDate()->parse($profile[$i]['value'], $format)->format($timeFormat); + try { + $profile[$i]['valueString'] = $this->getDate()->parse($profile[$i]['value'], $format)->format($timeFormat); + } catch (\InvalidArgumentException $invalidArgumentException) { + $this->getLog()->error('Display Profile contains an invalid time format, expecting ' . $format . ' value is ' . $profile[$i]['value']); + $profile[$i]['valueString'] = '00:00'; + } } } } @@ -747,6 +758,13 @@ function deleteForm($displayId) * required=false * ), * @SWG\Parameter( + * name="tags", + * in="formData", + * description="A comma separated list of tags for this item", + * type="string", + * required=false + * ), + * @SWG\Parameter( * name="auditingUntil", * in="formData", * description="A date this Display records auditing information until.", @@ -882,7 +900,7 @@ function deleteForm($displayId) */ function edit($displayId) { - $display = $this->displayFactory->getById($displayId); + $display = $this->displayFactory->getById($displayId, true); if (!$this->getUser()->checkEditable($display)) throw new AccessDeniedException(); @@ -912,6 +930,9 @@ function edit($displayId) $display->timeZone = $this->getSanitizer()->getString('timeZone'); $display->displayProfileId = $this->getSanitizer()->getInt('displayProfileId'); + // Tags are stored on the displaygroup, we're just passing through here + $display->tags = $this->tagFactory->tagsFromString($this->getSanitizer()->getString('tags')); + if ($display->auditingUntil !== null) $display->auditingUntil = $display->auditingUntil->format('U'); diff --git a/lib/Controller/DisplayGroup.php b/lib/Controller/DisplayGroup.php index 576873e863..30b5ac3387 100644 --- a/lib/Controller/DisplayGroup.php +++ b/lib/Controller/DisplayGroup.php @@ -31,6 +31,7 @@ use Xibo\Factory\MediaFactory; use Xibo\Factory\ModuleFactory; use Xibo\Factory\ScheduleFactory; +use Xibo\Factory\TagFactory; use Xibo\Service\ConfigServiceInterface; use Xibo\Service\DateServiceInterface; use Xibo\Service\LogServiceInterface; @@ -88,6 +89,11 @@ class DisplayGroup extends Base */ private $scheduleFactory; + /** + * @var TagFactory + */ + private $tagFactory; + /** * Set common dependencies. * @param LogServiceInterface $log @@ -105,8 +111,9 @@ class DisplayGroup extends Base * @param MediaFactory $mediaFactory * @param CommandFactory $commandFactory * @param ScheduleFactory $scheduleFactory + * @param TagFactory $tagFactory */ - public function __construct($log, $sanitizerService, $state, $user, $help, $date, $config, $playerAction, $displayFactory, $displayGroupFactory, $layoutFactory, $moduleFactory, $mediaFactory, $commandFactory, $scheduleFactory) + public function __construct($log, $sanitizerService, $state, $user, $help, $date, $config, $playerAction, $displayFactory, $displayGroupFactory, $layoutFactory, $moduleFactory, $mediaFactory, $commandFactory, $scheduleFactory, $tagFactory) { $this->setCommonDependencies($log, $sanitizerService, $state, $user, $help, $date, $config); @@ -118,6 +125,7 @@ public function __construct($log, $sanitizerService, $state, $user, $help, $date $this->mediaFactory = $mediaFactory; $this->commandFactory = $commandFactory; $this->scheduleFactory = $scheduleFactory; + $this->tagFactory = $tagFactory; } /** @@ -194,7 +202,9 @@ public function grid() 'displayGroup' => $this->getSanitizer()->getString('displayGroup'), 'displayId' => $this->getSanitizer()->getInt('displayId'), 'nestedDisplayId' => $this->getSanitizer()->getInt('nestedDisplayId'), - 'dynamicCriteria' => $this->getSanitizer()->getString('dynamicCriteria') + 'dynamicCriteria' => $this->getSanitizer()->getString('dynamicCriteria'), + 'tags' => $this->getSanitizer()->getString('tags'), + 'exactTags' => $this->getSanitizer()->getCheckbox('exactTags') ]; $displayGroups = $this->displayGroupFactory->query($this->gridRenderSort(), $this->gridRenderFilter($filter)); @@ -393,6 +403,13 @@ public function membersForm($displayGroupId) * required=false * ), * @SWG\Parameter( + * name="tags", + * in="formData", + * description="A comma separated list of tags for this item", + * type="string", + * required=false + * ), + * @SWG\Parameter( * name="isDynamic", * in="formData", * description="Flag indicating whether this DisplayGroup is Dynamic", @@ -425,6 +442,7 @@ public function add() $displayGroup->displayGroup = $this->getSanitizer()->getString('displayGroup'); $displayGroup->description = $this->getSanitizer()->getString('description'); + $displayGroup->tags = $this->tagFactory->tagsFromString($this->getSanitizer()->getString('tags')); $displayGroup->isDynamic = $this->getSanitizer()->getCheckbox('isDynamic'); $displayGroup->dynamicCriteria = $this->getSanitizer()->getString('dynamicCriteria'); $displayGroup->userId = $this->getUser()->userId; @@ -471,6 +489,13 @@ public function add() * required=false * ), * @SWG\Parameter( + * name="tags", + * in="formData", + * description="A comma separated list of tags for this item", + * type="string", + * required=false + * ), + * @SWG\Parameter( * name="isDynamic", * in="formData", * description="Flag indicating whether this DisplayGroup is Dynamic", @@ -501,6 +526,7 @@ public function edit($displayGroupId) $displayGroup->setChildObjectDependencies($this->displayFactory, $this->layoutFactory, $this->mediaFactory, $this->scheduleFactory); $displayGroup->displayGroup = $this->getSanitizer()->getString('displayGroup'); $displayGroup->description = $this->getSanitizer()->getString('description'); + $displayGroup->replaceTags($this->tagFactory->tagsFromString($this->getSanitizer()->getString('tags'))); $displayGroup->isDynamic = $this->getSanitizer()->getCheckbox('isDynamic'); $displayGroup->dynamicCriteria = ($displayGroup->isDynamic == 1) ? $this->getSanitizer()->getString('dynamicCriteria') : null; $displayGroup->save(); @@ -1106,7 +1132,7 @@ public function LayoutsForm($displayGroupId) * ) * ), * @SWG\Parameter( - * name="unassignLayoutsId", + * name="unassignLayoutId", * type="array", * in="formData", * description="Optional array of Layouts Id to unassign", @@ -1146,7 +1172,7 @@ public function assignLayouts($displayGroupId) } // Check for unassign - foreach ($this->getSanitizer()->getIntArray('unassignLayoutsId') as $layoutId) { + foreach ($this->getSanitizer()->getIntArray('unassignLayoutId') as $layoutId) { // Get the layout record $layout = $this->layoutFactory->getById($layoutId); @@ -1376,6 +1402,47 @@ public function collectNow($displayGroupId) ]); } + /** + * Cause the player to collect now + * @param int $displayGroupId + * @throws ConfigurationException when the message cannot be sent + * + * @SWG\Post( + * path="/displaygroup/{displayGroupId}/action/clearStatsAndLogs", + * operationId="displayGroupActionClearStatsAndLogs", + * tags={"displayGroup"}, + * summary="Action: Clear Stats and Logs", + * description="Clear all stats and logs on this Group", + * @SWG\Parameter( + * name="displayGroupId", + * in="path", + * description="The display group id", + * type="integer", + * required=true + * ), + * @SWG\Response( + * response=204, + * description="successful operation" + * ) + * ) + */ + public function clearStatsAndLogs($displayGroupId) + { + $displayGroup = $this->displayGroupFactory->getById($displayGroupId); + + if (!$this->getUser()->checkEditable($displayGroup)) + throw new AccessDeniedException(); + + $this->playerAction->sendAction($this->displayFactory->getByDisplayGroupId($displayGroupId), new CollectNowAction()); + + // Return + $this->getState()->hydrate([ + 'httpStatus' => 204, + 'message' => sprintf(__('Command Sent to %s'), $displayGroup->displayGroup), + 'id' => $displayGroup->displayGroupId + ]); + } + /** * Change to a new Layout * @param $displayGroupId @@ -1446,20 +1513,34 @@ public function changeLayout($displayGroupId) // Check that this user has permissions to see this layout $layout = $this->layoutFactory->getById($layoutId); + if (!$this->getUser()->checkViewable($layout)) + throw new AccessDeniedException(); + // Check to see if this layout is assigned to this display group. if (count($this->layoutFactory->query(null, ['disableUserCheck' => 1, 'layoutId' => $layoutId, 'displayGroupId' => $displayGroupId])) <= 0) { // Assign $displayGroup->setChildObjectDependencies($this->displayFactory, $this->layoutFactory, $this->mediaFactory, $this->scheduleFactory); $displayGroup->load(); $displayGroup->assignLayout($layout); - // Don't notify, this player action will cause a download. + + // Don't collect now, this player action will cause a download. + // notify will still occur if the layout isn't already assigned (which is shouldn't be) $displayGroup->setCollectRequired(false); + $displayGroup->save(['validate' => false]); // Convert into a download required $downloadRequired = true; + } else { + // The layout may not be built at this point + if ($downloadRequired) { + // in this case we should build it and notify before we send the action + // notify should NOT collect now, as we will do that during our own action. + $layout->xlfToDisk(['notify' => true, 'collectNow' => false]); + } } + // Create and send the player action $this->playerAction->sendAction($this->displayFactory->getByDisplayGroupId($displayGroupId), (new ChangeLayoutAction())->setLayoutDetails( $layoutId, $this->getSanitizer()->getInt('duration'), @@ -1579,6 +1660,9 @@ public function overlayLayout($displayGroupId) // Check that this user has permissions to see this layout $layout = $this->layoutFactory->getById($layoutId); + if (!$this->getUser()->checkViewable($layout)) + throw new AccessDeniedException(); + // Check to see if this layout is assigned to this display group. if (count($this->layoutFactory->query(null, ['disableUserCheck' => 1, 'layoutId' => $layoutId, 'displayGroupId' => $displayGroupId])) <= 0) { // Assign @@ -1591,6 +1675,13 @@ public function overlayLayout($displayGroupId) // Convert into a download required $downloadRequired = true; + } else { + // The layout may not be built at this point + if ($downloadRequired) { + // in this case we should build it and notify before we send the action + // notify should NOT collect now, as we will do that during our own action. + $layout->xlfToDisk(['notify' => true, 'collectNow' => false]); + } } $this->playerAction->sendAction($this->displayFactory->getByDisplayGroupId($displayGroupId), (new OverlayLayoutAction())->setLayoutDetails( diff --git a/lib/Controller/DisplayProfile.php b/lib/Controller/DisplayProfile.php index c14f6fb259..4d71323fe5 100644 --- a/lib/Controller/DisplayProfile.php +++ b/lib/Controller/DisplayProfile.php @@ -166,12 +166,6 @@ function grid() function addForm() { $this->getState()->template = 'displayprofile-form-add'; - $this->getState()->setData([ - 'clientTypes' => array( - array('key' => 'windows', 'value' => 'Windows'), - array('key' => 'android', 'value' => 'Android') - ) - ]); } /** diff --git a/lib/Controller/Layout.php b/lib/Controller/Layout.php index ca07d9a508..3514966e05 100644 --- a/lib/Controller/Layout.php +++ b/lib/Controller/Layout.php @@ -187,13 +187,14 @@ public function displayDesigner($layoutId) $resolution = $this->resolutionFactory->getByDimensions($layout->width, $layout->height); $moduleFactory = $this->moduleFactory; + $isTemplate = $layout->hasTag('template'); // Set up any JavaScript translations $data = [ 'layout' => $layout, 'resolution' => $resolution, - 'isTemplate' => $layout->hasTag('template'), - 'layouts' => $this->layoutFactory->query(), + 'isTemplate' => $isTemplate, + 'layouts' => $this->layoutFactory->query(null, ['excludeTemplates' => $isTemplate ? 0 : 1]), 'zoom' => $this->getSanitizer()->getDouble('zoom', $this->getUser()->getOptionValue('defaultDesignerZoom', 1)), 'modules' => array_map(function($element) use ($moduleFactory) { return $moduleFactory->createForInstall($element->class); }, $moduleFactory->getAssignableModules()) ]; @@ -602,6 +603,13 @@ function retire($layoutId) * required=false * ), * @SWG\Parameter( + * name="exactTags", + * in="formData", + * description="A flag indicating whether to treat the tags filter as an exact match", + * type="integer", + * required=false + * ), + * @SWG\Parameter( * name="ownerUserGroupId", * in="formData", * description="Filter by users in this UserGroupId", @@ -632,8 +640,14 @@ function grid() // Should we parse the description into markdown $showDescriptionId = $this->getSanitizer()->getInt('showDescriptionId'); - // Embed? - $embed = ($this->getSanitizer()->getString('embed') != null) ? explode(',', $this->getSanitizer()->getString('embed')) : []; + // We might need to embed some extra content into the response if the "Show Description" + // is set to media listing + if ($showDescriptionId === 3) { + $embed = ['regions', 'playlists', 'widgets']; + } else { + // Embed? + $embed = ($this->getSanitizer()->getString('embed') != null) ? explode(',', $this->getSanitizer()->getString('embed')) : []; + } // Get all layouts $layouts = $this->layoutFactory->query($this->gridRenderSort(), $this->gridRenderFilter([ @@ -641,9 +655,11 @@ function grid() 'userId' => $this->getSanitizer()->getInt('userId'), 'retired' => $this->getSanitizer()->getInt('retired'), 'tags' => $this->getSanitizer()->getString('tags'), + 'exactTags' => $this->getSanitizer()->getCheckbox('exactTags'), 'filterLayoutStatusId' => $this->getSanitizer()->getInt('layoutStatusId'), 'layoutId' => $this->getSanitizer()->getInt('layoutId'), - 'ownerUserGroupId' => $this->getSanitizer()->getInt('ownerUserGroupId') + 'ownerUserGroupId' => $this->getSanitizer()->getInt('ownerUserGroupId'), + 'mediaLike' => $this->getSanitizer()->getString('mediaLike') ])); foreach ($layouts as $layout) { @@ -666,6 +682,7 @@ function grid() continue; $layout->includeProperty('buttons'); + $layout->excludeProperty('regions'); $layout->thumbnail = ''; @@ -686,6 +703,24 @@ function grid() } } + if ($showDescriptionId === 3) { + // Load in the entire object model - creating module objects so that we can get the name of each + // widget and its items. + foreach ($layout->regions as $region) { + foreach ($region->playlists as $playlist) { + /* @var Playlist $playlist */ + + foreach ($playlist->widgets as $widget) { + /* @var Widget $widget */ + $widget->module = $this->moduleFactory->createWithWidget($widget, $region); + } + } + } + + // provide our layout object to a template to render immediately + $layout->descriptionFormatted = $this->renderTemplateToString('layout-page-grid-widgetlist', $layout); + } + switch ($layout->status) { case 1: @@ -945,9 +980,32 @@ public function copy($layoutId) $layout->layout = $this->getSanitizer()->getString('name'); $layout->description = $this->getSanitizer()->getString('description'); - // TODO: Copy the media on the layout and change the assignments. + // Copy the media on the layout and change the assignments. + // https://github.com/xibosignage/xibo/issues/1283 if ($this->getSanitizer()->getCheckbox('copyMediaFiles') == 1) { + foreach ($layout->getWidgets() as $widget) { + // Copy the media + $oldMedia = $this->mediaFactory->getById($widget->getPrimaryMediaId()); + $media = clone $oldMedia; + $media->setOwner($this->getUser()->userId); + $media->save(); + + $widget->unassignMedia($oldMedia->mediaId); + $widget->assignMedia($media->mediaId); + + // Update the widget option with the new ID + $widget->setOptionValue('uri', 'attrib', $media->storedAs); + } + // Also handle the background image, if there is one + if ($layout->backgroundImageId != 0) { + $oldMedia = $this->mediaFactory->getById($layout->backgroundImageId); + $media = clone $oldMedia; + $media->setOwner($this->getUser()->userId); + $media->save(); + + $layout->backgroundImageId = $media->mediaId; + } } // Save the new layout @@ -1127,7 +1185,7 @@ public function untag($layoutId) * @SWG\Parameter( * name="layoutId", * in="path", - * description="The Layout Id to Untag", + * description="The Layout Id to get the status", * type="integer", * required=true * ), diff --git a/lib/Controller/Library.php b/lib/Controller/Library.php index 302bdb2202..222252d935 100644 --- a/lib/Controller/Library.php +++ b/lib/Controller/Library.php @@ -348,6 +348,13 @@ function displayPage() * required=false * ), * @SWG\Parameter( + * name="exactTags", + * in="formData", + * description="A flag indicating whether to treat the tags filter as an exact match", + * type="integer", + * required=false + * ), + * @SWG\Parameter( * name="duration", * in="formData", * description="Filter by Duration - a number or less-than,greater-than,less-than-equal or great-than-equal followed by a | followed by a number", @@ -388,6 +395,7 @@ function grid() 'name' => $this->getSanitizer()->getString('media'), 'type' => $this->getSanitizer()->getString('type'), 'tags' => $this->getSanitizer()->getString('tags'), + 'exactTags' => $this->getSanitizer()->getCheckbox('exactTags'), 'ownerId' => $this->getSanitizer()->getInt('ownerId'), 'retired' => $this->getSanitizer()->getInt('retired'), 'duration' => $this->getSanitizer()->getString('duration'), @@ -953,12 +961,20 @@ public function libraryUsage() */ public function download($mediaId, $type = '') { - $this->getLog()->debug('Download request for mediaId %d and type %s', $mediaId, $type); - $media = $this->mediaFactory->getById($mediaId); - if (!$this->getUser()->checkViewable($media)) + $this->getLog()->debug('Download request for mediaId ' . $mediaId . ' and type ' . $type . '. Media is a ' . $media->mediaType); + + if ($media->mediaType === 'module') { + // Make sure that our user has this mediaId assigned to a Widget they can view + // we can't test for normal media permissions, because no user has direct access to these "module" files + // https://github.com/xibosignage/xibo/issues/1304 + if (count($this->widgetFactory->query(null, ['mediaId' => $mediaId])) <= 0) { + throw new AccessDeniedException(); + } + } else if (!$this->getUser()->checkViewable($media)) { throw new AccessDeniedException(); + } if ($type != '') { $widget = $this->moduleFactory->create($type); @@ -1371,7 +1387,7 @@ public function usage($mediaId) // Get a list of displays that this mediaId is used on by direct assignment $displays = $this->displayFactory->query($this->gridRenderSort(), $this->gridRenderFilter(['mediaId' => $mediaId])); - // if we've been provided a date, then we need to assess the schedules + // have we been provided with a date/time to restrict the scheduled events to? $mediaDate = $this->getSanitizer()->getDate('mediaEventDate'); if ($mediaDate !== null) { @@ -1380,15 +1396,27 @@ public function usage($mediaId) $events = $this->scheduleFactory->query(null, [ 'futureSchedulesFrom' => $mediaDate->format('U'), - 'futureSchedulesTo' => $toDate->format('U') + 'futureSchedulesTo' => $toDate->format('U'), + 'mediaId' => $mediaId ]); + } else { + // All scheduled events for this mediaId + $events = $this->scheduleFactory->query(null, [ + 'mediaId' => $mediaId + ]); + } + + // Total records returned from the schedules query + $totalRecords = $this->scheduleFactory->countLast(); - foreach ($events as $row) { - /* @var \Xibo\Entity\Schedule $row */ + foreach ($events as $row) { + /* @var \Xibo\Entity\Schedule $row */ - // Generate this event - $row->setDayPartFactory($this->dayPartFactory); + // Generate this event + $row->setDayPartFactory($this->dayPartFactory); + // Assess the date? + if ($mediaDate !== null) { try { $scheduleEvents = $row->getEvents($mediaDate, $toDate); } catch (XiboException $e) { @@ -1396,35 +1424,91 @@ public function usage($mediaId) continue; } + // Skip events that do not fall within the specified days if (count($scheduleEvents) <= 0) continue; $this->getLog()->debug('EventId ' . $row->eventId . ' as events: ' . json_encode($scheduleEvents)); + } - // Load the display groups - $row->load(); + // Load the display groups + $row->load(); - foreach ($row->displayGroups as $displayGroup) { - foreach ($this->displayFactory->getByDisplayGroupId($displayGroup->displayGroupId) as $display) { - $found = false; + foreach ($row->displayGroups as $displayGroup) { + foreach ($this->displayFactory->getByDisplayGroupId($displayGroup->displayGroupId) as $display) { + $found = false; - // Check to see if our ID is already in our list - foreach ($displays as $existing) { - if ($existing->displayId === $display->displayId) { - $found = true; - break; - } + // Check to see if our ID is already in our list + foreach ($displays as $existing) { + if ($existing->displayId === $display->displayId) { + $found = true; + break; } - - if (!$found) - $displays[] = $display; } + + if (!$found) + $displays[] = $display; } } } $this->getState()->template = 'grid'; - $this->getState()->recordsTotal = $this->mediaFactory->countLast(); + $this->getState()->recordsTotal = $totalRecords; $this->getState()->setData($displays); } + + /** + * @SWG\Get( + * path="/library/usage/layouts/{mediaId}", + * operationId="libraryUsageLayoutsReport", + * tags={"library"}, + * summary="Get Library Item Usage Report for Layouts", + * description="Get the records for the library item usage report for Layouts", + * @SWG\Response( + * response=200, + * description="successful operation" + * ) + * ) + * + * @param int $mediaId + */ + public function usageLayouts($mediaId) + { + $media = $this->mediaFactory->getById($mediaId); + + if (!$this->getUser()->checkViewable($media)) + throw new AccessDeniedException(); + + $layouts = $this->layoutFactory->query(null, ['mediaId' => $mediaId]); + + if (!$this->isApi()) { + foreach ($layouts as $layout) { + $layout->includeProperty('buttons'); + + // Add some buttons for this row + if ($this->getUser()->checkEditable($layout)) { + // Design Button + $layout->buttons[] = array( + 'id' => 'layout_button_design', + 'linkType' => '_self', 'external' => true, + 'url' => $this->urlFor('layout.designer', array('id' => $layout->layoutId)), + 'text' => __('Design') + ); + } + + // Preview + $layout->buttons[] = array( + 'id' => 'layout_button_preview', + 'linkType' => '_blank', + 'external' => true, + 'url' => $this->urlFor('layout.preview', ['id' => $layout->layoutId]), + 'text' => __('Preview Layout') + ); + } + } + + $this->getState()->template = 'grid'; + $this->getState()->recordsTotal = $this->layoutFactory->countLast(); + $this->getState()->setData($layouts); + } } diff --git a/lib/Controller/Login.php b/lib/Controller/Login.php index f5205e6de0..446df03597 100644 --- a/lib/Controller/Login.php +++ b/lib/Controller/Login.php @@ -102,6 +102,7 @@ public function login() // We are logged in! $user->loggedIn = 1; + $user->touch(); $this->getLog()->info('%s user logged in.', $user->userName); @@ -150,6 +151,7 @@ public function login() public function logout($redirect = true) { $this->getUser()->loggedIn = 0; + $this->getUser()->touch(); // to log out a user we need only to clear out some session vars unset($_SESSION['userid']); diff --git a/lib/Controller/Maintenance.php b/lib/Controller/Maintenance.php index 186bd4ba63..f1f88be1ca 100644 --- a/lib/Controller/Maintenance.php +++ b/lib/Controller/Maintenance.php @@ -224,11 +224,11 @@ public function tidyLibrary() SELECT media.mediaid, media.storedAs, media.type, media.isedited, SUM(CASE WHEN IFNULL(lkwidgetmedia.widgetId, 0) = 0 THEN 0 ELSE 1 END) AS UsedInLayoutCount, SUM(CASE WHEN IFNULL(lkmediadisplaygroup.id, 0) = 0 THEN 0 ELSE 1 END) AS UsedInDisplayCount, - SUM(CASE WHEN IFNULL(layout.layoutId, 0) = 0 THEN 0 ELSE 1 END) AS UsedInBackgroundImageCount, + SUM(CASE WHEN IFNULL(layout.layoutId, 0) = 0 THEN 0 ELSE 1 END) AS UsedInBackgroundImageCount '; if (count($dataSets) > 0) { - $sql .= ' SUM(CASE WHEN IFNULL(dataSetImages.mediaId, 0) = 0 THEN 0 ELSE 1 END) AS UsedInDataSetCount '; + $sql .= ' , SUM(CASE WHEN IFNULL(dataSetImages.mediaId, 0) = 0 THEN 0 ELSE 1 END) AS UsedInDataSetCount '; } $sql .= ' diff --git a/lib/Controller/MediaManager.php b/lib/Controller/MediaManager.php index a2ea28d412..c0fd747c20 100644 --- a/lib/Controller/MediaManager.php +++ b/lib/Controller/MediaManager.php @@ -122,13 +122,16 @@ public function grid() if (!$this->getUser()->checkEditable($widget)) continue; + // Create a module + $module = $this->moduleFactory->createWithWidget($widget); + // We are good to go $rows[] = [ 'layout' => $layout, 'region' => $region->name, 'playlist' => $playlist->name, - 'widget' => $widget->getOptionValue('name', $widget->type), - 'type' => $widget->type, + 'widget' => $module->getName(), + 'type' => $module->getModuleName(), 'displayOrder' => $widget->displayOrder, 'buttons' => [ [ diff --git a/lib/Controller/Module.php b/lib/Controller/Module.php index 3b63627232..fea9dc0268 100644 --- a/lib/Controller/Module.php +++ b/lib/Controller/Module.php @@ -239,6 +239,7 @@ public function settingsForm($moduleId) /** * Settings * @param int $moduleId + * @throws InvalidArgumentException */ public function settings($moduleId) { @@ -257,6 +258,10 @@ public function settings($moduleId) if (!$moduleConfigLocked) $module->getModule()->imageUri = $this->getSanitizer()->getString('imageUri'); + // Validation + if (strpbrk($module->getModule()->validExtensions, '*.{}[]|') !== false) + throw new InvalidArgumentException('Comma separated file extensions only please, without the .', 'validExtensions'); + // Install Files for this module $module->installFiles(); @@ -952,6 +957,7 @@ public function getResource($regionId, $widgetId) throw new AccessDeniedException(); // Call module GetResource + $module->setUser($this->getUser()); echo $module->getResource(); $this->setNoOutput(true); } diff --git a/lib/Controller/Schedule.php b/lib/Controller/Schedule.php index ec688e5839..f2f071acc6 100644 --- a/lib/Controller/Schedule.php +++ b/lib/Controller/Schedule.php @@ -327,7 +327,7 @@ function eventData() 'url' => ($editable) ? $url : null, 'start' => $fromDt->format('U') * 1000, 'end' => $toDt->format('U') * 1000, - 'sameDay' => ($fromDt->day == $toDt->day), + 'sameDay' => ($fromDt->day == $toDt->day && $fromDt->month == $toDt->month && $fromDt->year == $toDt->year), 'editable' => $editable, 'event' => $row, 'scheduleEvent' => $scheduleEvent @@ -640,7 +640,7 @@ function addForm() * @SWG\Parameter( * name="eventTypeId", * in="formData", - * description="The Event Type Id to use for this Event. 1=Campaign, 2=Command", + * description="The Event Type Id to use for this Event. 1=Campaign, 2=Command, 3=Overlay", * type="integer", * required=true * ), @@ -911,7 +911,7 @@ function editForm($eventId) * @SWG\Parameter( * name="eventTypeId", * in="formData", - * description="The Event Type Id to use for this Event. 1=Campaign, 2=Command", + * description="The Event Type Id to use for this Event. 1=Campaign, 2=Command, 3=Overlay", * type="integer", * required=true * ), diff --git a/lib/Controller/Stats.php b/lib/Controller/Stats.php index ff5114901f..3da03e4865 100644 --- a/lib/Controller/Stats.php +++ b/lib/Controller/Stats.php @@ -25,6 +25,9 @@ use Xibo\Factory\DisplayFactory; use Xibo\Factory\LayoutFactory; use Xibo\Factory\MediaFactory; +use Xibo\Factory\UserFactory; +use Xibo\Factory\UserGroupFactory; +use Xibo\Helper\ByteFormatter; use Xibo\Service\ConfigServiceInterface; use Xibo\Service\DateServiceInterface; use Xibo\Service\LogServiceInterface; @@ -55,6 +58,12 @@ class Stats extends Base /** @var LayoutFactory */ private $layoutFactory; + /** @var UserFactory */ + private $userFactory; + + /** @var UserGroupFactory */ + private $userGroupFactory; + /** * Set common dependencies. * @param LogServiceInterface $log @@ -68,8 +77,10 @@ class Stats extends Base * @param DisplayFactory $displayFactory * @param LayoutFactory $layoutFactory * @param MediaFactory $mediaFactory + * @param UserFactory $userFactory + * @param UserGroupFactory $userGroupFactory */ - public function __construct($log, $sanitizerService, $state, $user, $help, $date, $config, $store, $displayFactory, $layoutFactory, $mediaFactory) + public function __construct($log, $sanitizerService, $state, $user, $help, $date, $config, $store, $displayFactory, $layoutFactory, $mediaFactory, $userFactory, $userGroupFactory) { $this->setCommonDependencies($log, $sanitizerService, $state, $user, $help, $date, $config); @@ -77,12 +88,33 @@ public function __construct($log, $sanitizerService, $state, $user, $help, $date $this->displayFactory = $displayFactory; $this->layoutFactory = $layoutFactory; $this->mediaFactory = $mediaFactory; + $this->userFactory = $userFactory; + $this->userGroupFactory = $userGroupFactory; } /** * Stats page */ function displayPage() + { + $data = [ + // List of Displays this user has permission for + 'displays' => $this->displayFactory->query(), + 'defaults' => [ + 'fromDate' => $this->getDate()->getLocalDate(time() - (86400 * 35)), + 'fromDateOneDay' => $this->getDate()->getLocalDate(time() - 86400), + 'toDate' => $this->getDate()->getLocalDate() + ] + ]; + + $this->getState()->template = 'statistics-page'; + $this->getState()->setData($data); + } + + /** + * Stats page + */ + function displayProofOfPlayPage() { $data = [ // List of Displays this user has permission for @@ -98,7 +130,7 @@ function displayPage() ] ]; - $this->getState()->template = 'statistics-page'; + $this->getState()->template = 'stats-proofofplay-page'; $this->getState()->setData($data); } @@ -417,7 +449,8 @@ public function availabilityData() $rows = $this->store->select($SQL, $params); - $output = array(); + $labels = []; + $data = []; $maxDuration = 0; foreach ($rows as $row) { @@ -438,14 +471,13 @@ public function availabilityData() } foreach ($rows as $row) { - $output[] = array( - 'label' => $this->getSanitizer()->string($row['display']), - 'value' => round($this->getSanitizer()->double($row['duration']) / $divisor, 2) - ); + $labels[] = $this->getSanitizer()->string($row['display']); + $data[] = round($this->getSanitizer()->double($row['duration']) / $divisor, 2); } $this->getState()->extra = [ - 'data' => $output, + 'labels' => $labels, + 'data' => $data, 'postUnits' => $postUnits ]; } @@ -525,28 +557,27 @@ public function bandwidthData() // Decide what our units are going to be, based on the size $base = floor(log($maxSize) / log(1024)); - $output = array(); + $labels = []; + $data = []; foreach ($results as $row) { // label depends whether we are filtered by display if ($displayId != 0) { - $label = $row['type']; + $labels[] = $row['type']; } else { - $label = $row['display']; + $labels[] = $row['display']; } - $output[] = array( - 'label' => $label, - 'value' => round((double)$row['size'] / (pow(1024, $base)), 2) - ); + $data[] = round((double)$row['size'] / (pow(1024, $base)), 2); } // Set up some suffixes $suffixes = array('bytes', 'k', 'M', 'G', 'T'); $this->getState()->extra = [ - 'data' => $output, + 'labels' => $labels, + 'data' => $data, 'postUnits' => (isset($suffixes[$base]) ? $suffixes[$base] : '') ]; } @@ -636,4 +667,187 @@ public function export() $app->response()->header('Accept-Ranges', 'bytes'); $this->setNoOutput(true); } + + /** + * Stats page + */ + function displayLibraryPage() + { + $this->getState()->template = 'stats-library-page'; + $data = []; + + // Set up some suffixes + $suffixes = array('B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'); + + // Widget for the library usage pie chart + try { + if ($this->getUser()->libraryQuota != 0) { + $libraryLimit = $this->getUser()->libraryQuota * 1024; + } else { + $libraryLimit = $this->getConfig()->GetSetting('LIBRARY_SIZE_LIMIT_KB') * 1024; + } + + // Library Size in Bytes + $params = []; + $sql = 'SELECT IFNULL(SUM(FileSize), 0) AS SumSize, type FROM `media` WHERE 1 = 1 '; + $this->mediaFactory->viewPermissionSql('Xibo\Entity\Media', $sql, $params, '`media`.mediaId', '`media`.userId'); + $sql .= ' GROUP BY type '; + + $sth = $this->store->getConnection()->prepare($sql); + $sth->execute($params); + + $results = $sth->fetchAll(); + + // Do we base the units on the maximum size or the library limit + $maxSize = 0; + if ($libraryLimit > 0) { + $maxSize = $libraryLimit; + } else { + // Find the maximum sized chunk of the items in the library + foreach ($results as $library) { + $maxSize = ($library['SumSize'] > $maxSize) ? $library['SumSize'] : $maxSize; + } + } + + // Decide what our units are going to be, based on the size + $base = ($maxSize == 0) ? 0 : floor(log($maxSize) / log(1024)); + + $libraryUsage = []; + $libraryLabels = []; + $totalSize = 0; + foreach ($results as $library) { + $libraryUsage[] = round((double)$library['SumSize'] / (pow(1024, $base)), 2); + $libraryLabels[] = ucfirst($library['type']) . ' ' . $suffixes[$base]; + + $totalSize = $totalSize + $library['SumSize']; + } + + // Do we need to add the library remaining? + if ($libraryLimit > 0) { + $remaining = round(($libraryLimit - $totalSize) / (pow(1024, $base)), 2); + + $libraryUsage[] = $remaining; + $libraryLabels[] = __('Free') . ' ' . $suffixes[$base]; + } + + // What if we are empty? + if (count($results) == 0 && $libraryLimit <= 0) { + $libraryUsage[] = 0; + $libraryLabels[] = __('Empty'); + } + + $data['libraryLimitSet'] = ($libraryLimit > 0); + $data['libraryLimit'] = (round((double)$libraryLimit / (pow(1024, $base)), 2)) . ' ' . $suffixes[$base]; + $data['librarySize'] = ByteFormatter::format($totalSize, 1); + $data['librarySuffix'] = $suffixes[$base]; + $data['libraryWidgetLabels'] = json_encode($libraryLabels); + $data['libraryWidgetData'] = json_encode($libraryUsage); + + } catch (\Exception $exception) { + $this->getLog()->error('Error rendering the library stats page widget'); + } + + $data['users'] = $this->userFactory->query(); + $data['groups'] = $this->userGroupFactory->query(); + + $this->getState()->setData($data); + } + + public function libraryUsageGrid() + { + $params = []; + $select = ' + SELECT `user`.userId, + `user`.userName, + IFNULL(SUM(`media`.FileSize), 0) AS bytesUsed, + COUNT(`media`.mediaId) AS numFiles + '; + $body = ' + FROM `user` + LEFT OUTER JOIN `media` + ON `media`.userID = `user`.UserID + WHERE 1 = 1 + '; + + // Restrict on the users we have permission to see + // Normal users can only see themselves + $permissions = ''; + if ($this->getUser()->userTypeId == 3) { + $permissions .= ' AND user.userId = :currentUserId '; + $filterBy['currentUserId'] = $this->getUser()->userId; + } + // Group admins can only see users from their groups. + else if ($this->getUser()->userTypeId == 2) { + $permissions .= ' + AND user.userId IN ( + SELECT `otherUserLinks`.userId + FROM `lkusergroup` + INNER JOIN `group` + ON `group`.groupId = `lkusergroup`.groupId + AND `group`.isUserSpecific = 0 + INNER JOIN `lkusergroup` `otherUserLinks` + ON `otherUserLinks`.groupId = `group`.groupId + WHERE `lkusergroup`.userId = :currentUserId + ) + '; + $params['currentUserId'] = $this->getUser()->userId; + } + + // Filter by userId + if ($this->getSanitizer()->getInt('userId') !== null) { + $body .= ' AND user.userId = :userId '; + $params['userId'] = $this->getSanitizer()->getInt('userId'); + } + + // Filter by groupId + if ($this->getSanitizer()->getInt('groupId') !== null) { + $body .= ' AND user.userId IN (SELECT userId FROM `lkusergroup` WHERE groupId = :groupId) '; + $params['groupId'] = $this->getSanitizer()->getInt('groupId'); + } + + $body .= $permissions; + $body .= ' + GROUP BY `user`.userId, + `user`.userName + '; + + + // Sorting? + $filterBy = $this->gridRenderFilter(); + $sortOrder = $this->gridRenderSort(); + + $order = ''; + if (is_array($sortOrder)) + $order .= 'ORDER BY ' . implode(',', $sortOrder); + + $limit = ''; + // Paging + if ($filterBy !== null && $this->getSanitizer()->getInt('start', $filterBy) !== null && $this->getSanitizer()->getInt('length', $filterBy) !== null) { + $limit = ' LIMIT ' . intval($this->getSanitizer()->getInt('start', $filterBy), 0) . ', ' . $this->getSanitizer()->getInt('length', 10, $filterBy); + } + + $sql = $select . $body . $order . $limit; + $rows = []; + + foreach ($this->store->select($sql, $params) as $row) { + $entry = []; + + $entry['userId'] = $this->getSanitizer()->int($row['userId']); + $entry['userName'] = $this->getSanitizer()->string($row['userName']); + $entry['bytesUsed'] = $this->getSanitizer()->int($row['bytesUsed']); + $entry['bytesUsedFormatted'] = ByteFormatter::format($this->getSanitizer()->int($row['bytesUsed']), 2); + $entry['numFiles'] = $this->getSanitizer()->int($row['numFiles']); + + $rows[] = $entry; + } + + // Paging + if ($limit != '' && count($rows) > 0) { + $results = $this->store->select('SELECT COUNT(*) AS total FROM `user` ' . $permissions, $params); + $this->getState()->recordsTotal = intval($results[0]['total']); + } + + $this->getState()->template = 'grid'; + $this->getState()->setData($rows); + } } diff --git a/lib/Controller/StatusDashboard.php b/lib/Controller/StatusDashboard.php index 8bec678ae8..f536a60251 100644 --- a/lib/Controller/StatusDashboard.php +++ b/lib/Controller/StatusDashboard.php @@ -145,34 +145,53 @@ function displayPage() if ($xmdsLimit > 0) { // Convert to appropriate size (xmds limit is in KB) $xmdsLimit = ($xmdsLimit * 1024) / (pow(1024, $base)); - $data['xmdsLimit'] = $xmdsLimit . ' ' . $suffixes[$base]; + $data['xmdsLimit'] = round($xmdsLimit, 2) . ' ' . $suffixes[$base]; } - $output = array(); + $labels = []; + $usage = []; + $limit = []; foreach ($results as $row) { + $labels[] = $this->getDate()->getLocalDate($this->getSanitizer()->getDate('month', $row), 'F'); + $size = ((double)$row['size']) / (pow(1024, $base)); - $remaining = $xmdsLimit - $size; - $output[] = array( - 'label' => $this->getDate()->getLocalDate($this->getSanitizer()->getDate('month', $row), 'F'), - 'value' => round($size, 2), - 'limit' => round($remaining, 2) - ); + $usage[] = round($size, 2); + + $limit[] = round($xmdsLimit - $size, 2); } // What if we are empty? - if (count($output) == 0) { - $output[] = array( - 'label' => $this->getDate()->getLocalDate(null, 'F'), - 'value' => 0, - 'limit' => 0 - ); + if (count($results) == 0) { + $labels[] = $this->getDate()->getLocalDate(null, 'F'); + $usage[] = 0; + $limit[] = 0; + } + + // Organise our datasets + $dataSets = [ + [ + 'label' => __('Used'), + 'backgroundColor' => ($xmdsLimit > 0) ? 'rgb(255, 0, 0)' : 'rgb(11, 98, 164)', + 'data' => $usage + ] + ]; + + if ($xmdsLimit > 0) { + $dataSets[] = [ + 'label' => __('Available'), + 'backgroundColor' => 'rgb(0, 204, 0)', + 'data' => $limit + ]; } // Set the data $data['xmdsLimitSet'] = ($xmdsLimit > 0); $data['bandwidthSuffix'] = $suffixes[$base]; - $data['bandwidthWidget'] = json_encode($output); + $data['bandwidthWidget'] = json_encode([ + 'labels' => $labels, + 'datasets' => $dataSets + ]); // We would also like a library usage pie chart! if ($this->getUser()->libraryQuota != 0) { @@ -207,38 +226,36 @@ function displayPage() // Decide what our units are going to be, based on the size $base = ($maxSize == 0) ? 0 : floor(log($maxSize) / log(1024)); - $output = []; + $libraryUsage = []; + $libraryLabels = []; $totalSize = 0; foreach ($results as $library) { - $output[] = array( - 'value' => round((double)$library['SumSize'] / (pow(1024, $base)), 2), - 'label' => ucfirst($library['type']) - ); + $libraryUsage[] = round((double)$library['SumSize'] / (pow(1024, $base)), 2); + $libraryLabels[] = ucfirst($library['type']) . ' ' . $suffixes[$base]; + $totalSize = $totalSize + $library['SumSize']; } // Do we need to add the library remaining? if ($libraryLimit > 0) { $remaining = round(($libraryLimit - $totalSize) / (pow(1024, $base)), 2); - $output[] = array( - 'value' => $remaining, - 'label' => __('Free') - ); + + $libraryUsage[] = $remaining; + $libraryLabels[] = __('Free') . ' ' . $suffixes[$base]; } // What if we are empty? - if (count($output) == 0) { - $output[] = array( - 'label' => __('Empty'), - 'value' => 0 - ); + if (count($results) == 0 && $libraryLimit <= 0) { + $libraryUsage[] = 0; + $libraryLabels[] = __('Empty'); } - $data['libraryLimitSet'] = $libraryLimit; + $data['libraryLimitSet'] = ($libraryLimit > 0); $data['libraryLimit'] = (round((double)$libraryLimit / (pow(1024, $base)), 2)) . ' ' . $suffixes[$base]; $data['librarySize'] = ByteFormatter::format($totalSize, 1); $data['librarySuffix'] = $suffixes[$base]; - $data['libraryWidget'] = json_encode($output); + $data['libraryWidgetLabels'] = json_encode($libraryLabels); + $data['libraryWidgetData'] = json_encode($libraryUsage); // Also a display widget $data['displays'] = $displays; @@ -306,9 +323,19 @@ function displayPage() foreach ($feed->getItems() as $item) { /* @var \PicoFeed\Parser\Item $item */ + + // Try to get the description tag + if (!$desc = $item->getTag('description')) { + // use content with tags stripped + $content = strip_tags($item->getContent()); + } else { + // use description + $content = (isset($desc[0]) ? $desc[0] : strip_tags($item->getContent())); + } + $latestNews[] = array( 'title' => $item->getTitle(), - 'description' => $item->getContent(), + 'description' => $content, 'link' => $item->getUrl() ); } @@ -343,7 +370,7 @@ function displayPage() } // Do we have an embedded widget? - $data['embedded-widget'] = html_entity_decode($this->getConfig()->GetSetting('EMBEDDED_STATUS_WIDGET')); + $data['embeddedWidget'] = html_entity_decode($this->getConfig()->GetSetting('EMBEDDED_STATUS_WIDGET')); // Render the Theme and output $this->getState()->template = 'dashboard-status-page'; diff --git a/lib/Controller/User.php b/lib/Controller/User.php index 1ddcbb270d..3541e95f79 100644 --- a/lib/Controller/User.php +++ b/lib/Controller/User.php @@ -444,9 +444,11 @@ public function add() if ($this->getUser()->isSuperAdmin()) { $user->userTypeId = $this->getSanitizer()->getInt('userTypeId'); $user->isSystemNotification = $this->getSanitizer()->getCheckbox('isSystemNotification'); + $user->isDisplayNotification = $this->getSanitizer()->getCheckbox('isDisplayNotification'); } else { $user->userTypeId = 3; $user->isSystemNotification = 0; + $user->isDisplayNotification = 0; } $user->firstName = $this->getSanitizer()->getString('firstName'); @@ -508,6 +510,7 @@ public function edit($userId) if ($this->getUser()->isSuperAdmin()) { $user->userTypeId = $this->getSanitizer()->getInt('userTypeId'); $user->isSystemNotification = $this->getSanitizer()->getCheckbox('isSystemNotification'); + $user->isDisplayNotification = $this->getSanitizer()->getCheckbox('isDisplayNotification'); } $user->firstName = $this->getSanitizer()->getString('firstName'); @@ -872,10 +875,21 @@ public function permissions($entity, $objectId) if ($object->canChangeOwner()) { $object->setOwner($ownerId); - $object->save(); + $object->save(['notify' => false]); } else { throw new ConfigurationException(__('Cannot change owner on this Object')); } + + // Nasty handling for ownerId on the Layout + // ideally we'd remove that column and rely on the campaign ownerId in 1.9 onward + if ($object->permissionsClass() == 'Xibo\Entity\Campaign') { + $this->getLog()->debug('Changing owner on child Layout'); + + foreach ($this->layoutFactory->getByCampaignId($object->getId()) as $layout) { + $layout->setOwner($ownerId, true); + $layout->save(['notify' => false]); + } + } } // Cascade permissions diff --git a/lib/Controller/UserGroup.php b/lib/Controller/UserGroup.php index 3d938ded48..ed9952d1be 100644 --- a/lib/Controller/UserGroup.php +++ b/lib/Controller/UserGroup.php @@ -260,8 +260,10 @@ function add() $group->group = $this->getSanitizer()->getString('group'); $group->libraryQuota = $this->getSanitizer()->getInt('libraryQuota'); - if ($this->getUser()->userTypeId == 1) + if ($this->getUser()->userTypeId == 1) { $group->isSystemNotification = $this->getSanitizer()->getCheckbox('isSystemNotification'); + $group->isDisplayNotification = $this->getSanitizer()->getCheckbox('isDisplayNotification'); + } // Save $group->save(); @@ -294,8 +296,10 @@ function edit($groupId) $group->group = $this->getSanitizer()->getString('group'); $group->libraryQuota = $this->getSanitizer()->getInt('libraryQuota'); - if ($this->getUser()->userTypeId == 1) + if ($this->getUser()->userTypeId == 1) { $group->isSystemNotification = $this->getSanitizer()->getCheckbox('isSystemNotification'); + $group->isDisplayNotification = $this->getSanitizer()->getCheckbox('isDisplayNotification'); + } // Save $group->save(); diff --git a/lib/Entity/Campaign.php b/lib/Entity/Campaign.php index 7b627af718..3392d82271 100644 --- a/lib/Entity/Campaign.php +++ b/lib/Entity/Campaign.php @@ -299,14 +299,14 @@ public function replaceTags($tags = []) */ public function save($options = []) { - $options = array_merge([ 'validate' => true, 'notify' => true, + 'collectNow' => true, 'saveTags' => true ], $options); - $this->getLog()->debug('Saving %s', $this); + $this->getLog()->debug('Saving ' . $this); if ($options['validate']) $this->validate(); @@ -324,7 +324,7 @@ public function save($options = []) foreach ($this->tags as $tag) { /* @var Tag $tag */ - $this->getLog()->debug('Assigning tag %s', $tag->tag); + $this->getLog()->debug('Assigning tag ' . $tag->tag); $tag->assignCampaign($this->campaignId); $tag->save(); @@ -335,7 +335,7 @@ public function save($options = []) if (is_array($this->unassignTags)) { foreach ($this->unassignTags as $tag) { /* @var Tag $tag */ - $this->getLog()->debug('Unassigning tag %s', $tag->tag); + $this->getLog()->debug('Unassigning tag ' . $tag->tag); $tag->unassignCampaign($this->campaignId); $tag->save(); @@ -348,8 +348,7 @@ public function save($options = []) } // Notify anyone interested of the changes - if ($options['notify']) - $this->notify(); + $this->notify($options); } public function delete() @@ -537,11 +536,27 @@ private function unlinkLayouts() /** * Notify displays of this campaign change + * @param array $options */ - private function notify() + private function notify($options) { - $this->getLog()->debug('CampaignId ' . $this->campaignId . ' wants to notify.'); + $options = array_merge([ + 'notify' => true, + 'collectNow' => true, + ], $options); - $this->displayFactory->getDisplayNotifyService()->collectNow()->notifyByCampaignId($this->campaignId); + // Do we notify? + if ($options['notify']) { + $this->getLog()->debug('CampaignId ' . $this->campaignId . ' wants to notify.'); + + $notify = $this->displayFactory->getDisplayNotifyService(); + + // Should we collect immediately + if ($options['collectNow']) + $notify->collectNow(); + + // Notify + $notify->notifyByCampaignId($this->campaignId); + } } } diff --git a/lib/Entity/DataSet.php b/lib/Entity/DataSet.php index ac84a144ec..8ccb675153 100644 --- a/lib/Entity/DataSet.php +++ b/lib/Entity/DataSet.php @@ -424,7 +424,7 @@ public function validate() throw new InvalidArgumentException(__('Description can not be longer than 254 characters'), 'description'); try { - $existing = $this->dataSetFactory->getByName($this->dataSet); + $existing = $this->dataSetFactory->getByName($this->dataSet, $this->userId); if ($this->dataSetId == 0 || $this->dataSetId != $existing->dataSetId) throw new DuplicateEntityException(sprintf(__('There is already dataSet called %s. Please choose another name.'), $this->dataSet)); @@ -490,7 +490,15 @@ public function delete() if ($this->isLookup) throw new ConfigurationException(__('Lookup Tables cannot be deleted')); - // TODO check we aren't being used + if ($this->getStore()->exists(' + SELECT widgetId + FROM `widgetoption` + WHERE `widgetoption`.type = \'attrib\' + AND `widgetoption`.option = \'dataSetId\' + AND `widgetoption`.value = :dataSetId + ', ['dataSetId' => $this->dataSetId])) { + throw new InvalidArgumentException('Cannot delete because DataSet is in use on one or more Layouts.', 'dataSetId'); + } // Delete Permissions foreach ($this->permissions as $permission) { diff --git a/lib/Entity/DayPart.php b/lib/Entity/DayPart.php index a671d7a071..5aba284a47 100644 --- a/lib/Entity/DayPart.php +++ b/lib/Entity/DayPart.php @@ -319,7 +319,7 @@ private function handleEffectedSchedules() $schedule->save(); // Adjusting the fromdt on the new event - $newSchedule->fromDt = $now; + $newSchedule->fromDt = $this->getDate()->parse()->addDay()->format('U'); $newSchedule->save(); } else { $this->getLog()->debug('Schedule is for a single event'); diff --git a/lib/Entity/Display.php b/lib/Entity/Display.php index 4515aa2c25..fe8d023e0a 100644 --- a/lib/Entity/Display.php +++ b/lib/Entity/Display.php @@ -308,13 +308,19 @@ class Display implements \JsonSerializable */ public $timeZone; + /** + * @SWG\Property(description="Tags associated with this Display") + * @var Tag[] + */ + public $tags; + /** * Commands * @var array[Command] */ private $commands = null; - public static $saveOptionsMinimum = ['validate' => false, 'audit' => false, 'triggerDynamicDisplayGroupAssessment' => false]; + public static $saveOptionsMinimum = ['validate' => false, 'audit' => false]; /** * @var ConfigServiceInterface @@ -541,8 +547,7 @@ public function save($options = []) { $options = array_merge([ 'validate' => true, - 'audit' => true, - 'triggerDynamicDisplayGroupAssessment' => false + 'audit' => true ], $options); if ($options['validate']) @@ -557,7 +562,7 @@ public function save($options = []) $this->getLog()->audit('Display', $this->displayId, 'Display Saved', $this->getChangedProperties()); // Trigger an update of all dynamic DisplayGroups - if ($options['triggerDynamicDisplayGroupAssessment']) { + if ($this->hasPropertyChanged('display')) { foreach ($this->displayGroupFactory->getByIsDynamic(1) as $group) { /* @var DisplayGroup $group */ $group->setChildObjectDependencies($this->displayFactory, $this->layoutFactory, $this->mediaFactory, $this->scheduleFactory); @@ -579,7 +584,7 @@ public function delete() /* @var DisplayGroup $displayGroup */ $displayGroup->setChildObjectDependencies($this->displayFactory, $this->layoutFactory, $this->mediaFactory, $this->scheduleFactory); $displayGroup->unassignDisplay($this); - $displayGroup->save(['validate' => false]); + $displayGroup->save(['validate' => false, 'manageDynamicDisplayLinks' => false]); } // Delete our display specific group @@ -616,6 +621,7 @@ private function add() $displayGroup = $this->displayGroupFactory->createEmpty(); $displayGroup->displayGroup = $this->display; + $displayGroup->tags = $this->tags; $displayGroup->setDisplaySpecificDisplay($this); $displayGroup->save(); } @@ -699,12 +705,13 @@ private function edit() ]); // Maintain the Display Group - if ($this->hasPropertyChanged('display') || $this->hasPropertyChanged('description')) { + if ($this->hasPropertyChanged('display') || $this->hasPropertyChanged('description') || $this->hasPropertyChanged('tags')) { $this->getLog()->debug('Display specific DisplayGroup properties need updating'); $displayGroup = $this->displayGroupFactory->getById($this->displayGroupId); $displayGroup->displayGroup = $this->display; $displayGroup->description = $this->description; + $displayGroup->replaceTags($this->tags); $displayGroup->save(DisplayGroup::$saveOptionsMinimum); } } diff --git a/lib/Entity/DisplayGroup.php b/lib/Entity/DisplayGroup.php index 374726fa76..1b2fce2165 100644 --- a/lib/Entity/DisplayGroup.php +++ b/lib/Entity/DisplayGroup.php @@ -18,6 +18,7 @@ use Xibo\Factory\MediaFactory; use Xibo\Factory\PermissionFactory; use Xibo\Factory\ScheduleFactory; +use Xibo\Factory\TagFactory; use Xibo\Service\LogServiceInterface; use Xibo\Storage\StorageServiceInterface; @@ -87,6 +88,12 @@ class DisplayGroup implements \JsonSerializable */ public $userId = 0; + /** + * @SWG\Property(description="Tags associated with this DisplayGroup") + * @var Tag[] + */ + public $tags = []; + /** * Minimum save options * @var array @@ -105,6 +112,7 @@ class DisplayGroup implements \JsonSerializable private $media = []; private $permissions = []; private $events = []; + private $unassignTags = []; // Track original assignments private $originalDisplayGroups = []; @@ -151,19 +159,26 @@ class DisplayGroup implements \JsonSerializable */ private $scheduleFactory; + /** + * @var TagFactory + */ + private $tagFactory; + /** * Entity constructor. * @param StorageServiceInterface $store * @param LogServiceInterface $log * @param DisplayGroupFactory $displayGroupFactory * @param PermissionFactory $permissionFactory + * @param TagFactory $tagFactory */ - public function __construct($store, $log, $displayGroupFactory, $permissionFactory) + public function __construct($store, $log, $displayGroupFactory, $permissionFactory, $tagFactory) { $this->setCommonDependencies($store, $log); $this->displayGroupFactory = $displayGroupFactory; $this->permissionFactory = $permissionFactory; + $this->tagFactory = $tagFactory; } /** @@ -217,7 +232,8 @@ public function canChangeOwner() } /** - * Set Notify Required + * Set Collection Required + * If true will send a player action to collect immediately * @param bool|true $collectRequired */ public function setCollectRequired($collectRequired = true) @@ -382,11 +398,87 @@ public function unassignLayout($layout) }); } + /** + * Does the campaign have the provided tag? + * @param $searchTag + * @return bool + */ + public function hasTag($searchTag) + { + $this->load(); + + foreach ($this->tags as $tag) { + /* @var Tag $tag */ + if ($tag->tag == $searchTag) + return true; + } + + return false; + } + + /** + * Assign Tag + * @param Tag $tag + * @return $this + */ + public function assignTag($tag) + { + $this->load(); + + if (!in_array($tag, $this->tags)) + $this->tags[] = $tag; + + return $this; + } + + /** + * Unassign tag + * @param Tag $tag + * @return $this + */ + public function unassignTag($tag) + { + $this->tags = array_udiff($this->tags, [$tag], function($a, $b) { + /* @var Tag $a */ + /* @var Tag $b */ + return $a->tagId - $b->tagId; + }); + + return $this; + } + + /** + * @param array[Tag] $tags + */ + public function replaceTags($tags = []) + { + if (!is_array($this->tags) || count($this->tags) <= 0) + $this->tags = $this->tagFactory->loadByDisplayGroupId($this->displayGroupId); + + $this->unassignTags = array_udiff($this->tags, $tags, function($a, $b) { + /* @var Tag $a */ + /* @var Tag $b */ + return $a->tagId - $b->tagId; + }); + + $this->getLog()->debug('Tags to be removed: ' . json_encode($this->unassignTags)); + + // Replace the arrays + $this->tags = $tags; + + $this->getLog()->debug('Tags remaining: ' . json_encode($this->tags)); + } + /** * Load the contents for this display group + * @param array $options */ - public function load() + public function load($options = []) { + $options = array_merge([ + 'loadTags' => true + ], $options); + if ($this->loaded || $this->displayGroupId == null || $this->displayGroupId == 0) return; @@ -405,6 +497,10 @@ public function load() $this->events = $this->scheduleFactory->getByDisplayGroupId($this->displayGroupId); + // Load all tags + if ($options['loadTags']) + $this->tags = $this->tagFactory->loadByDisplayGroupId($this->displayGroupId); + // Set the originals $this->originalDisplayGroups = $this->displayGroups; @@ -449,7 +545,8 @@ public function save($options = []) 'validate' => true, 'saveGroup' => true, 'manageLinks' => true, - 'manageDisplayLinks' => true + 'manageDisplayLinks' => true, + 'manageDynamicDisplayLinks' => true, ], $options); if ($options['validate']) @@ -462,6 +559,29 @@ public function save($options = []) else if ($options['saveGroup']) $this->edit(); + // Tags + if (is_array($this->tags)) { + foreach ($this->tags as $tag) { + /* @var Tag $tag */ + + $this->getLog()->debug('Assigning tag ' . $tag->tag); + + $tag->assignDisplayGroup($this->displayGroupId); + $tag->save(); + } + } + + // Remove unwanted ones + if (is_array($this->unassignTags)) { + foreach ($this->unassignTags as $tag) { + /* @var Tag $tag */ + $this->getLog()->debug('Unassigning tag ' . $tag->tag); + + $tag->unassignDisplayGroup($this->displayGroupId); + $tag->save(); + } + } + if ($this->loaded) { if ($options['manageLinks']) { @@ -478,14 +598,14 @@ public function save($options = []) if ($options['manageDisplayLinks']) { // Handle any changes in the displays linked - $this->manageDisplayLinks(); + $this->manageDisplayLinks($options['manageDynamicDisplayLinks']); // Handle any group links $this->manageDisplayGroupLinks(); } - } else if ($this->isDynamic && $options['manageDisplayLinks']) { - $this->manageDisplayLinks(); + } else if ($this->isDynamic && $options['manageDynamicDisplayLinks']) { + $this->manageDisplayLinks(true); } // Set media incomplete if necessary @@ -586,10 +706,11 @@ private function edit() /** * Manage the links to this display, dynamic or otherwise + * @var bool $manageDynamic */ - private function manageDisplayLinks() + private function manageDisplayLinks($manageDynamic = true) { - if ($this->isDynamic) { + if ($this->isDynamic && $manageDynamic) { $this->getLog()->info('Managing Display Links for Dynamic Display Group %s', $this->displayGroup); diff --git a/lib/Entity/DisplayProfile.php b/lib/Entity/DisplayProfile.php index 57f94b97fa..6daa8e2093 100644 --- a/lib/Entity/DisplayProfile.php +++ b/lib/Entity/DisplayProfile.php @@ -589,12 +589,13 @@ private function loadFromFile() 'type' => 'string', 'fieldType' => 'dropdown', 'options' => array( - array('id' => 'Top Left', 'value' => 'Top Left'), - array('id' => 'Top Right', 'value' => 'Top Right'), - array('id' => 'Bottom Left', 'value' => 'Bottom Left'), - array('id' => 'Bottom Right', 'value' => 'Bottom Right'), + array('id' => 'Unchanged', 'value' => __('Unchanged')), + array('id' => 'Top Left', 'value' => __('Top Left')), + array('id' => 'Top Right', 'value' => __('Top Right')), + array('id' => 'Bottom Left', 'value' => __('Bottom Left')), + array('id' => 'Bottom Right', 'value' => __('Bottom Right')), ), - 'default' => 'Bottom Right', + 'default' => 'Unchanged', 'helpText' => __('The position of the cursor when the client starts up.'), 'enabled' => true, 'groupClass' => NULL @@ -617,7 +618,7 @@ private function loadFromFile() 'type' => 'int', 'fieldType' => 'text', 'default' => 10, - 'helpText' => __('If an empty layout is detected how long should it remain on screen. Must be greater then 1.'), + 'helpText' => __('If an empty layout is detected how long (in seconds) should it remain on screen? Must be greater than 1.'), 'validation' => 'number', 'enabled' => true, 'groupClass' => NULL @@ -1132,9 +1133,190 @@ private function loadFromFile() 'helpText' => __('The port number to use for the embedded web server on the Player. Only change this if there is a port conflict reported on the status screen.'), 'enabled' => true, 'groupClass' => NULL + ), + array( + 'name' => 'installWithLoadedLinkLibraries', + 'tabId' => 'advanced', + 'title' => __('Load Link Libraries for APK Update'), + 'type' => 'checkbox', + 'fieldType' => 'checkbox', + 'default' => 1, + 'helpText' => __('Should the update command include dynamic link libraries? Only change this if your updates are failing.'), + 'enabled' => true, + 'groupClass' => NULL ) ) - ) + ), + 'lg' => [ + 'synonym' => 'xiboforwebos', + 'tabs' => [ + ['id' => 'general', 'name' => __('General')], + ['id' => 'timers', 'name' => __('On/Off Time')], + ['id' => 'advanced', 'name' => __('Advanced')], + ], + 'settings' => [ + [ + 'name' => 'emailAddress', + 'tabId' => 'general', + 'title' => __('Email Address'), + 'type' => 'string', + 'fieldType' => 'text', + 'default' => '', + 'helpText' => __('The email address will be used to license this client. This is the email address you provided when you purchased the licence.'), + 'enabled' => true, + 'groupClass' => NULL + ], + [ + 'name' => 'collectInterval', + 'tabId' => 'general', + 'title' => __('Collect interval'), + 'type' => 'int', + 'fieldType' => 'dropdown', + 'options' => array( + array('id' => 60, 'value' => __('1 minute')), + array('id' => 300, 'value' => __('5 minutes')), + array('id' => 600, 'value' => __('10 minutes')), + array('id' => 1800, 'value' => __('30 minutes')), + array('id' => 3600, 'value' => __('1 hour')), + array('id' => 14400, 'value' => __('4 hours')), + array('id' => 43200, 'value' => __('12 hours')), + array('id' => 86400, 'value' => __('24 hours')) + ), + 'default' => 300, + 'helpText' => __('How often should the Player check for new content.'), + 'validation' => 'numeric', + 'enabled' => true, + 'groupClass' => NULL + ], + [ + 'name' => 'xmrNetworkAddress', + 'tabId' => 'general', + 'title' => __('XMR Public Address'), + 'type' => 'string', + 'fieldType' => 'text', + 'default' => '', + 'helpText' => __('Please enter the public address for XMR.'), + 'enabled' => true, + 'groupClass' => NULL + ], + [ + 'name' => 'statsEnabled', + 'tabId' => 'general', + 'title' => __('Enable stats reporting?'), + 'type' => 'checkbox', + 'fieldType' => 'checkbox', + 'default' => $this->configService->GetSetting('DISPLAY_PROFILE_STATS_DEFAULT', 0), + 'helpText' => __('Should the application send proof of play stats to the CMS.'), + 'enabled' => true, + 'groupClass' => NULL + ], + [ + 'name' => 'orientation', + 'tabId' => 'general', + 'title' => __('Orientation'), + 'type' => 'int', + 'fieldType' => 'dropdown', + 'options' => array( + array('id' => 0, 'value' => __('Landscape')), + array('id' => 1, 'value' => __('Portrait')), + array('id' => 8, 'value' => __('Reverse Landscape')), + array('id' => 9, 'value' => __('Reverse Portrait')) + ), + 'default' => 0, + 'helpText' => __('Set the orientation of the device.'), + 'enabled' => true, + 'groupClass' => NULL + ], + [ + 'name' => 'downloadStartWindow', + 'tabId' => 'general', + 'title' => __('Download Window Start Time'), + 'type' => 'string', + 'fieldType' => 'timePicker', + 'default' => '00:00', + 'helpText' => __('The start of the time window to connect to the CMS and download updates.'), + 'enabled' => true, + 'groupClass' => NULL + ], + [ + 'name' => 'downloadEndWindow', + 'tabId' => 'general', + 'title' => __('Download Window End Time'), + 'type' => 'string', + 'fieldType' => 'timePicker', + 'default' => '00:00', + 'helpText' => __('The end of the time window to connect to the CMS and download updates.'), + 'enabled' => true, + 'groupClass' => NULL + ], + [ + 'name' => 'actionBarMode', + 'tabId' => 'advanced', + 'title' => __('Action Bar Mode'), + 'type' => 'int', + 'fieldType' => 'dropdown', + 'options' => array( + array('id' => 0, 'value' => 'Hide'), + array('id' => 1, 'value' => 'Timed') + ), + 'default' => 1, + 'helpText' => __('How should the action bar behave?'), + 'enabled' => true, + 'groupClass' => NULL + ], + [ + 'name' => 'actionBarDisplayDuration', + 'tabId' => 'advanced', + 'title' => __('Action Bar Display Duration'), + 'type' => 'int', + 'fieldType' => 'text', + 'default' => 30, + 'helpText' => __('How long should the Action Bar be shown for, in seconds?'), + 'validation' => 'numeric', + 'enabled' => true, + 'groupClass' => NULL + ], + [ + 'name' => 'screenShotSize', + 'tabId' => 'advanced', + 'title' => __('Screen Shot Size'), + 'type' => 'int', + 'fieldType' => 'dropdown', + 'options' => [ + ['id' => 1, 'value' => 'Thumbnail'], + ['id' => 2, 'value' => 'HD'], + ['id' => 3, 'value' => 'FHD'], + ], + 'default' => 1, + 'helpText' => __('The size of the screenshot to return when requested.'), + 'enabled' => true, + 'groupClass' => NULL + ], + [ + 'name' => 'mediaInventoryTimer', + 'tabId' => 'advanced', + 'title' => __('Send progress while downloading'), + 'type' => 'int', + 'fieldType' => 'text', + 'default' => 0, + 'helpText' => __('How often, in minutes, should the Display send its download progress while it is downloading new content?'), + 'validation' => 'numeric', + 'enabled' => true, + 'groupClass' => NULL + ], + [ + 'name' => 'timers', + 'tabId' => 'timers', + 'title' => __('On/Off Timers'), + 'type' => 'string', + 'fieldType' => 'text', + 'default' => '{}', + 'helpText' => __('A JSON object indicating the on/off timers to set'), + 'enabled' => true, + 'groupClass' => NULL + ] + ] + ] ); } } \ No newline at end of file diff --git a/lib/Entity/Layout.php b/lib/Entity/Layout.php index dfb9ab079b..7f0271b90c 100644 --- a/lib/Entity/Layout.php +++ b/lib/Entity/Layout.php @@ -904,6 +904,12 @@ public function toXlf() $widget->calculatedDuration = 0; } + // Does our widget have a durationIsPerItem and a Number of Items? + $numItems = $widget->getOptionValue('numItems', 0); + if ($widget->getOptionValue('durationIsPerItem', 0) == 1 && $numItems > 1) { + $widget->calculatedDuration = (($widget->useDuration == 1) ? $widget->duration : $module->getModule()->defaultDuration) * $numItems; + } + // Region duration $region->duration = $region->duration + $widget->calculatedDuration; @@ -979,6 +985,7 @@ public function toXlf() $audioNode = $document->createElement('uri', $audioMedia->storedAs); $audioNode->setAttribute('volume', $audio->volume); $audioNode->setAttribute('loop', $audio->loop); + $audioNode->setAttribute('mediaId', $audio->mediaId); $audioNodes->appendChild($audioNode); } @@ -1178,14 +1185,15 @@ public function toZip($dataSetFactory, $fileName, $options = []) } /** - * Save the XLF to disk + * Save the XLF to disk if necessary * @param array $options * @return string the path */ public function xlfToDisk($options = []) { $options = array_merge([ - 'notify' => true + 'notify' => true, + 'collectNow' => true ], $options); $path = $this->getCachePath(); @@ -1219,7 +1227,8 @@ public function xlfToDisk($options = []) 'setBuildRequired' => false, 'audit' => false, 'validate' => false, - 'notify' => $options['notify'] + 'notify' => $options['notify'], + 'collectNow' => $options['collectNow'] ]); } @@ -1287,7 +1296,8 @@ private function add() private function update($options = []) { $options = array_merge([ - 'notify' => true + 'notify' => true, + 'collectNow' => true ], $options); $this->getLog()->debug('Editing Layout ' . $this->layout . '. Id = ' . $this->layoutId); @@ -1335,6 +1345,6 @@ private function update($options = []) $campaign = $this->campaignFactory->getById($this->campaignId); $campaign->campaign = $this->layout; $campaign->ownerId = $this->ownerId; - $campaign->save(['validate' => false, 'notify' => $options['notify']]); + $campaign->save(['validate' => false, 'notify' => $options['notify'], 'collectNow' => $options['collectNow']]); } } \ No newline at end of file diff --git a/lib/Entity/Media.php b/lib/Entity/Media.php index c7b3537785..fb780cfdd7 100644 --- a/lib/Entity/Media.php +++ b/lib/Entity/Media.php @@ -173,6 +173,22 @@ class Media implements \JsonSerializable */ public $apiRef; + /** + * @var string + * @SWG\Property( + * description="The datetime the Media was created" + * ) + */ + public $createdDt; + + /** + * @var string + * @SWG\Property( + * description="The datetime the Media was last modified" + * ) + */ + public $modifiedDt; + // Private private $unassignTags = []; @@ -285,7 +301,7 @@ public function __clone() $this->permissions = []; // We need to do something with the name - $this->name = sprintf(__('Copy of %s'), $this->name); + $this->name = sprintf(__('Copy of %s on %s'), $this->name, date('Y-m-d H:i:s')); // Set so that when we add, we copy the existing file in the library $this->fileName = $this->storedAs; @@ -670,8 +686,8 @@ public function delete($options = []) private function add() { $this->mediaId = $this->getStore()->insert(' - INSERT INTO `media` (`name`, `type`, duration, originalFilename, userID, retired, moduleSystemFile, released, apiRef, valid) - VALUES (:name, :type, :duration, :originalFileName, :userId, :retired, :moduleSystemFile, :released, :apiRef, :valid) + INSERT INTO `media` (`name`, `type`, duration, originalFilename, userID, retired, moduleSystemFile, released, apiRef, valid, `createdDt`) + VALUES (:name, :type, :duration, :originalFileName, :userId, :retired, :moduleSystemFile, :released, :apiRef, :valid, :createdDt) ', [ 'name' => $this->name, 'type' => $this->mediaType, @@ -682,7 +698,8 @@ private function add() 'moduleSystemFile' => (($this->moduleSystemFile) ? 1 : 0), 'released' => $this->released, 'apiRef' => $this->apiRef, - 'valid' => 0 + 'valid' => 0, + 'createdDt' => date('Y-m-d H:i:s') ]); } @@ -702,7 +719,8 @@ private function edit() isEdited = :isEdited, userId = :userId, released = :released, - apiRef = :apiRef + apiRef = :apiRef, + modifiedDt = :modifiedDt WHERE mediaId = :mediaId ', [ 'name' => $this->name, @@ -714,6 +732,7 @@ private function edit() 'userId' => $this->ownerId, 'released' => $this->released, 'apiRef' => $this->apiRef, + 'modifiedDt' => date('Y-m-d H:i:s'), 'mediaId' => $this->mediaId ]); } @@ -843,7 +862,7 @@ private function moveFile($from, $to) $return = copy($from, $to); if (!@unlink($from)) - $this->getLog()->error('Cannot delete file: ' . $to); + $this->getLog()->error('Cannot delete file: ' . $from . ' after copying to ' . $to); return $return; } diff --git a/lib/Entity/Region.php b/lib/Entity/Region.php index 0a535f83db..bc664eb950 100644 --- a/lib/Entity/Region.php +++ b/lib/Entity/Region.php @@ -384,11 +384,19 @@ public function save($options = []) if ($options['validate']) $this->validate(); - if ($this->regionId == null || $this->regionId == 0) + if ($this->regionId == null || $this->regionId == 0) { $this->add(); - else if ($this->hash != $this->hash()) + + if ($options['audit']) + $this->audit($this->regionId, 'Added', ['regionId' => $this->regionId, 'details' => (string)$this]); + } + else if ($this->hash != $this->hash()) { $this->update(); + if ($options['audit']) + $this->audit($this->regionId, 'Saved'); + } + if ($options['saveRegionOptions']) { // Save all Options foreach ($this->regionOptions as $regionOption) { @@ -403,9 +411,6 @@ public function save($options = []) // Manage the assignments to regions $this->manageAssignments(); } - - if ($options['audit']) - $this->audit($this->regionId, 'Saved'); } /** diff --git a/lib/Entity/Schedule.php b/lib/Entity/Schedule.php index 077886a244..d0eb0cf5df 100644 --- a/lib/Entity/Schedule.php +++ b/lib/Entity/Schedule.php @@ -333,8 +333,8 @@ private function inScheduleLookAhead() // If we are a recurring schedule and our recurring date is out after the required files lookahead $this->getLog()->debug('Checking look ahead based on recurrence'); return ($this->fromDt <= $currentDate->format('U') && ($this->recurrenceRange == 0 || $this->recurrenceRange > $rfLookAhead->format('U'))); - } else if ($this->dayPartId != self::$DAY_PART_CUSTOM) { - // Day parting event (non recurring) + } else if ($this->dayPartId != self::$DAY_PART_CUSTOM || $this->eventTypeId == self::$COMMAND_EVENT) { + // Day parting event (non recurring) or command event // only test the from date. $this->getLog()->debug('Checking look ahead based from date ' . $currentDate->toRssString()); return ($this->fromDt >= $currentDate->format('U') && $this->fromDt <= $rfLookAhead->format('U')); @@ -428,6 +428,18 @@ public function validate() // Make sure we have a sensible recurrence setting if ($this->dayPartId !== self::$DAY_PART_CUSTOM && ($this->recurrenceType == 'Minute' || $this->recurrenceType == 'Hour')) throw new InvalidArgumentException(__('Repeats selection is invalid for Always or Daypart events'), 'recurrencyType'); + + // Check display order is positive + if ($this->displayOrder < 0) + throw new InvalidArgumentException(__('Display Order must be 0 or a positive number'), 'displayOrder'); + + // Check priority is positive + if ($this->isPriority < 0) + throw new InvalidArgumentException(__('Priority must be 0 or a positive number'), 'isPriority'); + + // Check recurrenceDetail every is positive + if ($this->recurrenceDetail < 0) + throw new InvalidArgumentException(__('Repeat every must be a positive number'), 'recurrenceDetail'); } /** @@ -953,11 +965,14 @@ private function calculateDayPartTimes($start, $end) } if (!$found) { - if ($start > $end) - $end->addDay(); - + // Set the time section of our dates based on the daypart date $start->setTimeFromTimeString($dayPart->startTime); $end->setTimeFromTimeString($dayPart->endTime); + + if ($start > $end) { + $this->getLog()->debug('Start is ahead of end - adding a day to the end date'); + $end->addDay(); + } } } } diff --git a/lib/Entity/Tag.php b/lib/Entity/Tag.php index e4c0de2bfd..03ae45289d 100644 --- a/lib/Entity/Tag.php +++ b/lib/Entity/Tag.php @@ -65,9 +65,16 @@ class Tag implements \JsonSerializable */ public $mediaIds = []; + /** + * @SWG\Property(description="An array of displayGroupIds with this Tag") + * @var int[] + */ + public $displayGroupIds = []; + private $originalLayoutIds = []; private $originalMediaIds = []; private $originalCampaignIds = []; + private $originalDisplayGroupIds = []; /** * Entity constructor. @@ -153,6 +160,29 @@ public function unassignCampaign($campaignId) $this->campaignIds = array_diff($this->campaignIds, [$campaignId]); } + /** + * Assign DisplayGroup + * @param int $displayGroupId + */ + public function assignDisplayGroup($displayGroupId) + { + $this->load(); + + if (!in_array($displayGroupId, $this->displayGroupIds)) + $this->displayGroupIds[] = $displayGroupId; + } + + /** + * Unassign DisplayGroup + * @param int $displayGroupId + */ + public function unassignDisplayGroup($displayGroupId) + { + $this->load(); + + $this->displayGroupIds = array_diff($this->displayGroupIds, [$displayGroupId]); + } + /** * Load */ @@ -178,11 +208,17 @@ public function load() $this->mediaIds[] = $row['mediaId']; } + $this->displayGroupIds = []; + foreach ($this->getStore()->select('SELECT displayGroupId FROM `lktagdisplaygroup` WHERE tagId = :tagId', ['tagId' => $this->tagId]) as $row) { + $this->displayGroupIds[] = $row['displayGroupId']; + } + // Set the originals $this->originalLayoutIds = $this->layoutIds; $this->originalCampaignIds = $this->campaignIds; $this->originalMediaIds = $this->mediaIds; - + $this->originalDisplayGroupIds = $this->displayGroupIds; + $this->loaded = true; } @@ -199,6 +235,7 @@ public function save() $this->linkLayouts(); $this->linkCampaigns(); $this->linkMedia(); + $this->linkDisplayGroups(); $this->removeAssignments(); $this->getLog()->debug('Saving Tag: %s, %d', $this->tag, $this->tagId); @@ -212,6 +249,7 @@ public function removeAssignments() $this->unlinkLayouts(); $this->unlinkCampaigns(); $this->unlinkMedia(); + $this->unlinkDisplayGroups(); } /** @@ -377,6 +415,63 @@ private function unlinkMedia() + $this->getStore()->update($sql, $params); + } + + + /** + * Link all assigned displayGroups + */ + private function linkDisplayGroups() + { + // Didn't exist before 134 + if (DBVERSION < 134) + return; + + $displayGroupsToLink = array_diff($this->displayGroupIds, $this->originalDisplayGroupIds); + + $this->getLog()->debug('Linking ' . count($displayGroupsToLink) . ' displayGroups to Tag ' . $this->tag); + + // DisplayGroups that are in $this->displayGroupIds but not in $this->originalDisplayGroupIds + foreach ($displayGroupsToLink as $displayGroupId) { + $this->getStore()->update('INSERT INTO `lktagdisplaygroup` (tagId, displayGroupId) VALUES (:tagId, :displayGroupId) ON DUPLICATE KEY UPDATE displayGroupId = displayGroupId', array( + 'tagId' => $this->tagId, + 'displayGroupId' => $displayGroupId + )); + } + } + + /** + * Unlink all assigned displayGroups + */ + private function unlinkDisplayGroups() + { + // Didn't exist before 134 + if (DBVERSION < 134) + return; + + // DisplayGroups that are in the $this->originalDisplayGroupIds but not in the current $this->displayGroupIds + $displayGroupsToUnlink = array_diff($this->originalDisplayGroupIds, $this->displayGroupIds); + + $this->getLog()->debug('Unlinking ' . count($displayGroupsToUnlink) . ' displayGroups from Tag ' . $this->tag); + + if (count($displayGroupsToUnlink) <= 0) + return; + + // Unlink any displayGroups that are NOT in the collection + $params = ['tagId' => $this->tagId]; + + $sql = 'DELETE FROM `lktagdisplaygroup` WHERE tagId = :tagId AND displayGroupId IN (0'; + + $i = 0; + foreach ($displayGroupsToUnlink as $displayGroupId) { + $i++; + $sql .= ',:displayGroupId' . $i; + $params['displayGroupId' . $i] = $displayGroupId; + } + + $sql .= ')'; + $this->getStore()->update($sql, $params); } } \ No newline at end of file diff --git a/lib/Entity/User.php b/lib/Entity/User.php index c9e02ceeed..6bc7bde1d0 100644 --- a/lib/Entity/User.php +++ b/lib/Entity/User.php @@ -225,6 +225,12 @@ class User implements \JsonSerializable */ public $isSystemNotification = 0; + /** + * @SWG\Property(description="Does this Group receive system notifications.") + * @var int + */ + public $isDisplayNotification = 0; + /** * Cached Permissions * @var array[Permission] @@ -653,7 +659,7 @@ public function reassignAllTo($user) */ public function validate() { - if (!v::alnum('_.')->length(1, 50)->validate($this->userName) && !v::email()->validate($this->userName)) + if (!v::alnum('_.-')->length(1, 50)->validate($this->userName) && !v::email()->validate($this->userName)) throw new InvalidArgumentException(__('User name must be between 1 and 50 characters.'), 'userName'); if (!v::string()->notEmpty()->validate($this->password)) @@ -806,6 +812,7 @@ private function add() $group = $this->userGroupFactory->create($this->userName, $this->libraryQuota); $group->setOwner($this); $group->isSystemNotification = $this->isSystemNotification; + $group->isDisplayNotification = $this->isDisplayNotification; $group->save(); } @@ -822,7 +829,6 @@ private function update() Retired = :retired, userTypeId = :userTypeId, loggedIn = :loggedIn, - lastAccessed = :lastAccessed, newUserWizard = :newUserWizard, CSPRNG = :CSPRNG, `UserPassword` = :password, @@ -842,7 +848,6 @@ private function update() 'email' => $this->email, 'homePageId' => $this->homePageId, 'retired' => $this->retired, - 'lastAccessed' => $this->lastAccessed, 'loggedIn' => $this->loggedIn, 'newUserWizard' => $this->newUserWizard, 'CSPRNG' => $this->CSPRNG, @@ -866,6 +871,7 @@ private function update() $group->group = $this->userName; $group->libraryQuota = $this->libraryQuota; $group->isSystemNotification = $this->isSystemNotification; + $group->isDisplayNotification = $this->isDisplayNotification; $group->save(['linkUsers' => false]); } @@ -895,9 +901,9 @@ private function updatePassword() public function touch() { // This needs to happen on a separate connection - $this->getStore()->update('UPDATE `user` SET lastAccessed = :time, loggedIn = 1, newUserWizard = :newUserWizard WHERE userId = :userId', [ + $this->getStore()->update('UPDATE `user` SET lastAccessed = :time, loggedIn = :loggedIn WHERE userId = :userId', [ 'userId' => $this->userId, - 'newUserWizard' => $this->newUserWizard, + 'loggedIn' => $this->loggedIn, 'time' => date("Y-m-d H:i:s") ]); } diff --git a/lib/Entity/UserGroup.php b/lib/Entity/UserGroup.php index ebc1e3af87..88c71df794 100644 --- a/lib/Entity/UserGroup.php +++ b/lib/Entity/UserGroup.php @@ -64,6 +64,12 @@ class UserGroup */ public $isSystemNotification = 0; + /** + * @SWG\Property(description="Does this Group receive display notifications.") + * @var int + */ + public $isDisplayNotification = 0; + // Users private $users = []; @@ -286,12 +292,13 @@ private function removeAssignments() */ private function add() { - $this->groupId = $this->getStore()->insert('INSERT INTO `group` (`group`, IsUserSpecific, libraryQuota, `isSystemNotification`) - VALUES (:group, :isUserSpecific, :libraryQuota, :isSystemNotification)', [ + $this->groupId = $this->getStore()->insert('INSERT INTO `group` (`group`, IsUserSpecific, libraryQuota, `isSystemNotification`, `isDisplayNotification`) + VALUES (:group, :isUserSpecific, :libraryQuota, :isSystemNotification, :isDisplayNotification)', [ 'group' => $this->group, 'isUserSpecific' => $this->isUserSpecific, 'libraryQuota' => $this->libraryQuota, - 'isSystemNotification' => $this->isSystemNotification + 'isSystemNotification' => $this->isSystemNotification, + 'isDisplayNotification' => $this->isDisplayNotification ]); } @@ -300,11 +307,19 @@ private function add() */ private function edit() { - $this->getStore()->update('UPDATE `group` SET `group` = :group, libraryQuota = :libraryQuota, `isSystemNotification` = :isSystemNotification WHERE groupId = :groupId', [ + $this->getStore()->update(' + UPDATE `group` SET + `group` = :group, + libraryQuota = :libraryQuota, + `isSystemNotification` = :isSystemNotification, + `isDisplayNotification` = :isDisplayNotification + WHERE groupId = :groupId + ', [ 'groupId' => $this->groupId, 'group' => $this->group, 'libraryQuota' => $this->libraryQuota, - 'isSystemNotification' => $this->isSystemNotification + 'isSystemNotification' => $this->isSystemNotification, + 'isDisplayNotification' => $this->isDisplayNotification ]); } diff --git a/lib/Factory/ApplicationFactory.php b/lib/Factory/ApplicationFactory.php index 561d6da49f..d93b76720e 100644 --- a/lib/Factory/ApplicationFactory.php +++ b/lib/Factory/ApplicationFactory.php @@ -120,7 +120,7 @@ public function getByUserId($userId) return $this->query(null, ['userId' => $userId]); } - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $entries = array(); $params = array(); diff --git a/lib/Factory/ApplicationRedirectUriFactory.php b/lib/Factory/ApplicationRedirectUriFactory.php index 06cce0d8a6..2ce758db5b 100644 --- a/lib/Factory/ApplicationRedirectUriFactory.php +++ b/lib/Factory/ApplicationRedirectUriFactory.php @@ -71,10 +71,10 @@ public function getByClientId($clientId) /** * Query * @param null $sortOrder - * @param null $filterBy + * @param array $filterBy * @return array */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $entries = array(); $params = array(); diff --git a/lib/Factory/ApplicationScopeFactory.php b/lib/Factory/ApplicationScopeFactory.php index e7c3dcd5fd..14ec933ebe 100644 --- a/lib/Factory/ApplicationScopeFactory.php +++ b/lib/Factory/ApplicationScopeFactory.php @@ -69,10 +69,10 @@ public function getByClientId($clientId) /** * Query * @param null $sortOrder - * @param null $filterBy + * @param array $filterBy * @return array */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $entries = array(); $params = array(); diff --git a/lib/Factory/AuditLogFactory.php b/lib/Factory/AuditLogFactory.php index 5655e2e69a..7ba04f17c8 100644 --- a/lib/Factory/AuditLogFactory.php +++ b/lib/Factory/AuditLogFactory.php @@ -57,7 +57,7 @@ public function create() * @param array $filterBy * @return array */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $this->getLog()->debug('AuditLog Factory with filter: %s', var_export($filterBy, true)); diff --git a/lib/Factory/CommandFactory.php b/lib/Factory/CommandFactory.php index 22865175ac..ec14b9cd81 100644 --- a/lib/Factory/CommandFactory.php +++ b/lib/Factory/CommandFactory.php @@ -80,7 +80,7 @@ public function getByDisplayProfileId($displayProfileId) * @param array $filterBy * @return array */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $entries = array(); diff --git a/lib/Factory/DataSetColumnFactory.php b/lib/Factory/DataSetColumnFactory.php index 3e5cf98816..92db86d252 100644 --- a/lib/Factory/DataSetColumnFactory.php +++ b/lib/Factory/DataSetColumnFactory.php @@ -83,7 +83,7 @@ public function getByDataSetId($dataSetId) return $this->query(null, ['dataSetId' => $dataSetId]); } - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $entries = []; $params = []; diff --git a/lib/Factory/DataSetColumnTypeFactory.php b/lib/Factory/DataSetColumnTypeFactory.php index 21317c81ed..f1e40c13f6 100644 --- a/lib/Factory/DataSetColumnTypeFactory.php +++ b/lib/Factory/DataSetColumnTypeFactory.php @@ -58,10 +58,10 @@ public function getById($id) /** * @param null $sortOrder - * @param null $filterBy + * @param array $filterBy * @return array[DataSetColumnType] */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $entries = []; $params = []; diff --git a/lib/Factory/DataSetFactory.php b/lib/Factory/DataSetFactory.php index b4beb2fb78..c0493da55c 100644 --- a/lib/Factory/DataSetFactory.php +++ b/lib/Factory/DataSetFactory.php @@ -116,12 +116,13 @@ public function getByCode($code) /** * Get DataSets by Name * @param $dataSet + * @param int|null $userId the userId * @return DataSet * @throws NotFoundException */ - public function getByName($dataSet) + public function getByName($dataSet, $userId = null) { - $dataSets = $this->query(null, ['disableUserCheck' => 1, 'dataSet' => $dataSet]); + $dataSets = $this->query(null, ['dataSet' => $dataSet, 'userId' => $userId]); if (count($dataSets) <= 0) throw new NotFoundException(); @@ -135,7 +136,7 @@ public function getByName($dataSet) * @return array[DataSet] * @throws NotFoundException */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $entries = array(); $params = array(); @@ -187,9 +188,34 @@ public function query($sortOrder = null, $filterBy = null) $params['dataSetId'] = $this->getSanitizer()->getInt('dataSetId', $filterBy); } + if ($this->getSanitizer()->getInt('userId', $filterBy) !== null) { + $body .= ' AND dataset.userId = :userId '; + $params['userId'] = $this->getSanitizer()->getInt('userId', $filterBy); + } + if ($this->getSanitizer()->getString('dataSet', $filterBy) != null) { - $body .= ' AND dataset.dataSet = :dataSet '; - $params['dataSet'] = $this->getSanitizer()->getString('dataSet', $filterBy); + // convert into a space delimited array + $names = explode(' ', $this->getSanitizer()->getString('dataSet', $filterBy)); + + $i = 0; + foreach($names as $searchName) + { + $i++; + + // Ignore if the word is empty + if($searchName == '') + continue; + + // Not like, or like? + if (substr($searchName, 0, 1) == '-') { + $body.= " AND `dataset`.dataSet NOT LIKE :search$i "; + $params['search' . $i] = '%' . ltrim($searchName) . '%'; + } + else { + $body.= " AND `dataset`.dataSet LIKE :search$i "; + $params['search' . $i] = '%' . $searchName . '%'; + } + } } if ($this->getSanitizer()->getString('code', $filterBy) != null) { diff --git a/lib/Factory/DataTypeFactory.php b/lib/Factory/DataTypeFactory.php index 326faacf08..352a8c748c 100644 --- a/lib/Factory/DataTypeFactory.php +++ b/lib/Factory/DataTypeFactory.php @@ -58,10 +58,10 @@ public function getById($id) /** * @param null $sortOrder - * @param null $filterBy + * @param array $filterBy * @return array[DataType] */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $entries = []; diff --git a/lib/Factory/DayPartFactory.php b/lib/Factory/DayPartFactory.php index 95ddbf7392..ce87c5e47f 100644 --- a/lib/Factory/DayPartFactory.php +++ b/lib/Factory/DayPartFactory.php @@ -103,7 +103,7 @@ public function allWithSystem() * @param array $filterBy * @return array[Schedule] */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $entries = array(); @@ -118,7 +118,7 @@ public function query($sortOrder = null, $filterBy = null) $body .= ' WHERE 1 = 1 '; // View Permissions - $this->viewPermissionSql('Xibo\Entity\DayPart', $body, $params, '`daypart`.dayPartId'); + $this->viewPermissionSql('Xibo\Entity\DayPart', $body, $params, '`daypart`.dayPartId', '`daypart`.userId', $filterBy); if ($this->getSanitizer()->getInt('dayPartId', $filterBy) !== null) { $body .= ' AND `daypart`.dayPartId = :dayPartId '; diff --git a/lib/Factory/DisplayFactory.php b/lib/Factory/DisplayFactory.php index 78f11bf4dc..c55824c957 100644 --- a/lib/Factory/DisplayFactory.php +++ b/lib/Factory/DisplayFactory.php @@ -98,12 +98,13 @@ public function createEmpty() /** * @param int $displayId + * @param bool|false $showTags * @return Display * @throws NotFoundException */ - public function getById($displayId) + public function getById($displayId, $showTags = false) { - $displays = $this->query(null, ['disableUserCheck' => 1, 'displayId' => $displayId]); + $displays = $this->query(null, ['disableUserCheck' => 1, 'displayId' => $displayId, 'showTags' => $showTags]); if (count($displays) <= 0) throw new NotFoundException(); @@ -141,7 +142,7 @@ public function getByDisplayGroupId($displayGroupId) * @param array $filterBy * @return Display[] */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { if ($sortOrder === null) $sortOrder = ['display']; @@ -198,6 +199,19 @@ public function query($sortOrder = null, $filterBy = null) `display`.timeZone '; + if ($this->getSanitizer()->getCheckbox('showTags', $filterBy) === 1 && DBVERSION >= 134) { + $select .= ', + ( + SELECT GROUP_CONCAT(DISTINCT tag) + FROM tag + INNER JOIN lktagdisplaygroup + ON lktagdisplaygroup.tagId = tag.tagId + WHERE lktagdisplaygroup.displayGroupId = displaygroup.displayGroupID + GROUP BY lktagdisplaygroup.displayGroupId + ) AS tags + '; + } + $body = ' FROM `display` INNER JOIN `lkdisplaydg` @@ -345,6 +359,47 @@ public function query($sortOrder = null, $filterBy = null) $params['mediaId'] = $this->getSanitizer()->getInt('mediaId', $filterBy); } + // Tags + if ($this->getSanitizer()->getString('tags', $filterBy) != '') { + + $tagFilter = $this->getSanitizer()->getString('tags', $filterBy); + + if (trim($tagFilter) === '--no-tag') { + $body .= ' AND `displaygroup`.displaygroupId NOT IN ( + SELECT `lktagdisplaygroup`.displaygroupId + FROM tag + INNER JOIN `lktagdisplaygroup` + ON `lktagdisplaygroup`.tagId = tag.tagId + ) + '; + } else { + $operator = $this->getSanitizer()->getCheckbox('exactTags') == 1 ? '=' : 'LIKE'; + + $body .= " AND `displaygroup`.displaygroupId IN ( + SELECT `lktagdisplaygroup`.displaygroupId + FROM tag + INNER JOIN `lktagdisplaygroup` + ON `lktagdisplaygroup`.tagId = tag.tagId + "; + $i = 0; + foreach (explode(',', $tagFilter) as $tag) { + $i++; + + if ($i == 1) + $body .= ' WHERE `tag` ' . $operator . ' :tags' . $i; + else + $body .= ' OR `tag` ' . $operator . ' :tags' . $i; + + if ($operator === '=') + $params['tags' . $i] = $tag; + else + $params['tags' . $i] = '%' . $tag . '%'; + } + + $body .= " ) "; + } + } + // Sorting? $order = ''; if (is_array($sortOrder)) diff --git a/lib/Factory/DisplayGroupFactory.php b/lib/Factory/DisplayGroupFactory.php index 152609dc7f..6ac076d9f5 100644 --- a/lib/Factory/DisplayGroupFactory.php +++ b/lib/Factory/DisplayGroupFactory.php @@ -27,6 +27,11 @@ class DisplayGroupFactory extends BaseFactory */ private $permissionFactory; + /** + * @var TagFactory + */ + private $tagFactory; + /** * Construct a factory * @param StorageServiceInterface $store @@ -35,13 +40,15 @@ class DisplayGroupFactory extends BaseFactory * @param User $user * @param UserFactory $userFactory * @param PermissionFactory $permissionFactory + * @param TagFactory $tagFactory */ - public function __construct($store, $log, $sanitizerService, $user, $userFactory, $permissionFactory) + public function __construct($store, $log, $sanitizerService, $user, $userFactory, $permissionFactory, $tagFactory) { $this->setCommonDependencies($store, $log, $sanitizerService); $this->setAclDependencies($user, $userFactory); $this->permissionFactory = $permissionFactory; + $this->tagFactory = $tagFactory; } /** @@ -54,7 +61,8 @@ public function createEmpty() $this->getStore(), $this->getLog(), $this, - $this->permissionFactory + $this->permissionFactory, + $this->tagFactory ); } @@ -171,7 +179,7 @@ public function getByNotificationId($notificationId) * @param array $filterBy * @return array[DisplayGroup] */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { if ($sortOrder == null) $sortOrder = ['displayGroup']; @@ -186,7 +194,15 @@ public function query($sortOrder = null, $filterBy = null) `displaygroup`.description, `displaygroup`.isDynamic, `displaygroup`.dynamicCriteria, - `displaygroup`.userId + `displaygroup`.userId, + ( + SELECT GROUP_CONCAT(DISTINCT tag) + FROM tag + INNER JOIN lktagdisplaygroup + ON lktagdisplaygroup.tagId = tag.tagId + WHERE lktagdisplaygroup.displayGroupId = displaygroup.displayGroupID + GROUP BY lktagdisplaygroup.displayGroupId + ) AS tags '; $body = ' @@ -284,6 +300,47 @@ public function query($sortOrder = null, $filterBy = null) } } + // Tags + if ($this->getSanitizer()->getString('tags', $filterBy) != '') { + + $tagFilter = $this->getSanitizer()->getString('tags', $filterBy); + + if (trim($tagFilter) === '--no-tag') { + $body .= ' AND `displaygroup`.displaygroupId NOT IN ( + SELECT `lktagdisplaygroup`.displaygroupId + FROM tag + INNER JOIN `lktagdisplaygroup` + ON `lktagdisplaygroup`.tagId = tag.tagId + ) + '; + } else { + $operator = $this->getSanitizer()->getCheckbox('exactTags') == 1 ? '=' : 'LIKE'; + + $body .= " AND `displaygroup`.displaygroupId IN ( + SELECT `lktagdisplaygroup`.displaygroupId + FROM tag + INNER JOIN `lktagdisplaygroup` + ON `lktagdisplaygroup`.tagId = tag.tagId + "; + $i = 0; + foreach (explode(',', $tagFilter) as $tag) { + $i++; + + if ($i == 1) + $body .= ' WHERE `tag` ' . $operator . ' :tags' . $i; + else + $body .= ' OR `tag` ' . $operator . ' :tags' . $i; + + if ($operator === '=') + $params['tags' . $i] = $tag; + else + $params['tags' . $i] = '%' . $tag . '%'; + } + + $body .= " ) "; + } + } + // Sorting? $order = ''; if (is_array($sortOrder)) diff --git a/lib/Factory/DisplayProfileFactory.php b/lib/Factory/DisplayProfileFactory.php index 3443575991..23ca60f858 100644 --- a/lib/Factory/DisplayProfileFactory.php +++ b/lib/Factory/DisplayProfileFactory.php @@ -136,7 +136,7 @@ public function getByCommandId($commandId) * @return DisplayProfile[] * @throws NotFoundException */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $profiles = array(); diff --git a/lib/Factory/HelpFactory.php b/lib/Factory/HelpFactory.php index 875849ff59..025c983a42 100644 --- a/lib/Factory/HelpFactory.php +++ b/lib/Factory/HelpFactory.php @@ -60,7 +60,7 @@ public function getById($helpId) * @return array[Transition] * @throws NotFoundException */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $entries = array(); $params = array(); diff --git a/lib/Factory/LayoutFactory.php b/lib/Factory/LayoutFactory.php index f8dbbc224d..5b91c84336 100644 --- a/lib/Factory/LayoutFactory.php +++ b/lib/Factory/LayoutFactory.php @@ -31,6 +31,7 @@ use Xibo\Entity\Widget; use Xibo\Exception\InvalidArgumentException; use Xibo\Exception\NotFoundException; +use Xibo\Exception\XiboException; use Xibo\Service\ConfigServiceInterface; use Xibo\Service\DateServiceInterface; use Xibo\Service\LogServiceInterface; @@ -101,6 +102,9 @@ class LayoutFactory extends BaseFactory */ private $widgetOptionFactory; + /** @var WidgetAudioFactory */ + private $widgetAudioFactory; + /** @var PlaylistFactory */ private $playlistFactory; @@ -124,10 +128,11 @@ class LayoutFactory extends BaseFactory * @param WidgetFactory $widgetFactory * @param WidgetOptionFactory $widgetOptionFactory * @param PlaylistFactory $playlistFactory + * @param WidgetAudioFactory $widgetAudioFactory */ public function __construct($store, $log, $sanitizerService, $user, $userFactory, $config, $date, $dispatcher, $permissionFactory, $regionFactory, $tagFactory, $campaignFactory, $mediaFactory, $moduleFactory, $resolutionFactory, - $widgetFactory, $widgetOptionFactory, $playlistFactory) + $widgetFactory, $widgetOptionFactory, $playlistFactory, $widgetAudioFactory) { $this->setCommonDependencies($store, $log, $sanitizerService); $this->setAclDependencies($user, $userFactory); @@ -144,6 +149,7 @@ public function __construct($store, $log, $sanitizerService, $user, $userFactory $this->widgetFactory = $widgetFactory; $this->widgetOptionFactory = $widgetOptionFactory; $this->playlistFactory = $playlistFactory; + $this->widgetAudioFactory = $widgetAudioFactory; } /** @@ -235,6 +241,10 @@ public function createFromTemplate($layoutId, $ownerId, $name, $description, $ta // Ensure we have Playlists for each region foreach ($layout->regions as $region) { + + // Set the ownership of this region to the user creating from template + $region->setOwner($ownerId, true); + if (count($region->playlists) <= 0) { // Create a Playlist for this region $playlist = $this->playlistFactory->create($name, $ownerId); @@ -297,12 +307,18 @@ public function getByOwnerId($ownerId) /** * Get by CampaignId * @param int $campaignId - * @return array[Layout] + * @param bool $isOwnerOnly + * @return Layout[] * @throws NotFoundException */ - public function getByCampaignId($campaignId) + public function getByCampaignId($campaignId, $isOwnerOnly = false) { - return $this->query(['displayOrder'], array('campaignId' => $campaignId, 'excludeTemplates' => -1, 'retired' => -1)); + return $this->query(['displayOrder'], [ + 'campaignId' => ($isOwnerOnly) ? null : $campaignId, + 'ownerCampaignId' => ($isOwnerOnly) ? $campaignId : null, + 'excludeTemplates' => -1, + 'retired' => -1 + ]); } /** @@ -415,7 +431,9 @@ public function loadByXlf($layoutXlf, $layout = null) $module = $modules[$widget->type]; /* @var \Xibo\Entity\Module $module */ + // // Get all widget options + // $xpathQuery = '//region[@id="' . $region->tempId . '"]/media[@id="' . $widgetId . '"]/options'; foreach ($xpath->query($xpathQuery) as $optionsNode) { /* @var \DOMElement $optionsNode */ @@ -432,7 +450,9 @@ public function loadByXlf($layoutXlf, $layout = null) $this->getLog()->debug('Added %d options with xPath query: %s', count($widget->widgetOptions), $xpathQuery); + // // Get the MediaId associated with this widget (using the URI) + // if ($module->regionSpecific == 0) { $this->getLog()->debug('Library Widget, getting mediaId'); @@ -446,7 +466,9 @@ public function loadByXlf($layoutXlf, $layout = null) $widget->assignMedia($widget->tempId); } + // // Get all widget raw content + // foreach ($xpath->query('//region[@id="' . $region->tempId . '"]/media[@id="' . $widgetId . '"]/raw') as $rawNode) { /* @var \DOMElement $rawNode */ // Get children @@ -464,6 +486,33 @@ public function loadByXlf($layoutXlf, $layout = null) } } + // + // Audio + // + foreach ($xpath->query('//region[@id="' . $region->tempId . '"]/media[@id="' . $widgetId . '"]/audio') as $rawNode) { + /* @var \DOMElement $rawNode */ + // Get children + foreach ($rawNode->childNodes as $audioNode) { + /* @var \DOMElement $audioNode */ + if ($audioNode->textContent == null) + continue; + + $audioMediaId = $audioNode->getAttribute('mediaId'); + + if (empty($audioMediaId)) { + // Try to parse it from the text content + $audioMediaId = explode('.', $audioNode->textContent)[0]; + } + + $widgetAudio = $this->widgetAudioFactory->createEmpty(); + $widgetAudio->mediaId = $audioMediaId; + $widgetAudio->volume = $audioNode->getAttribute('volume'); + $widgetAudio->loop = $audioNode->getAttribute('loop'); + + $widget->assignAudio($widgetAudio); + } + } + // Add the widget to the playlist $playlist->assignWidget($widget); } @@ -503,6 +552,7 @@ public function loadByXlf($layoutXlf, $layout = null) * @param bool $importDataSetData * @param \Xibo\Controller\Library $libraryController * @return Layout + * @throws XiboException */ public function createFromZip($zipFile, $layoutName, $userId, $template, $replaceExisting, $importTags, $useExistingDataSets, $importDataSetData, $libraryController) { @@ -659,6 +709,7 @@ public function createFromZip($zipFile, $layoutName, $userId, $template, $replac // Keep the keys the same? Doesn't matter foreach ($widgets as $widget) { /* @var Widget $widget */ + $audioIds = $widget->getAudioIds(); $this->getLog()->debug('Checking Widget for the old mediaID [%d] so we can replace it with the new mediaId [%d] and storedAs [%s]. Media assigned to widget %s.', $oldMediaId, $newMediaId, $media->storedAs, json_encode($widget->mediaIds)); @@ -666,14 +717,27 @@ public function createFromZip($zipFile, $layoutName, $userId, $template, $replac $this->getLog()->debug('Removing %d and replacing with %d', $oldMediaId, $newMediaId); + // Are we an audio record? + if (in_array($oldMediaId, $audioIds)) { + // Swap the mediaId on the audio record + foreach ($widget->audio as $widgetAudio) { + if ($widgetAudio->mediaId == $oldMediaId) { + $widgetAudio->mediaId = $newMediaId; + break; + } + } + + } else { + // Non audio + $widget->setOptionValue('uri', 'attrib', $media->storedAs); + } + + // Always manage the assignments // Unassign the old ID $widget->unassignMedia($oldMediaId); // Assign the new ID $widget->assignMedia($newMediaId); - - // Update the widget option with the new ID - $widget->setOptionValue('uri', 'attrib', $media->storedAs); } } } @@ -850,10 +914,10 @@ public function createFromZip($zipFile, $layoutName, $userId, $template, $replac * Query for all Layouts * @param array $sortOrder * @param array $filterBy - * @return array[Layout] + * @return Layout[] * @throws NotFoundException */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $entries = array(); $params = array(); @@ -1003,6 +1067,12 @@ public function query($sortOrder = null, $filterBy = null) $params['retired'] = $this->getSanitizer()->getInt('retired', 0, $filterBy); } + if ($this->getSanitizer()->getInt('ownerCampaignId', $filterBy) !== null) { + // Join Campaign back onto it again + $body .= " AND `campaign`.campaignId = :ownerCampaignId "; + $params['ownerCampaignId'] = $this->getSanitizer()->getInt('ownerCampaignId', 0, $filterBy); + } + // Tags if ($this->getSanitizer()->getString('tags', $filterBy) != '') { @@ -1017,6 +1087,8 @@ public function query($sortOrder = null, $filterBy = null) ) '; } else { + $operator = $this->getSanitizer()->getCheckbox('exactTags') == 1 ? '=' : 'LIKE'; + $body .= " AND layout.layoutID IN ( SELECT lktaglayout.layoutId FROM tag @@ -1028,11 +1100,14 @@ public function query($sortOrder = null, $filterBy = null) $i++; if ($i == 1) - $body .= " WHERE tag LIKE :tags$i "; + $body .= ' WHERE `tag` ' . $operator . ' :tags' . $i; else - $body .= " OR tag LIKE :tags$i "; + $body .= ' OR `tag` ' . $operator . ' :tags' . $i; - $params['tags' . $i] = '%' . $tag . '%'; + if ($operator === '=') + $params['tags' . $i] = $tag; + else + $params['tags' . $i] = '%' . $tag . '%'; } $body .= " ) "; @@ -1082,6 +1157,26 @@ public function query($sortOrder = null, $filterBy = null) $params['mediaId'] = $this->getSanitizer()->getInt('mediaId', 0, $filterBy); } + // Media Like + if ($this->getSanitizer()->getString('mediaLike', $filterBy) !== null) { + $body .= ' AND layout.layoutId IN ( + SELECT DISTINCT `region`.layoutId + FROM `lkwidgetmedia` + INNER JOIN `widget` + ON `widget`.widgetId = `lkwidgetmedia`.widgetId + INNER JOIN `lkregionplaylist` + ON `lkregionplaylist`.playlistId = `widget`.playlistId + INNER JOIN `region` + ON `region`.regionId = `lkregionplaylist`.regionId + INNER JOIN `media` + ON `lkwidgetmedia`.mediaId = `media`.mediaId + WHERE `media`.name LIKE :mediaLike + ) + '; + + $params['mediaLike'] = '%' . $this->getSanitizer()->getString('mediaLike', $filterBy) . '%'; + } + // Sorting? $order = ''; if (is_array($sortOrder)) diff --git a/lib/Factory/LogFactory.php b/lib/Factory/LogFactory.php index 1fb3dd6303..f5dd152e35 100644 --- a/lib/Factory/LogFactory.php +++ b/lib/Factory/LogFactory.php @@ -46,7 +46,7 @@ public function createEmpty() * @param array $filterBy * @return array[\Xibo\Entity\Log] */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { if ($sortOrder == null) $sortOrder = ['logId DESC']; diff --git a/lib/Factory/MediaFactory.php b/lib/Factory/MediaFactory.php index 8844c48aba..84e40152b4 100644 --- a/lib/Factory/MediaFactory.php +++ b/lib/Factory/MediaFactory.php @@ -207,8 +207,23 @@ public function queueDownload($name, $uri, $expiry) $media->saveAsync(); // Add to our collection of queued downloads - if ($media->isSaveRequired) - $this->remoteDownloadQueue[] = $media; + // but only if its not already in the queue (we might have tried to queue it multiple times in the same request) + if ($media->isSaveRequired) { + $queueItem = true; + if ($media->getId() != null) { + // Existing media, check to see if we're already queued + foreach ($this->remoteDownloadQueue as $queue) { + // If we find this item already, don't queue + if ($queue->getId() === $media->getId()) { + $queueItem = false; + break; + } + } + } + + if ($queueItem) + $this->remoteDownloadQueue[] = $media; + } // Return the media item return $media; @@ -396,10 +411,10 @@ public function getByLayoutAndWidget($layoutId, $widgetId) /** * @param null $sortOrder - * @param null $filterBy + * @param array $filterBy * @return Media[] */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { if ($sortOrder === null) $sortOrder = ['name']; @@ -431,6 +446,13 @@ public function query($sortOrder = null, $filterBy = null) '; } + if (DBVERSION >= 134) { + $select .= ' + `media`.createdDt, + `media`.modifiedDt, + '; + } + $select .= " (SELECT GROUP_CONCAT(DISTINCT tag) FROM tag INNER JOIN lktagmedia ON lktagmedia.tagId = tag.tagId WHERE lktagmedia.mediaId = media.mediaID GROUP BY lktagmedia.mediaId) AS tags, "; $select .= " `user`.UserName AS owner, "; $select .= " (SELECT GROUP_CONCAT(DISTINCT `group`.group) @@ -621,6 +643,8 @@ public function query($sortOrder = null, $filterBy = null) ) '; } else { + $operator = $this->getSanitizer()->getCheckbox('exactTags') == 1 ? '=' : 'LIKE'; + $body .= " AND `media`.mediaId IN ( SELECT `lktagmedia`.mediaId FROM tag @@ -632,11 +656,14 @@ public function query($sortOrder = null, $filterBy = null) $i++; if ($i == 1) - $body .= " WHERE tag LIKE :tags$i "; + $body .= ' WHERE `tag` ' . $operator . ' :tags' . $i; else - $body .= " OR tag LIKE :tags$i "; + $body .= ' OR `tag` ' . $operator . ' :tags' . $i; - $params['tags' . $i] = '%' . $tag . '%'; + if ($operator === '=') + $params['tags' . $i] = $tag; + else + $params['tags' . $i] = '%' . $tag . '%'; } $body .= " ) "; diff --git a/lib/Factory/NotificationFactory.php b/lib/Factory/NotificationFactory.php index be77558ed3..d60c4cc7c2 100644 --- a/lib/Factory/NotificationFactory.php +++ b/lib/Factory/NotificationFactory.php @@ -60,9 +60,10 @@ public function createEmpty() * @param string $body * @param Date $date * @param bool $isEmail + * @param bool $addGroups * @return Notification */ - public function createSystemNotification($subject, $body, $date, $isEmail = true) + public function createSystemNotification($subject, $body, $date, $isEmail = true, $addGroups = true) { $notification = $this->createEmpty(); $notification->subject = $subject; @@ -74,10 +75,12 @@ public function createSystemNotification($subject, $body, $date, $isEmail = true $notification->userId = 0; $notification->isSystem = 1; - // Add the system notifications group - if there is one. - foreach ($this->userGroupFactory->getSystemNotificationGroups() as $group) { - /* @var \Xibo\Entity\UserGroup $group */ - $notification->assignUserGroup($group); + if ($addGroups) { + // Add the system notifications group - if there is one. + foreach ($this->userGroupFactory->getSystemNotificationGroups() as $group) { + /* @var \Xibo\Entity\UserGroup $group */ + $notification->assignUserGroup($group); + } } return $notification; @@ -113,9 +116,9 @@ public function getBySubjectAndDate($subject, $fromDt, $toDt) /** * @param array[Optional] $sortOrder * @param array[Optional] $filterBy - * @return array[Notification] + * @return Notification[] */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $entries = array(); @@ -149,14 +152,43 @@ public function query($sortOrder = null, $filterBy = null) $params['subject'] = $this->getSanitizer()->getString('subject', $filterBy); } - if ($this->getSanitizer()->getString('createFromDt', $filterBy) != null) { + if ($this->getSanitizer()->getInt('createFromDt', $filterBy) != null) { $body .= ' AND `notification`.createDt >= :createFromDt '; - $params['createFromDt'] = $this->getSanitizer()->getParam('createFromDt', $filterBy); + $params['createFromDt'] = $this->getSanitizer()->getInt('createFromDt', $filterBy); + } + + if ($this->getSanitizer()->getInt('releaseDt', $filterBy) != null) { + $body .= ' AND `notification`.releaseDt >= :releaseDt '; + $params['releaseDt'] = $this->getSanitizer()->getInt('releaseDt', $filterBy); } - if ($this->getSanitizer()->getString('createToDt', $filterBy) != null) { + if ($this->getSanitizer()->getInt('createToDt', $filterBy) != null) { $body .= ' AND `notification`.createDt < :createToDt '; - $params['createToDt'] = $this->getSanitizer()->getParam('createToDt', $filterBy); + $params['createToDt'] = $this->getSanitizer()->getInt('createToDt', $filterBy); + } + + // User Id? + if ($this->getSanitizer()->getInt('userId', $filterBy) !== null) { + $body .= ' AND `notification`.notificationId IN ( + SELECT notificationId + FROM `lknotificationuser` + WHERE userId = :userId + )'; + $params['userId'] = $this->getSanitizer()->getInt('userId', $filterBy); + } + + // Display Id? + if ($this->getSanitizer()->getInt('displayId', $filterBy) !== null) { + $body .= ' AND `notification`.notificationId IN ( + SELECT notificationId + FROM `lknotificationdg` + INNER JOIN `lkdgdg` + ON `lkdgdg`.parentId = `lknotificationdg`.displayGroupId + INNER JOIN `lkdisplaydg` + ON `lkdisplaydg`.displayGroupId = `lkdgdg`.childId + WHERE `lkdisplaydg`.displayId = :displayId + )'; + $params['displayId'] = $this->getSanitizer()->getInt('displayId', $filterBy); } // Sorting? diff --git a/lib/Factory/PermissionFactory.php b/lib/Factory/PermissionFactory.php index 4962c8624b..6c28e66157 100644 --- a/lib/Factory/PermissionFactory.php +++ b/lib/Factory/PermissionFactory.php @@ -264,7 +264,8 @@ public function getAllByObjectId($user, $entity, $objectId, $sortOrder = null, $ if ($this->getSanitizer()->getCheckbox('disableUserCheck', 0, $filterBy) == 0) { // Normal users can only see themselves if ($user->userTypeId == 3) { - $filterBy['userId'] = $user->userId; + $body .= ' AND `user`.userId = :currentUserId '; + $params['currentUserId'] = $user->userId; } // Group admins can only see users from their groups. else if ($user->userTypeId == 2) { diff --git a/lib/Factory/PlaylistFactory.php b/lib/Factory/PlaylistFactory.php index e1598d882c..5fdc478880 100644 --- a/lib/Factory/PlaylistFactory.php +++ b/lib/Factory/PlaylistFactory.php @@ -119,7 +119,7 @@ public function create($name, $ownerId) return $playlist; } - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $entries = array(); diff --git a/lib/Factory/RegionOptionFactory.php b/lib/Factory/RegionOptionFactory.php index ac6e3f0ae4..f212950052 100644 --- a/lib/Factory/RegionOptionFactory.php +++ b/lib/Factory/RegionOptionFactory.php @@ -86,7 +86,7 @@ public function create($regionId, $option, $value) * @param array $filterBy * @return array[RegionOption] */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $entries = array(); diff --git a/lib/Factory/ResolutionFactory.php b/lib/Factory/ResolutionFactory.php index 31f7f0ac7b..d59ec986e8 100644 --- a/lib/Factory/ResolutionFactory.php +++ b/lib/Factory/ResolutionFactory.php @@ -122,7 +122,7 @@ public function getByDesignerDimensions($width, $height) return $resolutions[0]; } - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { if ($sortOrder === null) $sortOrder = ['resolution']; diff --git a/lib/Factory/ScheduleFactory.php b/lib/Factory/ScheduleFactory.php index 067f0ffc01..fd29640c89 100644 --- a/lib/Factory/ScheduleFactory.php +++ b/lib/Factory/ScheduleFactory.php @@ -264,7 +264,7 @@ public function getForXmds($displayId, $fromDt, $toDt, $options = []) * @param array $filterBy * @return array[Schedule] */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $entries = []; $params = []; @@ -355,6 +355,34 @@ public function query($sortOrder = null, $filterBy = null) $params['futureSchedulesTo'] = $this->getSanitizer()->getInt('futureSchedulesTo', $filterBy); } + // Restrict to mediaId - meaning layout schedules of which the layouts contain the selected mediaId + if ($this->getSanitizer()->getInt('mediaId', $filterBy) !== null) { + $sql .= ' + AND schedule.campaignId IN ( + SELECT `lkcampaignlayout`.campaignId + FROM `lkwidgetmedia` + INNER JOIN `widget` + ON `widget`.widgetId = `lkwidgetmedia`.widgetId + INNER JOIN `lkregionplaylist` + ON `lkregionplaylist`.playlistId = `widget`.playlistId + INNER JOIN `region` + ON `region`.regionId = `lkregionplaylist`.regionId + INNER JOIN layout + ON layout.LayoutID = region.layoutId + INNER JOIN `lkcampaignlayout` + ON lkcampaignlayout.layoutId = layout.layoutId + WHERE lkwidgetmedia.mediaId = :mediaId + UNION + SELECT `lkcampaignlayout`.campaignId + FROM `layout` + INNER JOIN `lkcampaignlayout` + ON lkcampaignlayout.layoutId = layout.layoutId + WHERE `layout`.backgroundImageId = :mediaId + ) + '; + $params['mediaId'] = $this->getSanitizer()->getInt('mediaId', $filterBy); + } + // Sorting? if (is_array($sortOrder)) $sql .= 'ORDER BY ' . implode(',', $sortOrder); diff --git a/lib/Factory/SessionFactory.php b/lib/Factory/SessionFactory.php index f9a1a30061..5165abcc0b 100644 --- a/lib/Factory/SessionFactory.php +++ b/lib/Factory/SessionFactory.php @@ -70,7 +70,7 @@ public function getById($sessionId) * @return Session[] * @throws NotFoundException */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $entries = array(); $params = array(); diff --git a/lib/Factory/TagFactory.php b/lib/Factory/TagFactory.php index 26d5db5898..59ef8f4a57 100644 --- a/lib/Factory/TagFactory.php +++ b/lib/Factory/TagFactory.php @@ -191,4 +191,27 @@ public function loadByMediaId($mediaId) return $tags; } + + /** + * Gets tags for displayGroupId + * @param $displayGroupId + * @return array[Tag] + */ + public function loadByDisplayGroupId($displayGroupId) + { + $tags = array(); + + $sql = 'SELECT tag.tagId, tag.tag FROM `tag` INNER JOIN `lktagdisplaygroup` ON lktagdisplaygroup.tagId = tag.tagId WHERE lktagdisplaygroup.displayGroupId = :displayGroupId'; + + foreach ($this->getStore()->select($sql, array('displayGroupId' => $displayGroupId)) as $row) { + $tag = $this->createEmpty(); + $tag->tagId = $this->getSanitizer()->int($row['tagId']); + $tag->tag = $this->getSanitizer()->string($row['tag']); + $tag->assignMedia($displayGroupId); + + $tags[] = $tag; + } + + return $tags; + } } \ No newline at end of file diff --git a/lib/Factory/TransitionFactory.php b/lib/Factory/TransitionFactory.php index 7cc3bfbd9a..7775e84ee0 100644 --- a/lib/Factory/TransitionFactory.php +++ b/lib/Factory/TransitionFactory.php @@ -94,7 +94,7 @@ public function getEnabledByType($type) * @param array $filterBy * @return array[Transition] */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $entries = array(); $params = array(); diff --git a/lib/Factory/UpgradeFactory.php b/lib/Factory/UpgradeFactory.php index 0426f292df..64d101878b 100644 --- a/lib/Factory/UpgradeFactory.php +++ b/lib/Factory/UpgradeFactory.php @@ -91,7 +91,7 @@ public function getIncomplete() * @param array $filterBy * @return array[Upgrade] */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $this->checkAndProvision(); diff --git a/lib/Factory/UserFactory.php b/lib/Factory/UserFactory.php index cb6811f860..fb660d1c48 100644 --- a/lib/Factory/UserFactory.php +++ b/lib/Factory/UserFactory.php @@ -256,6 +256,13 @@ public function query($sortOrder = array(), $filterBy = array()) '; } + if (DBVERSION >= 134) { + $select .= ' + , + `group`.isDisplayNotification + '; + } + $body = ' FROM `user` INNER JOIN lkusergroup diff --git a/lib/Factory/UserGroupFactory.php b/lib/Factory/UserGroupFactory.php index 1d7dbc128d..d2deabb527 100644 --- a/lib/Factory/UserGroupFactory.php +++ b/lib/Factory/UserGroupFactory.php @@ -118,6 +118,21 @@ public function getSystemNotificationGroups() return $this->query(null, ['disableUserCheck' => 1, 'isSystemNotification' => 1, 'isUserSpecific' => -1]); } + /** + * Get isDisplayNotification Group + * @param int|null $displayGroupId Optionally provide a displayGroupId to restrict to view permissions. + * @return UserGroup[] + */ + public function getDisplayNotificationGroups($displayGroupId = null) + { + return $this->query(null, [ + 'disableUserCheck' => 1, + 'isDisplayNotification' => 1, + 'isUserSpecific' => -1, + 'displayGroupId' => $displayGroupId + ]); + } + /** * Get by User Id * @param int $userId @@ -141,12 +156,12 @@ public function getByNotificationId($notificationId) /** * Get by Display Group - * @param $displayGroupId - * @return array[User] + * @param int $displayGroupId + * @return UserGroup[] */ public function getByDisplayGroupId($displayGroupId) { - return $this->query(null, array('disableUserCheck' => 1, 'displayGroupId' => [$displayGroupId])); + return $this->query(null, ['disableUserCheck' => 1, 'displayGroupId' => $displayGroupId]); } /** @@ -155,7 +170,7 @@ public function getByDisplayGroupId($displayGroupId) * @return array[UserGroup] * @throws \Exception */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $entries = array(); $params = array(); @@ -184,6 +199,13 @@ public function query($sortOrder = null, $filterBy = null) '; } + if (DBVERSION >= 134) { + $select .= ' + , + `group`.isDisplayNotification + '; + } + $body = ' FROM `group` WHERE 1 = 1 @@ -265,20 +287,28 @@ public function query($sortOrder = null, $filterBy = null) $params['isSystemNotification'] = $this->getSanitizer()->getInt('isSystemNotification', $filterBy); } + if (DBVERSION >= 134 && $this->getSanitizer()->getInt('isDisplayNotification', $filterBy) !== null) { + $body .= ' AND isDisplayNotification = :isDisplayNotification '; + $params['isDisplayNotification'] = $this->getSanitizer()->getInt('isDisplayNotification', $filterBy); + } + if ($this->getSanitizer()->getInt('notificationId', $filterBy) !== null) { $body .= ' AND `group`.groupId IN (SELECT groupId FROM `lknotificationgroup` WHERE notificationId = :notificationId) '; $params['notificationId'] = $this->getSanitizer()->getInt('notificationId', $filterBy); } if ($this->getSanitizer()->getInt('displayGroupId', $filterBy) !== null) { - $body .= ' AND `group`.groupId IN ( - SELECT DISTINCT `permission`.groupId - FROM `permission` - INNER JOIN `permissionentity` - ON `permissionentity`.entityId = permission.entityId - AND `permissionentity`.entity = \'Xibo\\Entity\\DisplayGroup\' - WHERE `permission`.objectId = :displayGroupId - ) '; + $body .= ' + AND `group`.groupId IN ( + SELECT DISTINCT `permission`.groupId + FROM `permission` + INNER JOIN `permissionentity` + ON `permissionentity`.entityId = permission.entityId + AND `permissionentity`.entity = \'Xibo\\Entity\\DisplayGroup\' + WHERE `permission`.objectId = :displayGroupId + AND `permission`.view = 1 + ) + '; $params['displayGroupId'] = $this->getSanitizer()->getInt('displayGroupId', $filterBy); } diff --git a/lib/Factory/UserNotificationFactory.php b/lib/Factory/UserNotificationFactory.php index a13bfaffa0..af7b8312a6 100644 --- a/lib/Factory/UserNotificationFactory.php +++ b/lib/Factory/UserNotificationFactory.php @@ -119,7 +119,7 @@ public function countMyUnread() * @param array[Optional] $filterBy * @return array[UserNotification] */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $entries = array(); diff --git a/lib/Factory/UserOptionFactory.php b/lib/Factory/UserOptionFactory.php index 512083843b..917ff4c8db 100644 --- a/lib/Factory/UserOptionFactory.php +++ b/lib/Factory/UserOptionFactory.php @@ -73,7 +73,7 @@ public function create($userId, $option, $value) * @param array $filterBy * @return array[UserOption] */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { if (DBVERSION < 122) return []; diff --git a/lib/Factory/WidgetAudioFactory.php b/lib/Factory/WidgetAudioFactory.php index 86edc9bb69..8fe1c3184a 100644 --- a/lib/Factory/WidgetAudioFactory.php +++ b/lib/Factory/WidgetAudioFactory.php @@ -70,7 +70,7 @@ public function getByWidgetId($widgetId) * @param array $filterBy * @return array[int] */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $entries = []; $sql = 'SELECT `mediaId`, `widgetId`, `volume`, `loop` FROM `lkwidgetaudio` WHERE widgetId = :widgetId AND mediaId <> 0 '; diff --git a/lib/Factory/WidgetFactory.php b/lib/Factory/WidgetFactory.php index 7473b82c32..7b0f8755bb 100644 --- a/lib/Factory/WidgetFactory.php +++ b/lib/Factory/WidgetFactory.php @@ -177,7 +177,7 @@ public function create($ownerId, $playlistId, $type, $duration) return $widget; } - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { if ($sortOrder == null) $sortOrder = array('displayOrder'); diff --git a/lib/Factory/WidgetMediaFactory.php b/lib/Factory/WidgetMediaFactory.php index 52ba3f1f7a..220be19246 100644 --- a/lib/Factory/WidgetMediaFactory.php +++ b/lib/Factory/WidgetMediaFactory.php @@ -56,7 +56,7 @@ public function getByWidgetId($widgetId) * @param array $filterBy * @return array[int] */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $sql = 'SELECT mediaId FROM `lkwidgetmedia` WHERE widgetId = :widgetId AND mediaId <> 0 '; diff --git a/lib/Factory/WidgetOptionFactory.php b/lib/Factory/WidgetOptionFactory.php index 1c22569b5a..26799e44de 100644 --- a/lib/Factory/WidgetOptionFactory.php +++ b/lib/Factory/WidgetOptionFactory.php @@ -85,7 +85,7 @@ public function getByWidgetId($widgetId) * @param array $filterBy * @return array[WidgetOption] */ - public function query($sortOrder = null, $filterBy = null) + public function query($sortOrder = null, $filterBy = []) { $entries = array(); diff --git a/lib/Helper/DataSetUploadHandler.php b/lib/Helper/DataSetUploadHandler.php index 649e43a86f..95a00c8c1b 100644 --- a/lib/Helper/DataSetUploadHandler.php +++ b/lib/Helper/DataSetUploadHandler.php @@ -4,6 +4,7 @@ use Exception; use Xibo\Exception\AccessDeniedException; +use Xibo\Exception\InvalidArgumentException; /** * Class DataSetUploadHandler @@ -50,8 +51,8 @@ protected function handle_form_data($file, $index) // Has this column been provided in the mappings? $spreadSheetColumn = 0; - if( isset($_REQUEST['csvImport_' . $column->dataSetColumnId]) ) - $spreadSheetColumn = (($index === null) ? $_REQUEST['csvImport_' . $column->dataSetColumnId] : $_REQUEST['csvImport_' . $column->dataSetColumnId][$index]); + if (isset($_REQUEST['csvImport_' . $column->dataSetColumnId])) + $spreadSheetColumn = (($index === null) ? $_REQUEST['csvImport_' . $column->dataSetColumnId] : $_REQUEST['csvImport_' . $column->dataSetColumnId][$index]); // If it has been left blank, then skip if ($spreadSheetColumn != 0) @@ -67,9 +68,11 @@ protected function handle_form_data($file, $index) ini_set('auto_detect_line_endings', true); $firstRow = true; + $i = 0; $handle = fopen($controller->getConfig()->GetSetting('LIBRARY_LOCATION') . 'temp/' . $fileName, 'r'); while (($data = fgetcsv($handle)) !== FALSE ) { + $i++; $row = []; @@ -85,11 +88,24 @@ protected function handle_form_data($file, $index) // Insert the data into the correct column if (isset($spreadSheetMapping[$cell])) { - $row[$spreadSheetMapping[$cell]] = $data[$cell]; + // Sanitize the data a bit + $item = $data[$cell]; + + if ($item == '') + $item = null; + + $row[$spreadSheetMapping[$cell]] = $item; } } - $dataSet->addRow($row); + try { + $dataSet->addRow($row); + } catch (\PDOException $PDOException) { + $controller->getLog()->error('Error importing row ' . $i . '. E = ' . $PDOException->getMessage()); + $controller->getLog()->debug($PDOException->getTraceAsString()); + + throw new InvalidArgumentException(__('Unable to import row %d', $i), 'row'); + } } // Close the file diff --git a/lib/Helper/XiboUploadHandler.php b/lib/Helper/XiboUploadHandler.php index a39fe16d8c..d7a443d62c 100644 --- a/lib/Helper/XiboUploadHandler.php +++ b/lib/Helper/XiboUploadHandler.php @@ -45,18 +45,17 @@ protected function handle_form_data($file, $index) // Get some parameters if ($index === null) { - if (!isset($_REQUEST['name'])) - throw new \InvalidArgumentException(__('Missing Name Parameter')); - - $name = $_REQUEST['name']; - } + if (isset($_REQUEST['name'])) + $name = $_REQUEST['name']; + else + $name = $fileName; + } else { - if (!isset($_REQUEST['name'][$index])) - throw new \InvalidArgumentException(__('Missing Name Parameter')); - - $name = $_REQUEST['name'][$index]; - } - + if (isset($_REQUEST['name'][$index])) + $name = $_REQUEST['name'][$index]; + else + $name = $fileName; + } // Guess the type $module = $controller->getModuleFactory()->getByExtension(strtolower(substr(strrchr($fileName, '.'), 1))); $module = $controller->getModuleFactory()->create($module->type); @@ -298,11 +297,30 @@ protected function handle_form_data($file, $index) // Save the playlist $playlist->save(); + + // Handle permissions + // https://github.com/xibosignage/xibo/issues/1274 + if ($controller->getConfig()->GetSetting('INHERIT_PARENT_PERMISSIONS') == 1) { + // Apply permissions from the Parent + foreach ($playlist->permissions as $permission) { + /* @var Permission $permission */ + $permission = $controller->getPermissionFactory()->create($permission->groupId, get_class($widget), $widget->getId(), $permission->view, $permission->edit, $permission->delete); + $permission->save(); + } + } else { + foreach ($controller->getPermissionFactory()->createForNewEntity($controller->getUser(), get_class($widget), $widget->getId(), $controller->getConfig()->GetSetting('LAYOUT_DEFAULT'), $controller->getUserGroupFactory()) as $permission) { + /* @var Permission $permission */ + $permission->save(); + } + } } } catch (Exception $e) { $controller->getLog()->error('Error uploading media: %s', $e->getMessage()); $controller->getLog()->debug($e->getTraceAsString()); + // Unlink the temporary file + @unlink($filePath); + $file->error = $e->getMessage(); $controller->getApp()->commit = false; diff --git a/lib/Middleware/SAMLAuthentication.php b/lib/Middleware/SAMLAuthentication.php index 706f202b63..ce44847cca 100644 --- a/lib/Middleware/SAMLAuthentication.php +++ b/lib/Middleware/SAMLAuthentication.php @@ -84,7 +84,8 @@ public function call() $metadata = $settings->getSPMetadata(); $errors = $settings->validateMetadata($metadata); if (empty($errors)) { - header('Content-Type: text/xml'); + $app = $this->getApplication(); + $app->response()->header('Content-Type', 'text/xml'); echo $metadata; } else { throw new \Xibo\Exception\ConfigurationException( @@ -370,17 +371,7 @@ public function call() } }; - $updateUser = function () use ($app) { - $user = $app->user; - /* @var \Xibo\Entity\User $user */ - - if (!$app->public && $user->hasIdentity()) { - $user->touch(); - } - }; - $app->hook('slim.before.dispatch', $isAuthorised); - $app->hook('slim.after.dispatch', $updateUser); $this->next->call(); } diff --git a/lib/Middleware/State.php b/lib/Middleware/State.php index 8d2c7902bc..ef51e33982 100644 --- a/lib/Middleware/State.php +++ b/lib/Middleware/State.php @@ -174,7 +174,7 @@ public static function setState($app) }); // Set some public routes - $app->publicRoutes = array('/login', '/logout', '/clock', '/about', '/login/ping'); + $app->publicRoutes = array('/login', '/clock', '/about', '/login/ping'); // The state of the application response $app->container->singleton('state', function() { return new ApplicationState(); }); @@ -192,8 +192,13 @@ public static function setState($app) self::configureCache($app->container, $app->configService, $app->logWriter->getWriter()); // Register the help service - $app->container->singleton('helpService', function($container) { - return new HelpService($container->store, $container->configService, $container->pool); + $app->container->singleton('helpService', function($container) use ($app) { + return new HelpService( + $container->store, + $container->configService, + $container->pool, + ($app->router()->getCurrentRoute() !== null) ? $app->router()->getCurrentRoute()->getPattern() : null + ); }); // Create a session @@ -354,7 +359,8 @@ public static function registerControllersWithDi($app) $container->store, $container->applicationFactory, $container->applicationRedirectUriFactory, - $container->applicationScopeFactory + $container->applicationScopeFactory, + $container->userFactory ); }); @@ -497,7 +503,8 @@ public static function registerControllersWithDi($app) $container->mediaFactory, $container->scheduleFactory, $container->displayEventFactory, - $container->requiredFileFactory + $container->requiredFileFactory, + $container->tagFactory ); }); @@ -517,7 +524,8 @@ public static function registerControllersWithDi($app) $container->moduleFactory, $container->mediaFactory, $container->commandFactory, - $container->scheduleFactory + $container->scheduleFactory, + $container->tagFactory ); }); @@ -876,7 +884,9 @@ public static function registerControllersWithDi($app) $container->store, $container->displayFactory, $container->layoutFactory, - $container->mediaFactory + $container->mediaFactory, + $container->userFactory, + $container->userGroupFactory ); }); @@ -1155,7 +1165,8 @@ public static function registerFactoriesWithDi($container) $container->sanitizerService, $container->user, $container->userFactory, - $container->permissionFactory + $container->permissionFactory, + $container->tagFactory ); }); @@ -1197,7 +1208,8 @@ public static function registerFactoriesWithDi($container) $container->resolutionFactory, $container->widgetFactory, $container->widgetOptionFactory, - $container->playlistFactory + $container->playlistFactory, + $container->widgetAudioFactory ); }); diff --git a/lib/Middleware/WebAuthentication.php b/lib/Middleware/WebAuthentication.php index d21cbb0675..db7e3ff57b 100644 --- a/lib/Middleware/WebAuthentication.php +++ b/lib/Middleware/WebAuthentication.php @@ -104,6 +104,11 @@ public function call() // Store the current route so we can come back to it after login $app->flash('priorRoute', $app->request()->getRootUri() . $app->request()->getResourceUri()); + if ($user->hasIdentity()) { + $user->loggedIn = 0; + $user->touch(); + } + $redirectToLogin(); } } @@ -112,22 +117,18 @@ public function call() // If we are expired and come from ping/clock, then we redirect if ($app->session->isExpired() && ($resource == '/login/ping' || $resource == 'clock')) { - $redirectToLogin(); - } - } - }; - $updateUser = function () use ($app) { - $user = $app->user; - /* @var \Xibo\Entity\User $user */ + if ($user->hasIdentity()) { + $user->loggedIn = 0; + $user->touch(); + } - if (!$app->public && $user->hasIdentity()) { - $user->touch(); + $redirectToLogin(); + } } }; $app->hook('slim.before.dispatch', $isAuthorised); - $app->hook('slim.after.dispatch', $updateUser); $this->next->call(); } diff --git a/lib/Service/ConfigService.php b/lib/Service/ConfigService.php index 55435075fb..70f1c761c7 100644 --- a/lib/Service/ConfigService.php +++ b/lib/Service/ConfigService.php @@ -30,8 +30,8 @@ */ class ConfigService implements ConfigServiceInterface { - public static $WEBSITE_VERSION_NAME = '1.8.2'; - public static $WEBSITE_VERSION = 133; + public static $WEBSITE_VERSION_NAME = '1.8.3'; + public static $WEBSITE_VERSION = 134; public static $VERSION_REQUIRED = '5.5'; public static $VERSION_UNSUPPORTED = '7.0'; @@ -800,6 +800,22 @@ public function CheckEnvironment() 'advice' => $advice ); + // Check to see if OpenSSL is installed + $advice = __('OpenSSL is used to seal and verify messages sent to XMR'); + if ($this->checkOpenSsl()) { + $status = 1; + } else { + $this->envWarning = true; + $status = 2; + $advice .= __(' and is recommended.'); + } + + $rows[] = array( + 'item' => __('OpenSSL'), + 'status' => $status, + 'advice' => $advice + ); + $this->envTested = true; return $rows; @@ -1067,4 +1083,13 @@ public function checkBinLogFormat() return ($results[0]['Value'] != 'STATEMENT'); } + + /** + * Check open ssl is available + * @return bool + */ + public function checkOpenSsl() + { + return extension_loaded('openssl'); + } } diff --git a/lib/Service/DisplayNotifyService.php b/lib/Service/DisplayNotifyService.php index eb2cce3e19..8a8031b712 100644 --- a/lib/Service/DisplayNotifyService.php +++ b/lib/Service/DisplayNotifyService.php @@ -208,7 +208,11 @@ public function notifyByCampaignId($campaignId) ON lkdisplaydg.DisplayGroupID = `lkdgdg`.childId INNER JOIN `display` ON lkdisplaydg.DisplayID = display.displayID - WHERE `schedule`.CampaignID = :activeCampaignId + INNER JOIN `lkcampaignlayout` + ON `lkcampaignlayout`.campaignId = `schedule`.campaignId + INNER JOIN lkcampaignlayout layouts + ON layouts.layoutId = lkcampaignlayout.layoutId + WHERE `layouts`.campaignId = :activeCampaignId AND ( (schedule.FromDT < :toDt AND IFNULL(`schedule`.toDt, `schedule`.fromDt) > :fromDt) OR `schedule`.recurrence_range >= :fromDt diff --git a/lib/Service/HelpService.php b/lib/Service/HelpService.php index 745bcba818..20db66460a 100644 --- a/lib/Service/HelpService.php +++ b/lib/Service/HelpService.php @@ -43,14 +43,21 @@ class HelpService implements HelpServiceInterface */ private $pool; + /** @var string */ + private $currentPage; + /** * @inheritdoc */ - public function __construct($store, $config, $pool) + public function __construct($store, $config, $pool, $currentPage) { $this->store = $store; $this->config = $config; $this->pool = $pool; + + // Only take the first element of the current page + $currentPage = explode('/', ltrim($currentPage, '/')); + $this->currentPage = $currentPage[0]; } /** @@ -83,10 +90,10 @@ private function getConfig() /** * @inheritdoc */ - public function link($topic = '', $category = "General") + public function link($topic = null, $category = 'General') { // if topic is empty use the page name - $topic = ucfirst($topic); + $topic = ucfirst(($topic === null) ? $this->currentPage : $topic); $dbh = $this->getStore()->getConnection(); diff --git a/lib/Service/HelpServiceInterface.php b/lib/Service/HelpServiceInterface.php index f1fd3a7d98..53e7f5a5c1 100644 --- a/lib/Service/HelpServiceInterface.php +++ b/lib/Service/HelpServiceInterface.php @@ -21,8 +21,9 @@ interface HelpServiceInterface * @param StorageServiceInterface $store * @param ConfigServiceInterface $config * @param PoolInterface $pool + * @param string $currentPage */ - public function __construct($store, $config, $pool); + public function __construct($store, $config, $pool, $currentPage); /** * Get Help Link diff --git a/lib/Service/PlayerActionService.php b/lib/Service/PlayerActionService.php index 4803d440e5..a7ae991abd 100644 --- a/lib/Service/PlayerActionService.php +++ b/lib/Service/PlayerActionService.php @@ -11,6 +11,7 @@ use Xibo\Entity\Display; use Xibo\Exception\ConfigurationException; +use Xibo\Exception\InvalidArgumentException; use Xibo\XMR\PlayerAction; use Xibo\XMR\PlayerActionException; @@ -76,16 +77,21 @@ public function sendAction($displays, $action) throw new ConfigurationException(__('ZeroMQ is required to send Player Actions. Please check your configuration.')); if ($this->xmrAddress == '') - throw new \InvalidArgumentException(__('XMR address is not set')); + throw new InvalidArgumentException(__('XMR address is not set'), 'xmrAddress'); // Send a message to all displays foreach ($displays as $display) { /* @var Display $display */ if ($display->xmrChannel == '' || $display->xmrPubKey == '') - throw new \InvalidArgumentException(__('This Player is not configured or ready to receive push commands over XMR. Please contact your administrator.')); + throw new InvalidArgumentException(__('This Player is not configured or ready to receive push commands over XMR. Please contact your administrator.'), 'xmrRegistered'); $displayAction = clone $action; - $displayAction->setIdentity($display->xmrChannel, $display->xmrPubKey); + + try { + $displayAction->setIdentity($display->xmrChannel, $display->xmrPubKey); + } catch (\Exception $exception) { + throw new InvalidArgumentException(__('Invalid XMR registration'), 'xmrPubKey'); + } // Add to collection $this->actions[] = $displayAction; diff --git a/lib/Storage/PdoStorageService.php b/lib/Storage/PdoStorageService.php index b883f12562..ca8b8283e0 100644 --- a/lib/Storage/PdoStorageService.php +++ b/lib/Storage/PdoStorageService.php @@ -208,7 +208,11 @@ public function update($sql, $params) $sth->execute($params); + $rows = $sth->rowCount(); + $this->incrementStat('default', 'update'); + + return $rows; } /** diff --git a/lib/Storage/StorageServiceInterface.php b/lib/Storage/StorageServiceInterface.php index b0e21482b8..11d4255a35 100644 --- a/lib/Storage/StorageServiceInterface.php +++ b/lib/Storage/StorageServiceInterface.php @@ -79,6 +79,7 @@ public function insert($sql, $params); * Run Update SQL * @param string $sql * @param array $params + * @return int affected rows * @throws \PDOException */ public function update($sql, $params); diff --git a/lib/Twig/TransExtension.php b/lib/Twig/TransExtension.php index e66e701ee7..5b1a811638 100644 --- a/lib/Twig/TransExtension.php +++ b/lib/Twig/TransExtension.php @@ -17,9 +17,9 @@ class TransExtension extends \Twig_Extension * @return array An array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances */ public function getTokenParsers() -{ - return array(new TransTokenParser()); -} + { + return array(new TransTokenParser()); + } /** * Returns a list of filters to add to the existing list. @@ -27,11 +27,11 @@ public function getTokenParsers() * @return array An array of filters */ public function getFilters() -{ - return array( - new \Twig_SimpleFilter('trans', '__'), - ); -} + { + return array( + new \Twig_SimpleFilter('trans', '__'), + ); + } /** * Returns the name of the extension. @@ -39,7 +39,7 @@ public function getFilters() * @return string The extension name */ public function getName() -{ - return 'i18n'; -} + { + return 'i18n'; + } } \ No newline at end of file diff --git a/lib/Twig/TransNode.php b/lib/Twig/TransNode.php index 3bf8ecbbb2..6b077ba7fe 100644 --- a/lib/Twig/TransNode.php +++ b/lib/Twig/TransNode.php @@ -44,7 +44,10 @@ public function compile(\Twig_Compiler $compiler) } if ($vars) { + // Add a hint for xgettext so that poedit goes not treat the variable substitution as PHP format + // https://github.com/xibosignage/xibo/issues/1284 $compiler + ->write('/* xgettext:no-php-format */') ->write('echo strtr(' . $function . '(') ->subcompile($msg); diff --git a/lib/Widget/AlphaVantageBase.php b/lib/Widget/AlphaVantageBase.php new file mode 100644 index 0000000000..932e7ac44f --- /dev/null +++ b/lib/Widget/AlphaVantageBase.php @@ -0,0 +1,95 @@ +request('GET', 'https://www.alphavantage.co/query', [ + 'query' => [ + 'function' => 'CURRENCY_EXCHANGE_RATE', + 'from_currency' => $fromCurrency, + 'to_currency' => $toCurrency, + 'apikey' => $this->getApiKey() + ] + ]); + + return json_decode($request->getBody(), true); + } + /** + * @param $symbol + * @return array + */ + protected function getStockQuote($symbol) + { + // Use a web request + $client = new Client(); + + $request = $client->request('GET', 'https://www.alphavantage.co/query', [ + 'query' => [ + 'function' => 'TIME_SERIES_DAILY', + 'symbol' => $symbol, + 'apikey' => $this->getApiKey() + ] + ]); + + return json_decode($request->getBody(), true); + } + + /** + * Get the API Key + * @return string + * @throws ConfigurationException + */ + protected function getApiKey() + { + $apiKey = $this->getSetting('apiKey', null); + + if ($apiKey == null) + throw new ConfigurationException(__('Missing API Key')); + + return $apiKey; + } + + /** + * @param $base + * @param $pairs + * @return mixed + */ + protected function getPriorDay($base, $pairs) + { + // Use a web request + $client = new Client(); + + $yesterday = Date::yesterday()->format('Y-m-d'); + + $request = $client->request('GET', 'https://api.fixer.io/' . $yesterday, [ + 'query' => [ + 'base' => $base, + 'symbols' => is_array($pairs) ? implode(',', $pairs) : $pairs + ] + ]); + + return json_decode($request->getBody(), true)['rates']; + } +} \ No newline at end of file diff --git a/lib/Widget/Currencies.php b/lib/Widget/Currencies.php index 9d7f90f2a3..8f8688ade5 100644 --- a/lib/Widget/Currencies.php +++ b/lib/Widget/Currencies.php @@ -21,6 +21,8 @@ */ namespace Xibo\Widget; +use GuzzleHttp\Exception\RequestException; +use Stash\Invalidation; use Xibo\Exception\NotFoundException; use Xibo\Factory\ModuleFactory; @@ -28,7 +30,7 @@ * Class Currencies * @package Xibo\Widget */ -class Currencies extends YahooBase +class Currencies extends AlphaVantageBase { public $codeSchemaVersion = 1; @@ -44,7 +46,7 @@ public function installOrUpdate($moduleFactory) $module->name = 'Currencies'; $module->type = 'currencies'; $module->class = 'Xibo\Widget\Currencies'; - $module->description = 'Yahoo Currencies'; + $module->description = 'A module for showing Currency pairs and exchange rates'; $module->imageUri = 'forms/library.gif'; $module->enabled = 1; $module->previewEnabled = 1; @@ -88,6 +90,7 @@ public function settingsForm() */ public function settings() { + $this->module->settings['apiKey'] = $this->getSanitizer()->getString('apiKey'); $this->module->settings['cachePeriod'] = $this->getSanitizer()->getInt('cachePeriod', 300); // Return an array of the processed settings. @@ -335,19 +338,30 @@ public function setCommonOptions() } /** - * Get YQL Data - * @return array|bool an array of results according to the key specified by result identifier. false if an invalid value is returned. + * Get Results + * @return array|bool an array of results. false if an invalid value is returned. */ - protected function getYql() + protected function getResults() { - + // Does this require a reversed conversion? $reverseConversion = ($this->getOption('reverseConversion', 0) == 1); + // What items/base currencies are we interested in? $items = $this->getOption('items'); $base = $this->getOption('base'); + + if ($items == '' || $base == '' ) { + $this->getLog()->error('Missing Items for Currencies Module with WidgetId ' . $this->getWidgetId()); + return false; + } + + // Parse items out into an array + $items = explode(',', $items); // Get current item template - if( $this->getOption('overrideTemplate') == 0 ) { + $itemTemplate = null; + + if ($this->getOption('overrideTemplate') == 0) { $template = $this->getTemplateById($this->getOption('templateId')); if (isset($template)) { @@ -357,83 +371,111 @@ protected function getYql() $itemTemplate = $this->getRawNode('itemTemplate'); } - // Use the template to see if we need the xchange or quotes table - $useVariation = stripos($itemTemplate, '[ChangePercentage]') > -1; - if($useVariation){ - $resultIdentifier = "quote"; - $yql = "select * from yahoo.finance.quotes where symbol in ([Item])"; - } else { - $resultIdentifier = "rate"; - $yql = "select * from yahoo.finance.xchange where pair in ([Item])"; - } - - $this->getLog()->debug('Finance module with YQL = . Looking for %s in response', $yql); + // Does the template require a percentage change calculation. + $percentageChangeRequested = stripos($itemTemplate, '[ChangePercentage]') > -1; - if ($yql == '' || $items == '' || $base == '' ) { - $this->getLog()->error('Missing YQL/Items for Finance Module with WidgetId %d', $this->getWidgetId()); - return false; - } + // Our cache key is based on the base/items/changepercentage + /** @var \Stash\Item $cache */ + $cache = $this->getPool()->getItem($this->makeCacheKey(md5($base . implode(',', $items) . $percentageChangeRequested . $reverseConversion))); + $cache->setInvalidationMethod(Invalidation::SLEEP, 5000, 15); - if (strstr($items, ',')) - $items = explode(',', $items); - else - $items = [$items]; - - // quote each item - $itemsJoined = array(); - - foreach ($items as $key => $item) { - - // Remove the multiplier if there's one - $item = explode('|', $item)[0]; - - $baseItemPair = ( $reverseConversion ) ? ( trim($item) . trim($base) ) : ( trim($base) . trim($item) ); - - array_push( - $itemsJoined, - ( $useVariation ) ? ('\'' . $baseItemPair . '=X' . '\'') : ('\'' . $baseItemPair . '\'') - ); - } + $data = $cache->get(); - $yql = str_replace('[Item]', implode(',', $itemsJoined), $yql); + if ($cache->isMiss()) { + // Lock this cache record + $cache->lock(); - // Fire off a request for the data - $cache = $this->getPool()->getItem($this->makeCacheKey(md5($yql))); + // Start fresh + $data = []; + $priorDay = []; - $data = $cache->get(); + // Do we need to get the data for percentage change? + if ($percentageChangeRequested && !$reverseConversion) { + try { + // Get the prior day + $priorDay = $this->getPriorDay($base, $items); - if ($cache->isMiss()) { + $this->getLog()->debug('Percentage change requested, prior day is ' . var_export($priorDay, true)); - $cache->lock(); + } catch (RequestException $requestException) { + $this->getLog()->error('Problem getting percentage change currency information. E = ' . $requestException->getMessage()); + $this->getLog()->debug($requestException->getTraceAsString()); + } + } - $this->getLog()->debug('Querying API for ' . $yql); + // Each item we want is a call to the results API + try { + foreach ($items as $currency) { + // Remove the multiplier if there's one (this is handled when we substitute the results into the template) + $currency = explode('|', $currency)[0]; + + // Do we need to reverse the from/to currency for this comparison? + if ($reverseConversion) { + $result = $this->getCurrencyExchangeRate($currency, $base); + + // We need to get the proir day for this pair only (reversed) + $priorDay = $this->getPriorDay($currency, $base); + + $this->getLog()->debug('Percentage change requested, prior day is ' . var_export($priorDay, true)); + + } else { + $result = $this->getCurrencyExchangeRate($base, $currency); + } + + $this->getLog()->debug('Results are: ' . var_export($result, true)); + + $parsedResult = [ + 'time' => $result['Realtime Currency Exchange Rate']['6. Last Refreshed'], + 'ToName' => $result['Realtime Currency Exchange Rate']['3. To_Currency Code'], + 'ToCurrency' => $result['Realtime Currency Exchange Rate']['4. To_Currency Name'], + 'FromName' => $result['Realtime Currency Exchange Rate']['1. From_Currency Code'], + 'FromCurrency' => $result['Realtime Currency Exchange Rate']['2. From_Currency Name'], + 'Bid' => round($result['Realtime Currency Exchange Rate']['5. Exchange Rate'], 4), + 'Ask' => round($result['Realtime Currency Exchange Rate']['5. Exchange Rate'], 4), + 'LastTradePriceOnly' => round($result['Realtime Currency Exchange Rate']['5. Exchange Rate'], 4), + 'RawLastTradePriceOnly' => $result['Realtime Currency Exchange Rate']['5. Exchange Rate'], + 'TimeZone' => $result['Realtime Currency Exchange Rate']['7. Time Zone'], + ]; + + // Set the name/currency to be the full name including the base currency + $parsedResult['Name'] = $parsedResult['FromName'] . '/' . $parsedResult['ToName']; + $parsedResult['Currency'] = $parsedResult['FromCurrency'] . '/' . $parsedResult['ToCurrency']; + + // work out the change when compared to the previous day + if ($percentageChangeRequested && isset($priorDay[$parsedResult['ToName']]) && is_numeric($priorDay[$parsedResult['ToName']])) { + $parsedResult['YesterdayTradePriceOnly'] = $priorDay[$parsedResult['ToName']]; + $parsedResult['Change'] = $parsedResult['RawLastTradePriceOnly'] - $parsedResult['YesterdayTradePriceOnly']; + } else { + $parsedResult['YesterdayTradePriceOnly'] = 0; + $parsedResult['Change'] = 0; + } + + // Parse the result and add it to our data array + $data[] = $parsedResult; + } + } catch (RequestException $requestException) { + $this->getLog()->error('Problem getting currency information. E = ' . $requestException->getMessage()); + $this->getLog()->debug($requestException->getTraceAsString()); - if (!$data = $this->request($yql)) { return false; } + $this->getLog()->debug('Parsed Results are: ' . var_export($data, true)); + // Cache it $cache->set($data); - $cache->expiresAfter($this->getSetting('cachePeriod', 300)); + $cache->expiresAfter($this->getSetting('cachePeriod', 3600)); $this->getPool()->saveDeferred($cache); - } - // Pull out the results according to the resultIdentifier - // If the element to return is an array and we aren't, then box. - $results = $data[$resultIdentifier]; - - if (array_key_exists(0, $results)) - return $results; - else - return [$results]; + return $data; } /** * Run through the data and substitute into the template * @param $data * @param $source - * @param $base + * @param $baseCurrency * @return mixed */ private function makeSubstitutions($data, $source, $baseCurrency) @@ -460,7 +502,7 @@ private function makeSubstitutions($data, $source, $baseCurrency) $isPreview = ($this->getSanitizer()->getCheckbox('preview') == 1); // Match that in the array - if ( isset($data[$replace]) ){ + if (isset($data[$replace])) { // If the tag exists on the data variables use that var $replacement = $data[$replace]; } else { @@ -470,7 +512,7 @@ private function makeSubstitutions($data, $source, $baseCurrency) if (stripos($replace, 'time|') > -1) { $timeSplit = explode('|', $replace); - $time = $this->getDate()->getLocalDate($data['time'], $timeSplit[1]); + $time = $this->getDate()->parse($data['time']. 'Y-m-d H:i:s')->format($timeSplit[1]); $replacement = $time; @@ -490,12 +532,9 @@ private function makeSubstitutions($data, $source, $baseCurrency) // Replace the other tags switch ($replace) { case 'NameShort': - // Replace the name to have just the second currency (or the first if the currency is reversed) - $replaceBase = ( $reverseConversion ) ? ('/' . $baseCurrency) : ($baseCurrency . '/'); - - if (isset($data['Name'])) - $replacement = trim(str_replace($replaceBase,'',$data['Name'])); - + + $replacement = ($reverseConversion) ? $data['FromName'] : $data['ToName']; + break; case 'Multiplier': @@ -504,10 +543,7 @@ private function makeSubstitutions($data, $source, $baseCurrency) $replacement = ''; // Get the current currency name/code - $pairName = ( $reverseConversion ) ? ('/' . $baseCurrency) : ($baseCurrency . '/'); - - if (isset($data['Name'])) - $currencyName = trim(str_replace($pairName,'',$data['Name'])); + $currencyName = ($reverseConversion) ? $data['FromName'] : $data['ToName']; // Search for the item that relates to the actual currency foreach ($items as $item) { @@ -526,8 +562,8 @@ private function makeSubstitutions($data, $source, $baseCurrency) break; case 'CurrencyFlag': - $replaceBase = ( $reverseConversion ) ? ('/' . $baseCurrency) : ($baseCurrency . '/'); - $currencyCode = str_replace($replaceBase,'',$data['Name']); + + $currencyCode = ($reverseConversion) ? $data['FromName'] : $data['ToName']; if (!file_exists(PROJECT_ROOT . '/web/modules/currencies/currency-flags/' . $currencyCode . '.svg')) $currencyCode = 'default'; @@ -544,29 +580,12 @@ private function makeSubstitutions($data, $source, $baseCurrency) break; - case 'ChangePercentage': - // Protect against null values - if(($data['Change'] == null || $data['LastTradePriceOnly'] == null)){ - $replacement = "NULL"; - } else { - // Calculate the percentage dividing the change by the ( previous value minus the change ) - $percentage = $data['Change'] / ( $data['LastTradePriceOnly'] - $data['Change'] ); - - // Convert the value to percentage and round it - $replacement = round($percentage*100, 2); - } - - break; - case 'LastTradePriceOnlyValue': case 'BidValue': case 'AskValue': // Get the converted currency name - $currencyName = ( $reverseConversion ) ? ('/' . $baseCurrency) : ($baseCurrency . '/'); - - if (isset($data['Name'])) - $currencyName = trim(str_replace($currencyName, '', $data['Name'])); + $currencyName = ($reverseConversion) ? $data['FromName'] : $data['ToName']; // Get the field's name and set the replacement as the default value from the API $fieldName = str_replace('Value', '', $replace); @@ -589,15 +608,29 @@ private function makeSubstitutions($data, $source, $baseCurrency) } break; + + case 'ChangePercentage': + // Protect against null values + if(($data['Change'] == null || $data['LastTradePriceOnly'] == null)){ + $replacement = "NULL"; + } else { + // Calculate the percentage dividing the change by the ( previous value minus the change ) + $percentage = $data['Change'] / ( $data['LastTradePriceOnly'] - $data['Change'] ); + + // Convert the value to percentage and round it + $replacement = round($percentage*100, 2); + } + + break; case 'ChangeStyle': // Default value as no change $replacement = 'value-equal'; // Protect against null values - if(($data['Change'] != null && $data['LastTradePriceOnly'] != null)){ + if (($data['Change'] != null && $data['LastTradePriceOnly'] != null)) { - if ( $data['Change'] > 0 ) { + if ($data['Change'] > 0) { $replacement = 'value-up'; } else if ( $data['Change'] < 0 ){ $replacement = 'value-down'; @@ -612,7 +645,7 @@ private function makeSubstitutions($data, $source, $baseCurrency) $replacement = 'right-arrow'; // Protect against null values - if(($data['Change'] != null && $data['LastTradePriceOnly'] != null)){ + if (($data['Change'] != null && $data['LastTradePriceOnly'] != null)) { if ( $data['Change'] > 0 ) { $replacement = 'up-arrow'; @@ -649,7 +682,7 @@ private function makeSubstitutions($data, $source, $baseCurrency) */ public function getTab($tab) { - if (!$data = $this->getYql()) + if (!$data = $this->getResults()) throw new NotFoundException(__('No data returned, please check error log.')); return ['results' => $data[0]]; @@ -673,7 +706,7 @@ public function getResource($displayId = 0) $durationIsPerItem = $this->getOption('durationIsPerItem', 1); // Generate a JSON string of items. - if (!$items = $this->getYql()) { + if (!$items = $this->getResults()) { return ''; } diff --git a/lib/Widget/DataSetView.php b/lib/Widget/DataSetView.php index caa3090819..da605d550d 100644 --- a/lib/Widget/DataSetView.php +++ b/lib/Widget/DataSetView.php @@ -447,9 +447,14 @@ public function hoverPreview() $output .= '
{%=o.formatFileSize(file.size)%}
- {% if (!o.files.error) { %}