diff --git a/.gitignore b/.gitignore index 0d5cb14..27220d0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,10 @@ /web/modules/contrib/ /web/themes/contrib/ /web/profiles/contrib/ +/web/libraries/ +/modules/ +/1modules/ + #/web/libraries/ /modules/ /1modules/ @@ -31,4 +35,13 @@ /web/sites/default/local.services.yml /web/sites/default/1local.services.yml /web/themes/custom/barbell/css/Henrik/ -/web/themes/custom/barbell/css/__MACOSX/ \ No newline at end of file +/web/themes/custom/barbell/css/__MACOSX/ + +# Kata +.vscode/ +web/themes/custom/barbell/img/aics.png +web/themes/custom/barbell/img/barbell logo.psd +web/themes/custom/barbell/img/se.png +web/themes/custom/barbell/img/fe.png +config/ +files.zip \ No newline at end of file diff --git a/backups/backup-2019-11-07T17-59-08.mysql.gz b/backups/backup-2019-11-07T17-59-08.mysql.gz new file mode 100644 index 0000000..76a66f5 Binary files /dev/null and b/backups/backup-2019-11-07T17-59-08.mysql.gz differ diff --git a/composer.json b/composer.json index 20ab815..eaedbde 100644 --- a/composer.json +++ b/composer.json @@ -28,11 +28,14 @@ "drupal/core": "^8.7.0", "drupal/devel": "2.x-dev", "drupal/entity_clone": "1.x-dev", + "drupal/entityqueue": "^1.0", "drupal/fontawesome": "^2.14", + "drupal/pathauto": "^1.6", "drupal/responsive_menus": "1.x-dev", "drupal/slick": "^2.0", "drupal/slick_lightbox": "^1.0", "drupal/slick_views": "^2.0", + "drupal/webform": "^5.5", "drush/drush": "^9.0.0", "vlucas/phpdotenv": "^2.4", "webflo/drupal-finder": "^1.0.0", diff --git a/composer.lock b/composer.lock index 0d1b614..e14a178 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "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": "7a45619b75813915b6b1fd3a185b06f4", + "content-hash": "98dd2023a6aef6ca5fad93e3ace8327e", "packages": [ { "name": "alchemy/zippy", @@ -1558,16 +1558,16 @@ }, { "name": "doctrine/annotations", - "version": "v1.7.0", + "version": "v1.8.0", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "fa4c4e861e809d6a1103bd620cce63ed91aedfeb" + "reference": "904dca4eb10715b92569fbcd79e201d5c349b6bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/fa4c4e861e809d6a1103bd620cce63ed91aedfeb", - "reference": "fa4c4e861e809d6a1103bd620cce63ed91aedfeb", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/904dca4eb10715b92569fbcd79e201d5c349b6bc", + "reference": "904dca4eb10715b92569fbcd79e201d5c349b6bc", "shasum": "" }, "require": { @@ -1576,7 +1576,7 @@ }, "require-dev": { "doctrine/cache": "1.*", - "phpunit/phpunit": "^7.5@dev" + "phpunit/phpunit": "^7.5" }, "type": "library", "extra": { @@ -1622,7 +1622,7 @@ "docblock", "parser" ], - "time": "2019-08-08T18:11:40+00:00" + "time": "2019-10-01T18:55:10+00:00" }, { "name": "doctrine/cache", @@ -2320,17 +2320,17 @@ }, { "name": "drupal/blazy", - "version": "2.0.0-rc4", + "version": "2.0.0-rc5", "source": { "type": "git", "url": "https://git.drupalcode.org/project/blazy.git", - "reference": "8.x-2.0-rc4" + "reference": "8.x-2.0-rc5" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/blazy-8.x-2.0-rc4.zip", - "reference": "8.x-2.0-rc4", - "shasum": "5b464535cc2a27f66903c7d0fd32a1cb1b8a5c08" + "url": "https://ftp.drupal.org/files/projects/blazy-8.x-2.0-rc5.zip", + "reference": "8.x-2.0-rc5", + "shasum": "cc60f84ae6e9eac75d6ceed88da2ed0456a6d7ec" }, "require": { "drupal/core": "^8.6" @@ -2341,8 +2341,8 @@ "dev-2.x": "2.x-dev" }, "drupal": { - "version": "8.x-2.0-rc4", - "datestamp": "1567239185", + "version": "8.x-2.0-rc5", + "datestamp": "1569670984", "security-coverage": { "status": "not-covered", "message": "RC releases are not covered by Drupal security advisories." @@ -2639,16 +2639,16 @@ }, { "name": "drupal/core", - "version": "8.7.7", + "version": "8.7.8", "source": { "type": "git", "url": "https://github.com/drupal/core.git", - "reference": "32e1d7a67bbc28f07dc43d9ff692c0e90a4aeb92" + "reference": "476f491b85306c09101106d9b66a5dbe73c21bf0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/drupal/core/zipball/32e1d7a67bbc28f07dc43d9ff692c0e90a4aeb92", - "reference": "32e1d7a67bbc28f07dc43d9ff692c0e90a4aeb92", + "url": "https://api.github.com/repos/drupal/core/zipball/476f491b85306c09101106d9b66a5dbe73c21bf0", + "reference": "476f491b85306c09101106d9b66a5dbe73c21bf0", "shasum": "" }, "require": { @@ -2686,7 +2686,7 @@ "symfony/http-kernel": "~3.4.14", "symfony/polyfill-iconv": "^1.0", "symfony/process": "~3.4.0", - "symfony/psr-http-message-bridge": "^1.0", + "symfony/psr-http-message-bridge": "^1.1.2", "symfony/routing": "~3.4.0", "symfony/serializer": "~3.4.0", "symfony/translation": "~3.4.0", @@ -2880,7 +2880,100 @@ "GPL-2.0-or-later" ], "description": "Drupal is an open source content management platform powering millions of websites and applications.", - "time": "2019-09-04T10:26:35+00:00" + "time": "2019-10-02T18:41:30+00:00" + }, + { + "name": "drupal/ctools", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/ctools.git", + "reference": "8.x-3.2" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/ctools-8.x-3.2.zip", + "reference": "8.x-3.2", + "shasum": "d6da87239b64ba708a5977e7b33b1e009e36b091" + }, + "require": { + "drupal/core": "^8.5" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-3.x": "3.x-dev" + }, + "drupal": { + "version": "8.x-3.2", + "datestamp": "1550728386", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "Kris Vanderwater (EclipseGc)", + "homepage": "https://www.drupal.org/u/eclipsegc", + "role": "Maintainer" + }, + { + "name": "Jakob Perry (japerry)", + "homepage": "https://www.drupal.org/u/japerry", + "role": "Maintainer" + }, + { + "name": "Tim Plunkett (tim.plunkett)", + "homepage": "https://www.drupal.org/u/timplunkett", + "role": "Maintainer" + }, + { + "name": "James Gilliland (neclimdul)", + "homepage": "https://www.drupal.org/u/neclimdul", + "role": "Maintainer" + }, + { + "name": "Daniel Wehner (dawehner)", + "homepage": "https://www.drupal.org/u/dawehner", + "role": "Maintainer" + }, + { + "name": "joelpittet", + "homepage": "https://www.drupal.org/user/160302" + }, + { + "name": "merlinofchaos", + "homepage": "https://www.drupal.org/user/26979" + }, + { + "name": "neclimdul", + "homepage": "https://www.drupal.org/user/48673" + }, + { + "name": "sdboyer", + "homepage": "https://www.drupal.org/user/146719" + }, + { + "name": "sun", + "homepage": "https://www.drupal.org/user/54136" + }, + { + "name": "tim.plunkett", + "homepage": "https://www.drupal.org/user/241634" + } + ], + "description": "Provides a number of utility and helper APIs for Drupal developers and site builders.", + "homepage": "https://www.drupal.org/project/ctools", + "support": { + "source": "http://cgit.drupalcode.org/ctools", + "issues": "https://www.drupal.org/project/issues/ctools" + } }, { "name": "drupal/devel", @@ -3004,6 +3097,69 @@ }, "time": "2019-04-29T12:18:02+00:00" }, + { + "name": "drupal/entityqueue", + "version": "1.0.0-beta5", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/entityqueue.git", + "reference": "8.x-1.0-beta5" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/entityqueue-8.x-1.0-beta5.zip", + "reference": "8.x-1.0-beta5", + "shasum": "b4194b275eb36cea4b77dc267c368341fc37d455" + }, + "require": { + "drupal/core": "~8.0" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + }, + "drupal": { + "version": "8.x-1.0-beta5", + "datestamp": "1559148188", + "security-coverage": { + "status": "not-covered", + "message": "Beta releases are not covered by Drupal security advisories." + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "Andrei Mateescu", + "homepage": "https://www.drupal.org/u/amateescu", + "role": "Maintainer" + }, + { + "name": "Jonathan Jordan", + "homepage": "https://www.drupal.org/u/jojonaloha", + "role": "Maintainer" + }, + { + "name": "quicksketch", + "homepage": "https://www.drupal.org/user/35821" + }, + { + "name": "tim.plunkett", + "homepage": "https://www.drupal.org/user/241634" + } + ], + "description": "The Entityqueue module allows users to create queues of any entity type.", + "homepage": "https://www.drupal.org/project/entityqueue", + "support": { + "source": "http://cgit.drupalcode.org/entityqueue", + "issues": "https://www.drupal.org/project/issues/entityqueue", + "irc": "irc://irc.freenode.org/drupal-contribute" + } + }, { "name": "drupal/fontawesome", "version": "2.14.0", @@ -3068,6 +3224,67 @@ "source": "https://git.drupalcode.org/project/fontawesome" } }, + { + "name": "drupal/pathauto", + "version": "1.6.0-alpha1", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/pathauto.git", + "reference": "8.x-1.6-alpha1" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/pathauto-8.x-1.6-alpha1.zip", + "reference": "8.x-1.6-alpha1", + "shasum": "1d4aeca37c2bb1255186be4f8d2570b7585aa4e2" + }, + "require": { + "drupal/core": "^8.6", + "drupal/ctools": "*", + "drupal/token": "*" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + }, + "drupal": { + "version": "8.x-1.6-alpha1", + "datestamp": "1571587984", + "security-coverage": { + "status": "not-covered", + "message": "Alpha releases are not covered by Drupal security advisories." + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Berdir", + "homepage": "https://www.drupal.org/user/214652" + }, + { + "name": "Dave Reid", + "homepage": "https://www.drupal.org/user/53892" + }, + { + "name": "Freso", + "homepage": "https://www.drupal.org/user/27504" + }, + { + "name": "greggles", + "homepage": "https://www.drupal.org/user/36762" + } + ], + "description": "Provides a mechanism for modules to automatically generate aliases for the content they manage.", + "homepage": "https://www.drupal.org/project/pathauto", + "support": { + "source": "https://git.drupalcode.org/project/pathauto" + } + }, { "name": "drupal/responsive_menus", "version": "dev-1.x", @@ -3282,6 +3499,187 @@ "issues": "https://drupal.org/project/issues/slick_views" } }, + { + "name": "drupal/token", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/token.git", + "reference": "8.x-1.5" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/token-8.x-1.5.zip", + "reference": "8.x-1.5", + "shasum": "6382a7e1aabbd8246f1117a26bf4916d285b401d" + }, + "require": { + "drupal/core": "^8.5" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + }, + "drupal": { + "version": "8.x-1.5", + "datestamp": "1537557481", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Berdir", + "homepage": "https://www.drupal.org/user/214652" + }, + { + "name": "Dave Reid", + "homepage": "https://www.drupal.org/user/53892" + }, + { + "name": "eaton", + "homepage": "https://www.drupal.org/user/16496" + }, + { + "name": "fago", + "homepage": "https://www.drupal.org/user/16747" + }, + { + "name": "greggles", + "homepage": "https://www.drupal.org/user/36762" + }, + { + "name": "mikeryan", + "homepage": "https://www.drupal.org/user/4420" + } + ], + "description": "Provides a user interface for the Token API and some missing core tokens.", + "homepage": "https://www.drupal.org/project/token", + "support": { + "source": "https://git.drupalcode.org/project/token" + } + }, + { + "name": "drupal/webform", + "version": "5.5.0", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/webform.git", + "reference": "8.x-5.5" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/webform-8.x-5.5.zip", + "reference": "8.x-5.5", + "shasum": "71b2b14176d0dbf9e3dd5b266cbb0e9fc7b49c26" + }, + "require": { + "drupal/core": "*" + }, + "require-dev": { + "drupal/address": "~1.4", + "drupal/bootstrap": "~3.0", + "drupal/captcha": "~1.0", + "drupal/chosen": "~2.6", + "drupal/devel": "*", + "drupal/entity_print": "^2.1", + "drupal/jsonapi": "~2.0 || ~8.7", + "drupal/mailsystem": "~4.0", + "drupal/select2": "~1.1", + "drupal/smtp": "~1.0", + "drupal/telephone_validation": "^2.2", + "drupal/token": "~1.3", + "drupal/webform_access": "*", + "drupal/webform_attachment": "*", + "drupal/webform_entity_print": "*", + "drupal/webform_node": "*", + "drupal/webform_options_limit": "*", + "drupal/webform_scheduled_email": "*", + "drupal/webform_ui": "*" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-5.x": "5.x-dev" + }, + "drupal": { + "version": "8.x-5.5", + "datestamp": "1572377284", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + }, + "drush": { + "services": { + "drush.services.yml": "^9" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "Jacob Rockowitz (jrockowitz)", + "homepage": "https://www.drupal.org/u/jrockowitz", + "role": "Maintainer" + }, + { + "name": "Alexander Trotsenko (bucefal91)", + "homepage": "https://www.drupal.org/u/bucefal91", + "role": "Co-maintainer" + }, + { + "name": "bucefal91", + "homepage": "https://www.drupal.org/user/504128" + }, + { + "name": "fenstrat", + "homepage": "https://www.drupal.org/user/362649" + }, + { + "name": "jrockowitz", + "homepage": "https://www.drupal.org/user/371407" + }, + { + "name": "podarok", + "homepage": "https://www.drupal.org/user/116002" + }, + { + "name": "quicksketch", + "homepage": "https://www.drupal.org/user/35821" + }, + { + "name": "sanchiz", + "homepage": "https://www.drupal.org/user/1671246" + }, + { + "name": "tedbow", + "homepage": "https://www.drupal.org/user/240860" + }, + { + "name": "torotil", + "homepage": "https://www.drupal.org/user/865256" + } + ], + "description": "Enables the creation of webforms and questionnaires.", + "homepage": "https://drupal.org/project/webform", + "support": { + "source": "http://cgit.drupalcode.org/webform", + "issues": "https://www.drupal.org/project/issues/webform?version=8.x", + "docs": "https://www.drupal.org/docs/8/modules/webform", + "forum": "https://drupal.stackexchange.com/questions/tagged/webform" + } + }, { "name": "drush/drush", "version": "9.7.1", @@ -4776,7 +5174,7 @@ }, { "name": "symfony/class-loader", - "version": "v3.4.31", + "version": "v3.4.32", "source": { "type": "git", "url": "https://github.com/symfony/class-loader.git", @@ -4832,16 +5230,16 @@ }, { "name": "symfony/config", - "version": "v3.4.31", + "version": "v3.4.32", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "24a60c0d7ad98a0fa5d1f892e9286095a389404f" + "reference": "717ad66b5257e9752ae3c5722b5810bb4c40b236" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/24a60c0d7ad98a0fa5d1f892e9286095a389404f", - "reference": "24a60c0d7ad98a0fa5d1f892e9286095a389404f", + "url": "https://api.github.com/repos/symfony/config/zipball/717ad66b5257e9752ae3c5722b5810bb4c40b236", + "reference": "717ad66b5257e9752ae3c5722b5810bb4c40b236", "shasum": "" }, "require": { @@ -4892,20 +5290,20 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2019-08-26T07:52:57+00:00" + "time": "2019-09-19T15:32:51+00:00" }, { "name": "symfony/console", - "version": "v3.4.31", + "version": "v3.4.32", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "4510f04e70344d70952566e4262a0b11df39cb10" + "reference": "4727d7f3c99b9dea0ae70ed4f34645728aa90453" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/4510f04e70344d70952566e4262a0b11df39cb10", - "reference": "4510f04e70344d70952566e4262a0b11df39cb10", + "url": "https://api.github.com/repos/symfony/console/zipball/4727d7f3c99b9dea0ae70ed4f34645728aa90453", + "reference": "4727d7f3c99b9dea0ae70ed4f34645728aa90453", "shasum": "" }, "require": { @@ -4964,20 +5362,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2019-08-26T07:52:58+00:00" + "time": "2019-10-06T19:52:09+00:00" }, { "name": "symfony/css-selector", - "version": "v3.4.31", + "version": "v3.4.32", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "e18c5c4b35e7f17513448a25d02f7af34a4bdb41" + "reference": "f819f71ae3ba6f396b4c015bd5895de7d2f1f85f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/e18c5c4b35e7f17513448a25d02f7af34a4bdb41", - "reference": "e18c5c4b35e7f17513448a25d02f7af34a4bdb41", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/f819f71ae3ba6f396b4c015bd5895de7d2f1f85f", + "reference": "f819f71ae3ba6f396b4c015bd5895de7d2f1f85f", "shasum": "" }, "require": { @@ -5017,20 +5415,20 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2019-08-20T13:31:17+00:00" + "time": "2019-10-01T11:57:37+00:00" }, { "name": "symfony/debug", - "version": "v3.4.31", + "version": "v3.4.32", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "0b600300918780001e2821db77bc28b677794486" + "reference": "b3e7ce815d82196435d16dc458023f8fb6b36ceb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/0b600300918780001e2821db77bc28b677794486", - "reference": "0b600300918780001e2821db77bc28b677794486", + "url": "https://api.github.com/repos/symfony/debug/zipball/b3e7ce815d82196435d16dc458023f8fb6b36ceb", + "reference": "b3e7ce815d82196435d16dc458023f8fb6b36ceb", "shasum": "" }, "require": { @@ -5073,20 +5471,20 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2019-08-20T13:31:17+00:00" + "time": "2019-09-19T15:32:51+00:00" }, { "name": "symfony/dependency-injection", - "version": "v3.4.31", + "version": "v3.4.32", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "2709bc2978ceb90f5180181f777f8a09125f2d89" + "reference": "9cf81798f857205c5bbb4c8c7895f838d40b0c4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/2709bc2978ceb90f5180181f777f8a09125f2d89", - "reference": "2709bc2978ceb90f5180181f777f8a09125f2d89", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/9cf81798f857205c5bbb4c8c7895f838d40b0c4b", + "reference": "9cf81798f857205c5bbb4c8c7895f838d40b0c4b", "shasum": "" }, "require": { @@ -5144,20 +5542,20 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2019-08-26T16:07:57+00:00" + "time": "2019-09-27T15:47:48+00:00" }, { "name": "symfony/dom-crawler", - "version": "v3.4.31", + "version": "v3.4.32", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "8558d1bc4554f5cb0b66e50377457967a8969263" + "reference": "29cffc38a38f2a8ed7e494c9cea2f890a40c2359" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/8558d1bc4554f5cb0b66e50377457967a8969263", - "reference": "8558d1bc4554f5cb0b66e50377457967a8969263", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/29cffc38a38f2a8ed7e494c9cea2f890a40c2359", + "reference": "29cffc38a38f2a8ed7e494c9cea2f890a40c2359", "shasum": "" }, "require": { @@ -5201,11 +5599,11 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2019-08-26T07:52:58+00:00" + "time": "2019-08-30T17:42:32+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v3.4.31", + "version": "v3.4.32", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", @@ -5268,7 +5666,7 @@ }, { "name": "symfony/filesystem", - "version": "v3.4.31", + "version": "v3.4.32", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", @@ -5318,16 +5716,16 @@ }, { "name": "symfony/finder", - "version": "v3.4.31", + "version": "v3.4.32", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "1fcad80b440abcd1451767349906b6f9d3961d37" + "reference": "2b6a666d6ff7fb65d10b97d817c8e7930944afb9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/1fcad80b440abcd1451767349906b6f9d3961d37", - "reference": "1fcad80b440abcd1451767349906b6f9d3961d37", + "url": "https://api.github.com/repos/symfony/finder/zipball/2b6a666d6ff7fb65d10b97d817c8e7930944afb9", + "reference": "2b6a666d6ff7fb65d10b97d817c8e7930944afb9", "shasum": "" }, "require": { @@ -5363,20 +5761,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2019-08-14T09:39:58+00:00" + "time": "2019-09-01T21:32:23+00:00" }, { "name": "symfony/http-foundation", - "version": "v3.4.31", + "version": "v3.4.32", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "b3d57a1c325f39f703b249bed7998ce8c64236b4" + "reference": "233f40cbebd595ffd91ddf291355f8a930a13777" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/b3d57a1c325f39f703b249bed7998ce8c64236b4", - "reference": "b3d57a1c325f39f703b249bed7998ce8c64236b4", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/233f40cbebd595ffd91ddf291355f8a930a13777", + "reference": "233f40cbebd595ffd91ddf291355f8a930a13777", "shasum": "" }, "require": { @@ -5417,20 +5815,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2019-08-26T07:50:50+00:00" + "time": "2019-10-02T16:15:21+00:00" }, { "name": "symfony/http-kernel", - "version": "v3.4.31", + "version": "v3.4.32", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "f6d35bb306b26812df007525f5757a8b0e95857e" + "reference": "1103850c7f34bf9c0bf8c0e6e9aab9b1f2308f01" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/f6d35bb306b26812df007525f5757a8b0e95857e", - "reference": "f6d35bb306b26812df007525f5757a8b0e95857e", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/1103850c7f34bf9c0bf8c0e6e9aab9b1f2308f01", + "reference": "1103850c7f34bf9c0bf8c0e6e9aab9b1f2308f01", "shasum": "" }, "require": { @@ -5506,7 +5904,7 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2019-08-26T16:36:29+00:00" + "time": "2019-10-07T14:41:56+00:00" }, { "name": "symfony/polyfill-ctype", @@ -5800,16 +6198,16 @@ }, { "name": "symfony/process", - "version": "v3.4.31", + "version": "v3.4.32", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d822cb654000a95b7855362c0d5b127f6a6d8baa" + "reference": "344dc588b163ff58274f1769b90b75237f32ed16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d822cb654000a95b7855362c0d5b127f6a6d8baa", - "reference": "d822cb654000a95b7855362c0d5b127f6a6d8baa", + "url": "https://api.github.com/repos/symfony/process/zipball/344dc588b163ff58274f1769b90b75237f32ed16", + "reference": "344dc588b163ff58274f1769b90b75237f32ed16", "shasum": "" }, "require": { @@ -5845,7 +6243,7 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2019-08-26T07:52:58+00:00" + "time": "2019-09-25T14:09:38+00:00" }, { "name": "symfony/psr-http-message-bridge", @@ -5914,7 +6312,7 @@ }, { "name": "symfony/routing", - "version": "v3.4.31", + "version": "v3.4.32", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", @@ -5990,16 +6388,16 @@ }, { "name": "symfony/serializer", - "version": "v3.4.31", + "version": "v3.4.32", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "758c3cb8cd4a949ee76ee457450abdc80ea82aa1" + "reference": "14e29c5977dbae8beb8f56b098b2d1a313f201eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/758c3cb8cd4a949ee76ee457450abdc80ea82aa1", - "reference": "758c3cb8cd4a949ee76ee457450abdc80ea82aa1", + "url": "https://api.github.com/repos/symfony/serializer/zipball/14e29c5977dbae8beb8f56b098b2d1a313f201eb", + "reference": "14e29c5977dbae8beb8f56b098b2d1a313f201eb", "shasum": "" }, "require": { @@ -6065,20 +6463,20 @@ ], "description": "Symfony Serializer Component", "homepage": "https://symfony.com", - "time": "2019-08-26T07:52:58+00:00" + "time": "2019-09-30T23:11:46+00:00" }, { "name": "symfony/translation", - "version": "v3.4.31", + "version": "v3.4.32", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "49a884e9ac297f99c56052bad30b2af89f716ee1" + "reference": "dd313664be0588560acacb252543b585f5408547" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/49a884e9ac297f99c56052bad30b2af89f716ee1", - "reference": "49a884e9ac297f99c56052bad30b2af89f716ee1", + "url": "https://api.github.com/repos/symfony/translation/zipball/dd313664be0588560acacb252543b585f5408547", + "reference": "dd313664be0588560acacb252543b585f5408547", "shasum": "" }, "require": { @@ -6135,20 +6533,20 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2019-08-26T07:52:58+00:00" + "time": "2019-09-27T05:57:25+00:00" }, { "name": "symfony/validator", - "version": "v3.4.31", + "version": "v3.4.32", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "4dde4e74331ffa897c31e4423d02ae08d56f7784" + "reference": "ce65fe341eb87fb34c80f9e4f12edc6472d1a74b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/4dde4e74331ffa897c31e4423d02ae08d56f7784", - "reference": "4dde4e74331ffa897c31e4423d02ae08d56f7784", + "url": "https://api.github.com/repos/symfony/validator/zipball/ce65fe341eb87fb34c80f9e4f12edc6472d1a74b", + "reference": "ce65fe341eb87fb34c80f9e4f12edc6472d1a74b", "shasum": "" }, "require": { @@ -6221,20 +6619,20 @@ ], "description": "Symfony Validator Component", "homepage": "https://symfony.com", - "time": "2019-08-26T07:52:58+00:00" + "time": "2019-10-07T09:27:57+00:00" }, { "name": "symfony/var-dumper", - "version": "v4.3.4", + "version": "v4.3.5", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "641043e0f3e615990a0f29479f9c117e8a6698c6" + "reference": "bde8957fc415fdc6964f33916a3755737744ff05" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/641043e0f3e615990a0f29479f9c117e8a6698c6", - "reference": "641043e0f3e615990a0f29479f9c117e8a6698c6", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/bde8957fc415fdc6964f33916a3755737744ff05", + "reference": "bde8957fc415fdc6964f33916a3755737744ff05", "shasum": "" }, "require": { @@ -6297,20 +6695,20 @@ "debug", "dump" ], - "time": "2019-08-26T08:26:39+00:00" + "time": "2019-10-04T19:48:13+00:00" }, { "name": "symfony/yaml", - "version": "v3.4.31", + "version": "v3.4.32", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "3dc414b7db30695bae671a1d86013d03f4ae9834" + "reference": "768f817446da74a776a31eea335540f9dcb53942" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/3dc414b7db30695bae671a1d86013d03f4ae9834", - "reference": "3dc414b7db30695bae671a1d86013d03f4ae9834", + "url": "https://api.github.com/repos/symfony/yaml/zipball/768f817446da74a776a31eea335540f9dcb53942", + "reference": "768f817446da74a776a31eea335540f9dcb53942", "shasum": "" }, "require": { @@ -6356,7 +6754,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2019-08-20T13:31:17+00:00" + "time": "2019-09-10T10:38:46+00:00" }, { "name": "twig/twig", @@ -6659,16 +7057,16 @@ }, { "name": "zaporylie/composer-drupal-optimizations", - "version": "1.1.0", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/zaporylie/composer-drupal-optimizations.git", - "reference": "173c198fd7c9aefa5e5b68cd755ee63eb0abf7e8" + "reference": "fb231d92adc862a2c9276bccbc90f684816dc75d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zaporylie/composer-drupal-optimizations/zipball/173c198fd7c9aefa5e5b68cd755ee63eb0abf7e8", - "reference": "173c198fd7c9aefa5e5b68cd755ee63eb0abf7e8", + "url": "https://api.github.com/repos/zaporylie/composer-drupal-optimizations/zipball/fb231d92adc862a2c9276bccbc90f684816dc75d", + "reference": "fb231d92adc862a2c9276bccbc90f684816dc75d", "shasum": "" }, "require": { @@ -6698,7 +7096,7 @@ } ], "description": "Composer plugin to improve composer performance for Drupal projects", - "time": "2019-02-20T10:00:17+00:00" + "time": "2019-10-02T17:01:11+00:00" }, { "name": "zendframework/zend-diactoros", @@ -7299,16 +7697,16 @@ }, { "name": "instaclick/php-webdriver", - "version": "1.4.5", + "version": "1.4.6", "source": { "type": "git", "url": "https://github.com/instaclick/php-webdriver.git", - "reference": "6fa959452e774dcaed543faad3a9d1a37d803327" + "reference": "bd9405077ca04129a73059a06873bedb5e138402" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/instaclick/php-webdriver/zipball/6fa959452e774dcaed543faad3a9d1a37d803327", - "reference": "6fa959452e774dcaed543faad3a9d1a37d803327", + "url": "https://api.github.com/repos/instaclick/php-webdriver/zipball/bd9405077ca04129a73059a06873bedb5e138402", + "reference": "bd9405077ca04129a73059a06873bedb5e138402", "shasum": "" }, "require": { @@ -7354,7 +7752,7 @@ "webdriver", "webtest" ], - "time": "2017-06-30T04:02:48+00:00" + "time": "2019-09-23T15:50:44+00:00" }, { "name": "jcalderonzumba/gastonjs", @@ -7885,22 +8283,22 @@ }, { "name": "phpspec/prophecy", - "version": "1.8.1", + "version": "1.9.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76" + "reference": "f6811d96d97bdf400077a0cc100ae56aa32b9203" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/1927e75f4ed19131ec9bcc3b002e07fb1173ee76", - "reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/f6811d96d97bdf400077a0cc100ae56aa32b9203", + "reference": "f6811d96d97bdf400077a0cc100ae56aa32b9203", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", "sebastian/comparator": "^1.1|^2.0|^3.0", "sebastian/recursion-context": "^1.0|^2.0|^3.0" }, @@ -7944,7 +8342,7 @@ "spy", "stub" ], - "time": "2019-06-13T12:50:23+00:00" + "time": "2019-10-03T11:07:50+00:00" }, { "name": "phpunit/php-code-coverage", @@ -8900,16 +9298,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.4.2", + "version": "3.5.0", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8" + "reference": "0afebf16a2e7f1e434920fa976253576151effe9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8", - "reference": "b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/0afebf16a2e7f1e434920fa976253576151effe9", + "reference": "0afebf16a2e7f1e434920fa976253576151effe9", "shasum": "" }, "require": { @@ -8947,20 +9345,20 @@ "phpcs", "standards" ], - "time": "2019-04-10T23:49:02+00:00" + "time": "2019-09-26T23:12:26+00:00" }, { "name": "symfony/browser-kit", - "version": "v4.3.4", + "version": "v4.3.5", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "9e5dddb637b13db82e35695a8603fe6e118cc119" + "reference": "78b7611c45039e8ce81698be319851529bf040b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/9e5dddb637b13db82e35695a8603fe6e118cc119", - "reference": "9e5dddb637b13db82e35695a8603fe6e118cc119", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/78b7611c45039e8ce81698be319851529bf040b1", + "reference": "78b7611c45039e8ce81698be319851529bf040b1", "shasum": "" }, "require": { @@ -9006,20 +9404,20 @@ ], "description": "Symfony BrowserKit Component", "homepage": "https://symfony.com", - "time": "2019-08-26T08:26:39+00:00" + "time": "2019-09-10T11:25:17+00:00" }, { "name": "symfony/phpunit-bridge", - "version": "v3.4.31", + "version": "v3.4.32", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "028617b04ae19d99d89089626ac969d161244ebc" + "reference": "cbea8818e9f34e4e9d780bd22bdda21b57d4d5c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/028617b04ae19d99d89089626ac969d161244ebc", - "reference": "028617b04ae19d99d89089626ac969d161244ebc", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/cbea8818e9f34e4e9d780bd22bdda21b57d4d5c7", + "reference": "cbea8818e9f34e4e9d780bd22bdda21b57d4d5c7", "shasum": "" }, "require": { @@ -9071,7 +9469,7 @@ ], "description": "Symfony PHPUnit Bridge", "homepage": "https://symfony.com", - "time": "2019-08-20T13:31:17+00:00" + "time": "2019-09-30T20:33:19+00:00" }, { "name": "theseer/tokenizer", @@ -9115,16 +9513,16 @@ }, { "name": "webflo/drupal-core-require-dev", - "version": "8.7.7", + "version": "8.7.8", "source": { "type": "git", "url": "https://github.com/webflo/drupal-core-require-dev.git", - "reference": "f535c939fee065b202ed5a84b15aa5385ba49d5e" + "reference": "593123300b3cf9974fa0742282af90fa72ac06fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webflo/drupal-core-require-dev/zipball/f535c939fee065b202ed5a84b15aa5385ba49d5e", - "reference": "f535c939fee065b202ed5a84b15aa5385ba49d5e", + "url": "https://api.github.com/repos/webflo/drupal-core-require-dev/zipball/593123300b3cf9974fa0742282af90fa72ac06fc", + "reference": "593123300b3cf9974fa0742282af90fa72ac06fc", "shasum": "" }, "require": { @@ -9132,7 +9530,7 @@ "behat/mink-goutte-driver": "^1.2", "behat/mink-selenium2-driver": "1.3.x-dev", "drupal/coder": "^8.3.1", - "drupal/core": "8.7.7", + "drupal/core": "8.7.8", "jcalderonzumba/gastonjs": "^1.0.2", "jcalderonzumba/mink-phantomjs-driver": "^0.3.1", "justinrainbow/json-schema": "^5.2", @@ -9149,7 +9547,7 @@ "GPL-2.0-or-later" ], "description": "require-dev dependencies from drupal/core", - "time": "2019-09-04T10:31:38+00:00" + "time": "2019-10-02T19:31:42+00:00" } ], "aliases": [], diff --git a/modules/backup_migrate/.gitignore b/modules/backup_migrate/.gitignore new file mode 100644 index 0000000..2659611 --- /dev/null +++ b/modules/backup_migrate/.gitignore @@ -0,0 +1 @@ +composer.lock diff --git a/modules/backup_migrate/LICENSE.txt b/modules/backup_migrate/LICENSE.txt new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/modules/backup_migrate/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/modules/backup_migrate/README.md b/modules/backup_migrate/README.md new file mode 100644 index 0000000..76c5e72 --- /dev/null +++ b/modules/backup_migrate/README.md @@ -0,0 +1,23 @@ +# Backup and Migrate for Drupal 8 + +The Drupal 8 rebuild of Backup and Migrate (WIP) + +## Installation + +This module uses composer to manage dependencies. To install from this repository: + +Clone the repository into your Drupal site modules directory: + +`git clone git@github.com:backupmigrate/backup_migrate_drupal.git /path/to/site/modules/backup_migrate` + +Change to the module directory: + +`cd /path/to/site/modules/backup_migrate` + +Install using composer + +`composer install` + +Install the module as usual using Drush or the Drupal UI. + +For more information on using composer see: https://getcomposer.org/ diff --git a/modules/backup_migrate/backup_migrate.info.yml b/modules/backup_migrate/backup_migrate.info.yml new file mode 100644 index 0000000..371efdb --- /dev/null +++ b/modules/backup_migrate/backup_migrate.info.yml @@ -0,0 +1,12 @@ +name: 'Backup and Migrate' +description: 'Backup the Drupal database and files or migrate them to another environment.' +package: Other +type: module +# core: 8.x +configure: backup_migrate.quick_backup + +# Information added by Drupal.org packaging script on 2018-03-29 +version: '8.x-4.0' +core: '8.x' +project: 'backup_migrate' +datestamp: 1522330988 diff --git a/modules/backup_migrate/backup_migrate.links.action.yml b/modules/backup_migrate/backup_migrate.links.action.yml new file mode 100644 index 0000000..60a4950 --- /dev/null +++ b/modules/backup_migrate/backup_migrate.links.action.yml @@ -0,0 +1,23 @@ +entity.backup_migrate_schedule.add: + route_name: 'entity.backup_migrate_schedule.add' + title: 'Add Schedule' + appears_on: + - entity.backup_migrate_schedule.collection + +entity.backup_migrate_settings.add_form: + route_name: 'entity.backup_migrate_settings.add' + title: 'Add Settings Profile' + appears_on: + - entity.backup_migrate_settings.collection + +entity.backup_migrate_destination.add_form: + route_name: 'entity.backup_migrate_destination.add_form' + title: 'Add Backup Destination' + appears_on: + - entity.backup_migrate_destination.collection + +entity.backup_migrate_source.add_form: + route_name: 'entity.backup_migrate_source.add_form' + title: 'Add Backup Source' + appears_on: + - entity.backup_migrate_source.collection diff --git a/modules/backup_migrate/backup_migrate.links.menu.yml b/modules/backup_migrate/backup_migrate.links.menu.yml new file mode 100644 index 0000000..1ba9d54 --- /dev/null +++ b/modules/backup_migrate/backup_migrate.links.menu.yml @@ -0,0 +1,5 @@ +backup_migrate.quick_backup: + title: Backup and Migrate + description: '' + parent: system.admin_config_development + route_name: backup_migrate.quick_backup diff --git a/modules/backup_migrate/backup_migrate.links.task.yml b/modules/backup_migrate/backup_migrate.links.task.yml new file mode 100644 index 0000000..b7b054c --- /dev/null +++ b/modules/backup_migrate/backup_migrate.links.task.yml @@ -0,0 +1,54 @@ +backup_migrate.quick_backup: + title: Backup + route_name: backup_migrate.quick_backup + base_route: backup_migrate.quick_backup + +backup_migrate.restore: + title: Restore + route_name: backup_migrate.restore + base_route: backup_migrate.quick_backup + +backup_migrate.backups: + title: Saved Backups + route_name: backup_migrate.backups + base_route: backup_migrate.quick_backup + +backup_migrate.quick_backup_sub: + title: Quick Backup + route_name: backup_migrate.quick_backup + parent_id: backup_migrate.quick_backup + +backup_migrate.advanced_backup: + title: Advanced Backup + route_name: backup_migrate.advanced_backup + parent_id: backup_migrate.quick_backup + +backup_migrate.schedule: + title: Schedules + route_name: entity.backup_migrate_schedule.collection + base_route: backup_migrate.quick_backup + +backup_migrate.settings: + title: Settings + route_name: entity.backup_migrate_settings.collection + base_route: backup_migrate.quick_backup + +backup_migrate.settings_profiles: + title: Settings Profiles + route_name: entity.backup_migrate_settings.collection + base_route: backup_migrate.quick_backup + parent_id: backup_migrate.settings + + +backup_migrate.destination: + title: Destinations + route_name: entity.backup_migrate_destination.collection + base_route: backup_migrate.quick_backup + parent_id: backup_migrate.settings + + +backup_migrate.source: + title: Sources + route_name: entity.backup_migrate_source.collection + base_route: backup_migrate.quick_backup + parent_id: backup_migrate.settings diff --git a/modules/backup_migrate/backup_migrate.module b/modules/backup_migrate/backup_migrate.module new file mode 100644 index 0000000..91b2ed5 --- /dev/null +++ b/modules/backup_migrate/backup_migrate.module @@ -0,0 +1,202 @@ +addPrefix('BackupMigrate\\Core\\', __DIR__ . '/lib/backup_migrate_core/src'); +$loader->addPrefix('BackupMigrate\\Drupal\\', __DIR__ . '/src'); +$loader->register(); + +define('BACKUP_MIGRATE_MODULE_VERSION', '8.x-4.x-dev'); + +/** + * Back up a source to 1 or more destinations. + * + * @param string $source_id + * @param string|array $destination_id + * @param array $config + */ +function backup_migrate_perform_backup($source_id, $destination_id, $config = []) { + try { + // Retrieve the service. + $bam = backup_migrate_get_service_object($config); + + // Run the backup. + $bam->backup($source_id, $destination_id); + drupal_set_message(t('Backup Complete.')); + } + catch (Exception $e) { + drupal_set_message($e->getMessage(), 'error'); + } +} + +/** + * Restore a source from a destination and file id. + * + * @param string $source_id + * @param string|array $destination_id + * @param string|null $file_id + * @param array $config + * @param string $file_id + */ +function backup_migrate_perform_restore($source_id, $destination_id, $file_id = NULL, $config = []) { + try { + // Retrieve the service. + $bam = backup_migrate_get_service_object($config); + + // Run the backup. + $bam->restore($source_id, $destination_id, $file_id); + drupal_set_message(t('Restore Complete.')); + } + catch (Exception $e) { + drupal_set_message($e->getMessage(), 'error'); + return; + } +} + +/** + * Get a BackupMigrate service object. + * + * @param array $config_array + * An array of configuration arrays, keyed by plugin id. + * @param array $options + * A keyed array of options. + * + * @return \BackupMigrate\Core\Main\BackupMigrate + */ +function backup_migrate_get_service_object($config_array = [], $options = []) { + static $bam = NULL; + + // If the static cached object has not been loaded. + if ($bam === NULL) { + // Create the service object. + $bam = new \BackupMigrate\Core\Main\BackupMigrate(); + + // Allow other modules to alter the object. + \Drupal::moduleHandler()->alter('backup_migrate_service_object', $bam, $options); + } + + // Set the configuration overrides if any were passed in. + if ($config_array) { + $bam->setConfig(new Config($config_array)); + } + + return $bam; +} + +/** + * Implements hook_backup_migrate_service_object_alter(). + * + * Add the core Backup and Migrate plugins to the service object. + * + * @param \BackupMigrate\Core\Main\BackupMigrateInterface $bam + * @param array $options + */ +function backup_migrate_backup_migrate_service_object_alter(BackupMigrateInterface &$bam, $options = []) { + $sources = $bam->sources(); + $destinations = $bam->destinations(); + $plugins = $bam->plugins(); + + $services = $bam->services(); + + // Add a temp file manager which can access the drupal temp directory. + $services->add('TempFileAdapter', + new \BackupMigrate\Drupal\File\DrupalTempFileAdapter(\Drupal::service('file_system'), 'temporary://', 'bam') + ); + + $services->add('TempFileManager', + new \BackupMigrate\Core\File\TempFileManager($services->get('TempFileAdapter')) + ); + + // Add a logger which prints everything to the browser. + $services->add('Logger', + new \BackupMigrate\Drupal\Environment\DrupalSetMessageLogger() + ); + + $services->add('ArchiveReader', new \BackupMigrate\Core\Service\TarArchiveReader()); + $services->add('ArchiveWriter', new \BackupMigrate\Core\Service\TarArchiveWriter()); + + // If this is a nobrowser op (cron) then do not add the browser plugins. + // TODO: Make this better. + if (empty($options['nobrowser'])) { + // Add a download destination. + $user = \Drupal::currentUser(); + if ($user->hasPermission('access backup files')) { + $destinations->add('download', new \BackupMigrate\Drupal\Destination\DrupalBrowserDownloadDestination(new Config(['name' => t('Download')]))); + } + // Add an upload destination. + $destinations->add('upload', new \BackupMigrate\Drupal\Destination\DrupalBrowserUploadDestination(new Config(['name' => t('Upload')]))); + } + + // Add a file naming filter. + $plugins->add('namer', new \BackupMigrate\Core\Filter\FileNamer()); + + // Add a compression filter. + $plugins->add('compressor', new \BackupMigrate\Core\Filter\CompressionFilter()); + + // Add the Drupal utilities filter. + $plugins->add('utils', new \BackupMigrate\Drupal\Filter\DrupalUtils()); + + // Add a file metadata filter. + $plugins->add('metadata', new \BackupMigrate\Core\Filter\MetadataWriter( + new Config([ + 'generator' => 'Backup and Migrate for Drupal (https://www.drupal.org/project/backup_migrate)', + 'generatorurl' => 'https://www.drupal.org/project/backup_migrate', + 'generatorversion' => BACKUP_MIGRATE_MODULE_VERSION + ]) + )); + + // Add the custom configured sources. + foreach (Source::loadMultiple() as $source) { + $source->getPlugin()->alterBackupMigrate($bam, $source->get('id'), $options); + } + + // Add the custom configured destination. + foreach (Destination::loadMultiple() as $destination) { + $destination->getPlugin()->alterBackupMigrate($bam, $destination->get('id'), $options); + } +} + +/** + * Implements hook_cron(). + * + * Runs all of the enabled schedules if they are due to be run.. + */ +function backup_migrate_cron() { + $bam = backup_migrate_get_service_object([], ['nobrowser' => TRUE]); + + $schedules = Schedule::loadMultiple(); + foreach ($schedules as $schedule) { + $schedule->run($bam); + } +} + +/** + * Implements hook_form_alter(). + */ +function backup_migrate_form_alter(&$form, FormStateInterface $form_state, $form_id) { + // Label the items being deleted on uninstall to make the 'entire site' listing less terrifying. + if ($form_id === 'system_modules_uninstall_confirm_form') { + if (isset($form['entity_deletes']['backup_migrate_source'])) { + $form['text']['#markup'] .= '

' . t('Uninstalling Backup and Migrate will delete any custom Backup and Migrate configuration. Previously created backups will not be deleted.') . '

'; + } + if (isset($form['entity_deletes']['backup_migrate_source']['#items']['entire_site'])) { + $form['entity_deletes']['backup_migrate_source']['#items']['entire_site'] = t('Full Site Backup Source'); + } + } +} diff --git a/modules/backup_migrate/backup_migrate.permissions.yml b/modules/backup_migrate/backup_migrate.permissions.yml new file mode 100644 index 0000000..eac6528 --- /dev/null +++ b/modules/backup_migrate/backup_migrate.permissions.yml @@ -0,0 +1,19 @@ +'perform backup': + 'title': 'Perform a backup' + 'description': 'Back up any of the available sources.' + restrict access: true + +'access backup files': + 'title': 'Access backup files' + 'description': 'Access and download the previously created backup files.' + restrict access: true + +'restore from backup': + 'title': 'Restore the site' + 'description': 'Restore the site''s database from a backup file.' + restrict access: true + +'administer backup and migrate': + 'title': 'Administer Backup and Migrate' + 'description': 'Edit Backup and Migrate profiles, schedules and destinations.' + restrict access: true diff --git a/modules/backup_migrate/backup_migrate.routing.yml b/modules/backup_migrate/backup_migrate.routing.yml new file mode 100644 index 0000000..70d1c6e --- /dev/null +++ b/modules/backup_migrate/backup_migrate.routing.yml @@ -0,0 +1,215 @@ +backup_migrate.quick_backup: + path: '/admin/config/development/backup_migrate' + defaults: + _form: '\Drupal\backup_migrate\Form\BackupMigrateQuickBackupForm' + _title: 'Backup and Migrate' + requirements: + _permission: 'perform backup' + +backup_migrate.advanced_backup: + path: '/admin/config/development/backup_migrate/advanced' + defaults: + _form: '\Drupal\backup_migrate\Form\BackupMigrateAdvancedBackupForm' + _title: 'Advanced Backup' + requirements: + _permission: 'perform backup' + +backup_migrate.restore: + path: '/admin/config/development/backup_migrate/restore' + defaults: + _form: '\Drupal\backup_migrate\Form\BackupMigrateRestoreForm' + _title: 'Restore' + requirements: + _permission: 'restore from backup' + + +# Backups +backup_migrate.backups: + path: '/admin/config/development/backup_migrate/backups' + defaults: + _controller: '\Drupal\backup_migrate\Controller\BackupController::listAll' + _title: 'Backups' + requirements: + _permission: 'access backup files' + + +# Schedule +entity.backup_migrate_schedule.collection: + path: '/admin/config/development/backup_migrate/schedule' + defaults: + _entity_list: 'backup_migrate_schedule' + _title: 'Schedule' + requirements: + _permission: 'administer backup and migrate' + +entity.backup_migrate_schedule.add: + path: '/admin/config/development/backup_migrate/schedule/add' + defaults: + _entity_form: backup_migrate_schedule.default + _title: 'Add schedule' + requirements: + _permission: 'administer backup and migrate' + +entity.backup_migrate_schedule.edit_form: + path: '/admin/config/development/backup_migrate/schedule/edit/{backup_migrate_schedule}' + defaults: + _entity_form: backup_migrate_schedule.default + _title: 'Edit schedule' + requirements: + _permission: 'administer backup and migrate' + +entity.backup_migrate_schedule.delete_form: + path: '/admin/config/development/backup_migrate/schedule/delete/{backup_migrate_schedule}' + defaults: + _entity_form: backup_migrate_schedule.delete + _title: 'Delete schedule' + requirements: + _permission: 'administer backup and migrate' + + +# Settings Profile +entity.backup_migrate_settings.collection: + path: '/admin/config/development/backup_migrate/settings' + defaults: + _entity_list: 'backup_migrate_settings' + _title: 'Settings' + requirements: + _permission: 'administer backup and migrate' + +entity.backup_migrate_settings.add: + path: '/admin/config/development/backup_migrate/settings/add' + defaults: + _entity_form: backup_migrate_settings.default + _title: 'Add settings profile' + requirements: + _permission: 'administer backup and migrate' + +entity.backup_migrate_settings.edit_form: + path: '/admin/config/development/backup_migrate/settings/edit/{backup_migrate_settings}' + defaults: + _entity_form: backup_migrate_settings.default + _title: 'Edit settings profile' + requirements: + _permission: 'administer backup and migrate' + +entity.backup_migrate_settings.delete_form: + path: '/admin/config/development/backup_migrate/settings/delete/{backup_migrate_settings}' + defaults: + _entity_form: backup_migrate_settings.delete + _title: 'Delete settings profile' + requirements: + _permission: 'administer backup and migrate' + + +# Backup Destination +entity.backup_migrate_destination.collection: + path: '/admin/config/development/backup_migrate/settings/destination' + defaults: + _entity_list: 'backup_migrate_destination' + _title: 'Backup Destination' + requirements: + _permission: 'administer backup and migrate' + +entity.backup_migrate_destination.add_form: + path: '/admin/config/development/backup_migrate/settings/destination/add' + defaults: + _entity_form: backup_migrate_destination.default + _title: 'Add destination' + requirements: + _permission: 'administer backup and migrate' + +entity.backup_migrate_destination.edit_form: + path: '/admin/config/development/backup_migrate/settings/destination/edit/{backup_migrate_destination}' + defaults: + _entity_form: backup_migrate_destination.default + _title: 'Edit destination' + requirements: + _permission: 'administer backup and migrate' + +entity.backup_migrate_destination.delete_form: + path: '/admin/config/development/backup_migrate/settings/destination/delete/{backup_migrate_destination}' + defaults: + _entity_form: backup_migrate_destination.delete + _title: 'Delete destination' + requirements: + _permission: 'administer backup and migrate' + +entity.backup_migrate_destination.backups: + path: '/admin/config/development/backup_migrate/settings/destination/backups/{backup_migrate_destination}' + defaults: + _controller: '\Drupal\backup_migrate\Controller\BackupController::listDestinationEntityBackups' + _title_callback: '\Drupal\backup_migrate\Controller\BackupController::listDestinationEntityBackupsTitle' + requirements: + _permission: 'administer backup and migrate' + options: + parameters: + backup_migrate_destination: + type: entity:backup_migrate_destination + +entity.backup_migrate_destination.backup_delete: + path: '/admin/config/development/backup_migrate/settings/destination/backups/{backup_migrate_destination}/delete/{backup_id}' + defaults: + _form: '\Drupal\backup_migrate\Form\BackupDeleteForm' + requirements: + _permission: 'administer backup and migrate' + options: + parameters: + backup_migrate_destination: + type: entity:backup_migrate_destination + +entity.backup_migrate_destination.backup_download: + path: '/admin/config/development/backup_migrate/settings/destination/backups/{backup_migrate_destination}/download/{backup_id}' + defaults: + _controller: '\Drupal\backup_migrate\Controller\BackupController::download' + _title: 'Downlod Backup' + requirements: + _permission: 'administer backup and migrate' + options: + parameters: + backup_migrate_destination: + type: entity:backup_migrate_destination + +entity.backup_migrate_destination.backup_restore: + path: '/admin/config/development/backup_migrate/settings/destination/backups/{backup_migrate_destination}/restore/{backup_id}' + defaults: + _form: '\Drupal\backup_migrate\Form\BackupRestoreForm' + requirements: + _permission: 'administer backup and migrate' + options: + parameters: + backup_migrate_destination: + type: entity:backup_migrate_destination + + +# Backup Source +entity.backup_migrate_source.collection: + path: '/admin/config/development/backup_migrate/settings/source' + defaults: + _entity_list: 'backup_migrate_source' + _title: 'Backup sources' + requirements: + _permission: 'administer backup and migrate' + +entity.backup_migrate_source.add_form: + path: '/admin/config/development/backup_migrate/settings/source/add' + defaults: + _entity_form: backup_migrate_source.default + _title: 'Add Backup Source' + requirements: + _permission: 'administer backup and migrate' + +entity.backup_migrate_source.edit_form: + path: '/admin/config/development/backup_migrate/settings/source/edit/{backup_migrate_source}' + defaults: + _entity_form: backup_migrate_source.default + _title: 'Edit source' + requirements: + _permission: 'administer backup and migrate' + +entity.backup_migrate_source.delete_form: + path: '/admin/config/development/backup_migrate/settings/source/delete/{backup_migrate_source}' + defaults: + _entity_form: backup_migrate_source.delete + _title: 'Delete source' + requirements: + _permission: 'administer backup and migrate' diff --git a/modules/backup_migrate/backup_migrate.services.yml b/modules/backup_migrate/backup_migrate.services.yml new file mode 100644 index 0000000..f0ab973 --- /dev/null +++ b/modules/backup_migrate/backup_migrate.services.yml @@ -0,0 +1,7 @@ +services: + plugin.manager.backup_migrate_source: + class: BackupMigrate\Drupal\EntityPlugins\SourcePluginManager + parent: default_plugin_manager + plugin.manager.backup_migrate_destination: + class: BackupMigrate\Drupal\EntityPlugins\DestinationPluginManager + parent: default_plugin_manager diff --git a/modules/backup_migrate/composer.json b/modules/backup_migrate/composer.json new file mode 100644 index 0000000..6ed4d33 --- /dev/null +++ b/modules/backup_migrate/composer.json @@ -0,0 +1,19 @@ +{ + "name": "drupal/backup_migrate", + "description": "Backup and Migrate Drupal Module", + "type": "drupal-module", + "license": "GPL-2.0+", + "homepage": "https://www.drupal.org/project/backup_migrate", + "minimum-stability": "dev", + "authors": [ + { + "name": "Ronan Dowling", + "homepage": "https://www.drupal.org/u/ronan", + "role": "Maintainer" + } + ], + "support": { + "issues": "https://www.drupal.org/project/issues/backup_migrate", + "source": "http://cgit.drupalcode.org/backup_migrate" + } +} diff --git a/modules/backup_migrate/config/install/backup_migrate.backup_migrate_destination.private_files.yml b/modules/backup_migrate/config/install/backup_migrate.backup_migrate_destination.private_files.yml new file mode 100644 index 0000000..e697055 --- /dev/null +++ b/modules/backup_migrate/config/install/backup_migrate.backup_migrate_destination.private_files.yml @@ -0,0 +1,8 @@ +langcode: en +status: true +dependencies: { } +id: private_files +label: 'Private Files Directory' +type: Directory +config: + directory: 'private://backup_migrate/' diff --git a/modules/backup_migrate/config/install/backup_migrate.backup_migrate_schedule.daily.yml b/modules/backup_migrate/config/install/backup_migrate.backup_migrate_schedule.daily.yml new file mode 100644 index 0000000..e142a4a --- /dev/null +++ b/modules/backup_migrate/config/install/backup_migrate.backup_migrate_schedule.daily.yml @@ -0,0 +1,2 @@ +id: daily_schedule +label: Daily Schedule diff --git a/modules/backup_migrate/config/install/backup_migrate.backup_migrate_source.default_db.yml b/modules/backup_migrate/config/install/backup_migrate.backup_migrate_source.default_db.yml new file mode 100644 index 0000000..ad00bef --- /dev/null +++ b/modules/backup_migrate/config/install/backup_migrate.backup_migrate_source.default_db.yml @@ -0,0 +1,6 @@ +langcode: en +status: true +dependencies: { } +id: default_db +label: 'Default Drupal Database' +type: DefaultDB diff --git a/modules/backup_migrate/config/install/backup_migrate.backup_migrate_source.entire_site.yml b/modules/backup_migrate/config/install/backup_migrate.backup_migrate_source.entire_site.yml new file mode 100644 index 0000000..65437ce --- /dev/null +++ b/modules/backup_migrate/config/install/backup_migrate.backup_migrate_source.entire_site.yml @@ -0,0 +1,6 @@ +langcode: en +status: true +dependencies: { } +id: entire_site +label: 'Entire Site' +type: EntireSite diff --git a/modules/backup_migrate/config/install/backup_migrate.backup_migrate_source.private_files.yml b/modules/backup_migrate/config/install/backup_migrate.backup_migrate_source.private_files.yml new file mode 100644 index 0000000..c85b03b --- /dev/null +++ b/modules/backup_migrate/config/install/backup_migrate.backup_migrate_source.private_files.yml @@ -0,0 +1,8 @@ +langcode: en +status: true +dependencies: { } +id: private_files +label: 'Private Files Directory' +type: DrupalFiles +config: + directory: 'private://' diff --git a/modules/backup_migrate/config/install/backup_migrate.backup_migrate_source.public_files.yml b/modules/backup_migrate/config/install/backup_migrate.backup_migrate_source.public_files.yml new file mode 100644 index 0000000..591e4b4 --- /dev/null +++ b/modules/backup_migrate/config/install/backup_migrate.backup_migrate_source.public_files.yml @@ -0,0 +1,8 @@ +langcode: en +status: true +dependencies: { } +id: public_files +label: 'Public Files Directory' +type: DrupalFiles +config: + directory: 'public://' diff --git a/modules/backup_migrate/config/schema/backup_migrate.schema.yml b/modules/backup_migrate/config/schema/backup_migrate.schema.yml new file mode 100644 index 0000000..9a795fa --- /dev/null +++ b/modules/backup_migrate/config/schema/backup_migrate.schema.yml @@ -0,0 +1,108 @@ +# Schema for configuration files of the Backup and Migrate module. + +backup_migrate.profile.*: + type: config_entity + label: 'Backup and Migrate settings profile' + mapping: + name: + type: string + label: + type: label + label: 'Label' + label: + type: string + label: 'Filename' + append_timestamp: + type: boolean + label: 'Append Timestamp' + timestamp_format: + type: string + label: 'Timestamp Format' + +backup_migrate.backup_migrate_schedule.*: + type: config_entity + label: 'Backup and Migrate Schedule' + mapping: + id: + type: string + label: 'ID' + label: + type: label + label: 'Schedule Name' + uuid: + type: string + enabled: + type: boolean + label: 'Enabled' + keep: + type: integer + label: 'Backups to keep' + period: + type: integer + label: 'Frequency' + cron: + type: boolean + label: 'Run on cron' + source_id: + type: string + label: 'Source' + destination_id: + type: string + label: 'Destination' + settings_profile_id: + type: string + label: 'Settings Profile' + +backup_migrate.backup_migrate_settings.*: + type: config_entity + label: 'Backup and Migrate Settings Profile' + mapping: + id: + type: string + label: 'ID' + label: + type: label + label: 'Label' + uuid: + type: string + config: + type: mapping + label: 'Configuration' + +backup_migrate.backup_migrate_destination.*: + type: config_entity + label: 'Backup Destination' + mapping: + id: + type: string + label: 'ID' + label: + type: label + label: 'Label' + uuid: + type: string + type: + type: string + label: 'Destination Type' + config: + type: mapping + label: 'Configuration' + +backup_migrate.backup_migrate_source.*: + type: config_entity + label: 'Backup Source' + mapping: + id: + type: string + label: 'ID' + label: + type: label + label: 'Label' + uuid: + type: string + type: + type: string + label: 'Source Type' + config: + type: mapping + label: 'Configuration' diff --git a/modules/backup_migrate/lib/backup_migrate_core/README.md b/modules/backup_migrate/lib/backup_migrate_core/README.md new file mode 100644 index 0000000..fe49733 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/README.md @@ -0,0 +1,129 @@ +# Backup and Migrate Core + +The core functionality for Backup and Migrate. + +Backup and Migrate Core is a PHP-based library which manages the backing up and restoring of resources such as databases and file directories. It is primarily intended for backing up content managed web sites and was originally written as [a Drupal module](https://www.drupal.org/project/backup_migrate). + +This library represents a ground up refactoring and abstraction which allows the core functionality to be used in plugins for other content management systems or for uses beyond CMS-managed websites. + +## Usage + +The following is a simplified version of how to call the library to perform a backup: + + [ + 'host' => '127.0.0.1', + 'database' => 'mydb', + 'user' => 'myuser', + 'password' => 'mypass', + 'port' => '8889', + ], + // Configure the destination. + 'mybackups' => [ + 'directory' => '~/mybackups', + ], + // Configure the compression filter. + 'compressor' => [ + 'compression' => 'gzip', + ], + // Configure the file namer. + 'name' => [ + 'filename' => 'backup', + 'timestamp' => true, + ], + ] + ); + + // Create a new Backup and Migrate object with this configuration. + $bam = new BackupMigrate(null, null, null, $config); + + // Add the database source. This will read the configuration with the same key + $bam->sources()->add('database1', new MySQLiSource()); + // Add the destination. + $bam->destinations()->add('mybackups', new DirectoryDestination()); + + // Add the filters. + $bam->plugins()->add('compression', new CompressionFilter()); + $bam->plugins()->add('name', new FileNamer()); + + // Backup from the 'database1' db to the 'mybackups' directory. + $bam->backup('databse1', 'mybackups'); + +## Reference Implementation +[Backup and Migrate CLI](https://github.com/backupmigrate/backup_migrate_cli) is a simple command-line tool which consumes the Backup and Migrate Core library. It serves as a simple reference implementation. + +## Concepts + +### Dependency Inversion +As much as possible, Backup and Migrate tries to embrace the [Dependency Inversion Principal](https://en.wikipedia.org/wiki/Dependency_inversion_principle). This means that Backup and Migrate Core relies on the consuming application to pass to it all of the pieces it needs to run. This allows the library to run in a wide variety of environments without requiring a lot of hacky internal business logic. This philosophy is balanced against the desire for a pleasant developer experience so that consuming the library does not an excess of tedious boilerplate glue code. + +### The BackupMigrate Object +This `\BackupMigrate\Core\Main\BackupMigrate` object is the main task-runner of the library. It is the primary object that a consuming application interacts with. It contains two primary operation methods: `backup()` and `restore()` which do exactly what you expect them to. The consuming application is responsible for injecting to this object the following: + +* All plugins (sources, destinations, filters) required to run. +* (Optional) The environment dependency injection container. +* (Optional) All necessary configuration. + +See: [Backup and Migrate](https://github.com/backupmigrate/backup_migrate_core/tree/master/src/Main) + +### Plugins +Plugins are the meat of the library. All of the actual work is done by plugins. Plugins come in three types: + +* **Sources** - Items which can be backed up and restored. (e.g: A MySQL database) +* **Destinations** - Places where backup files can be stored. (e.g: A directory on your server) +* **Filters** - Actions that can be performed on backup files after backup or before restore. (e.g: Gzip compression) + +While these three types of plugin are conceptually separate they are technically identical. + +See: [Plugins](https://github.com/backupmigrate/backup_migrate_core/tree/master/src/Plugin) + +##### Sources +Each backup and restore operation works on a single source. For simplicity more than one source may be added to the BackupMigrate object. The source to be backed up is identified by id when `backup()` or `restore()` is called. + +See: [Sources](https://github.com/backupmigrate/backup_migrate_core/tree/master/src/Source) + +##### Destinations +Destinations act the same way as sources. These are the places where the backup files are sent (during `backup()`) or from which they are loaded (during `restore()`). + +See: [Destinations](https://github.com/backupmigrate/backup_migrate_core/tree/master/src/Destination) + +##### Filters +Filters can alter backup files before `restore()` or after `backup()`. Unlike sources and destinations there can be many filters run per operation. + +#### Plugin Managers +A plugin manager maintains a list of injected plugins and configures them and injects services as needed. Consuming software interacts with the plugin manager by calling `plugins()` on the BackupMigrate object. This is the method used to inject plugins into the controller: + + // Create a new BackupMigrate controller. + $bam = new BackupMigrate(); + + // Add a new custom plugin with the id 'mycustomplugin' + $bam->plugins()->add('mycustomplugin', new CustomPlugin()); + +The controller also has a PluginManager for sources and one for destinations. + + // Add a source + $bam->sources()->add('source_id', new CustomSource()); + + // Add a destination + $bam->destinations()->add('destination_id', new CustomDestination()); + +### Configuration +Backup and Migrate Core has very little configuration management built in. It is the responsibility to inject the necessary configuration into the library as a `ConfigInterface` object. If no configuration object is provided then each plugin will use it's configuration defaults. + +See: [Configuration](https://github.com/backupmigrate/backup_migrate_core/tree/master/src/Config) + +### Services +Services are object that provide some global functionality such as logging or temporary file creation. Services are managed and automatically injected by the service manager. A consuming application can add services by passing them to the service manager of the `BackupMigrate` object: + + + // Create a new BackupMigrate controller. + $bam = new BackupMigrate(); + + // Add a new custom plugin with the id 'mycustomplugin' + $bam->services()->add('Logger', new MyCustomLogger()); + +See: [Configuration](https://github.com/backupmigrate/backup_migrate_core/tree/master/src/Services) diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Config/Config.php b/modules/backup_migrate/lib/backup_migrate_core/src/Config/Config.php new file mode 100644 index 0000000..bf35b80 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Config/Config.php @@ -0,0 +1,87 @@ +fromArray($init->toArray()); + } + elseif (is_array($init)) { + $this->fromArray($init); + } + } + + /** + * Get a setting value. + * + * @param string $key The key for the setting. + * @param mixed $default + * The default to return if the value does not exist. + * + * @return mixed The value of the setting. + */ + public function get($key, $default = NULL) { + return $this->keyIsSet($key) ? $this->config[$key] : $default; + } + + /** + * Set a setting value. + * + * @param string $key The key for the setting. + * @param mixed $value The value for the setting. + */ + public function set($key, $value) { + $this->config[$key] = $value; + } + + + /** + * Determine if the given key has had a value set for it. + * + * @param $key + * + * @return bool + */ + public function keyIsSet($key) { + return isset($this->config[$key]); + } + + /** + * Get all settings as an associative array. + * + * @return array All of the settings in this profile + */ + public function toArray() { + return $this->config; + } + + /** + * Set all from an array. + * + * @param array $values An associative array of settings. + */ + public function fromArray($values) { + $this->config = $values; + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Config/ConfigInterface.php b/modules/backup_migrate/lib/backup_migrate_core/src/Config/ConfigInterface.php new file mode 100644 index 0000000..c302a62 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Config/ConfigInterface.php @@ -0,0 +1,53 @@ +init = $init; + + // Set the config to a blank object to populate all values with the initial + // and default values. + $this->setConfig(new Config()); + } + + /** + * Set the configuration for all plugins. + * + * @param ConfigInterface $config + * A configuration object containing only configuration for all plugins + */ + public function setConfig(ConfigInterface $config) { + // Set the configuration object to the one passed in. + $this->config = $config; + + // Add the init/default values to the config object so they will always exist. + // @TODO: Make this cascade happen when the config key is requested. + // That will allow read-only or runtime generation config object to be passed + // This would work by creating a CascadeConfig object which takes an array + // of ConfigInterface objects and queries each in order to find the given key. + $defaults = $this->configDefaults(); + $init = $this->init; + foreach ([$init, $defaults] as $config_object) { + foreach ($config_object->toArray() as $key => $value) { + if (!$this->config->keyIsSet($key)) { + $this->config->set($key, $value); + } + } + } + } + + /** + * Get the configuration object for this item. + * + * @return \BackupMigrate\Core\Config\ConfigInterface + */ + public function config() { + return $this->config ? $this->config : new Config(); + } + + /** + * Get the default values for the plugin. + * + * @return \BackupMigrate\Core\Config\Config + */ + public function configDefaults() { + return new Config(); + } + + /** + * Get a default (blank) schema. + * + * @param array $params + * The parameters including: + * - operation - The operation being performed, will be one of: + * - 'backup': Configuration needed during a backup operation + * - 'restore': Configuration needed during a restore + * - 'initialize': Core configuration always needed by this item + * + * @return array + */ + public function configSchema($params = []) { + return []; + } + + /** + * Get any validation errors in the config. + * + * @param array $params + * + * @return array + */ + public function configErrors($params = []) { + $out = []; + + // Do some basic validation based on length and regex matching. + $schema = $this->configSchema($params); + + // Check each specified field. + foreach ($schema['fields'] as $key => $field) { + $value = $this->confGet($key); + + // Check if it's required. + if (!empty($field['required']) && empty($value)) { + $out[] = new ValidationError($key, $this->t('%title is required.'), ['%title' => $field['title']]); + } + + // Check it for length. + if (!empty($field['min_length']) && strlen($value) < $field['min_length']) { + $out[] = new ValidationError($key, $this->t('%title must be at least %count characters.'), ['%title' => $field['title'], '%count' => $field['min_length']]); + } + if (!empty($field['max_length']) && strlen($value) > $field['max_length']) { + $out[] = new ValidationError($key, $this->t('%title must be at no more than %count characters.'), ['%title' => $field['title'], '%count' => $field['max_length']]); + } + + // Check for the regular expression match. + if (!empty($field['must_match']) && !preg_match($field['must_match'], $value)) { + if (!empty($field['must_match_error'])) { + $out[] = new ValidationError($key, $field['must_match_error'], ['%title' => $field['title']]); + } + else { + $out[] = new ValidationError($key, $this->t('%title contains invalid characters.'), ['%title' => $field['title']]); + } + } + } + return $out; + } + + /** + * Get a specific value from the configuration. + * + * @param string $key The configuration object key to retrieve. + * + * @return mixed The configuration value. + */ + public function confGet($key) { + return $this->config()->get($key); + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Config/README.md b/modules/backup_migrate/lib/backup_migrate_core/src/Config/README.md new file mode 100644 index 0000000..323224b --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Config/README.md @@ -0,0 +1,76 @@ +# Configuration + +Backup and Migrate core is configured by the consuming software when the library is instantiated using a `\BackupMigrate\Core\Config\ConfigInterface` object. This object is a simple key-value store which should contain the configuration for each of the available plugins (sources, destinations and filters). Each plugin should have it's own entry in the config object which contains an array of all of the configuration for that item. The key for this entry must be the same as the key assigned to the plugin when it is added to the `BackupMigrate` object using `->plugins()->add()`. + +Any object that implements the `\BackupMigrate\Core\Config\ConfigInterface` may be used to configure Backup and Migrate. For example, a consuming application may want to implement a class that directly accesses the application's persistence layer to retrieve configuration values. In many cases, however the simple default `\BackupMigrate\Core\Config\Config` will suffice. + +## The Config Class +The built in `\BackupMigrate\Core\Config\Config` is a simple implementation of the configuration interface which can be instantiated using a PHP associative array: + + [ + 'host' => '127.0.0.1', + 'database' => 'mydb', + 'user' => 'myuser', + 'password' => 'mypass', + 'port' => '8889', + ], + // Configure the compression filter. + 'compressor' => [ + 'compression' => 'gzip', + ], + // Add more filter, source and destination configuration. + ] + ); + + $plugins = new PluginManager(); + + // Add the database source. This will read the configuration with the same key ('database1') + plugins->add( + 'database1', + new \BackupMigrate\Core\Source\MySQLiSource() + ); + // Add the compression plugin. + plugins->add( + 'compressor', + new \BackupMigrate\Core\Filter\CompressionFilter() + ); + // Add more filters and a destination. + ... + + + // Create a new Backup and Migrate object with this configuration. + $bam = new BackupMigrate($plugins); + + $bam->backup('database1', 'somedestination'); + +## Initial Config vs. Run-time Config ## + +A plugin may have two types of configuration: initial configuration, added when the plugin is created, and run-time configuration, added later by the plugin manager. Initial configuration can be overriden by run-time configuration but it cannot be overwritten by run-time config. That means that you can reconfigure plugins after the plugin manager has been created but the initial configuration will not be permanently overwriten. + +An example that illustrates the difference is a database source plugin. The database connection information should not change per operation and should be considered initial configuration. The list of tables to exclude during a backup, or whether the tables should be locked during a restore may change from run to run and should be run-time configuration. + +To specify initial configuration pass it to the plugin's constructor: + + // The db credentials are passed in to the constructor and are permanent. + $plugins->add( + 'main_database', + new MySQLiSource(new Config([ + 'database' => '...', + 'username' => '...', + ... + ]) + ); + + // Setting this configuration will not overwrite the db credentials. + $plugins->setConfig(new Config([ + 'main_database' => [ + 'exclude_tables' => [...], + ]); + diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Config/ValidationError.php b/modules/backup_migrate/lib/backup_migrate_core/src/Config/ValidationError.php new file mode 100644 index 0000000..5f95997 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Config/ValidationError.php @@ -0,0 +1,74 @@ +field_key = $field_key; + $this->message = $message; + $this->replacement = $replacement; + } + + /** + * @return string + */ + public function getMessage() { + return $this->message; + } + + /** + * @return array + */ + public function getReplacement() { + return $this->replacement; + } + + /** + * @return string + */ + public function getFieldKey() { + return $this->field_key; + } + + /** + * String representation of the exception. + * + * @return string the string representation of the exception. + */ + public function __toString() { + return strtr($this->getMessage(), $this->getReplacement()); + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Config/ValidationErrorInterface.php b/modules/backup_migrate/lib/backup_migrate_core/src/Config/ValidationErrorInterface.php new file mode 100644 index 0000000..0149418 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Config/ValidationErrorInterface.php @@ -0,0 +1,31 @@ + 'Content-Disposition', 'value' => 'attachment; filename="' . $file->getFullName() . '"'], + ['key' => 'Cache-Control', 'value' => 'no-cache'], + ]; + + // Set a mime-type header. + if ($mime = $file->getMeta('mimetype')) { + $headers[] = ['key' => 'Content-Type', 'value' => $mime]; + } + else { + // Get the mime type for this file if possible. + $mime = 'application/octet-stream'; + $mime = $this->plugins()->call('alterMime', $mime, ['ext' => $file->getExtLast()]); + + $headers[] = ['key' => 'Content-Type', 'value' => $mime]; + } + + // In some circumstances, web-servers will double compress gzipped files. + // This may help aleviate that issue by disabling mod-deflate. + if ($file->getMeta('mimetype') == 'application/x-gzip') { + if (function_exists('apache_setenv')) { + apache_setenv('no-gzip', '1'); + } + $headers[] = ['key' => 'Content-Encoding', 'value' => 'gzip']; + } + if ($size = $file->getMeta('filesize')) { + $headers[] = ['key' => 'Content-Length', 'value' => $size]; + } + + // Suppress the warning you get when the buffer is empty. + @ob_end_clean(); + + if ($file->openForRead()) { + foreach ($headers as $header) { + // To prevent HTTP header injection, we delete new lines that are + // not followed by a space or a tab. + // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 + $header['value'] = preg_replace('/\r?\n(?!\t| )/', '', $header['value']); + header($header['key'] . ': ' . $header['value']); + } + // Transfer file in 1024 byte chunks to save memory usage. + while ($data = $file->readBytes(1024 * 512)) { + print $data; + } + $file->close(); + } + // @TODO Throw exception. + } + + /** + * {@inheritdoc} + */ + public function checkWritable() { + // Check that we're running as a web process via a browser. + // @TODO: we could check if the 'HTTP_ACCEPT' header contains the right mime but that is probably overkill. + if (!isset($_SERVER['REQUEST_METHOD'])) { + throw new DestinationNotWritableException( + "The download destination only works when accessed through a http client." + ); + } + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Destination/DebugDestination.php b/modules/backup_migrate/lib/backup_migrate_core/src/Destination/DebugDestination.php new file mode 100644 index 0000000..6192279 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Destination/DebugDestination.php @@ -0,0 +1,80 @@ +confGet('format') == 'html') { + print '
';
+    }
+
+    // Output the metadata.
+    if ($this->confGet('showmeta')) {
+      print "---------------------\n";
+      print "Metadata: \n";
+      print_r($file->getMetaAll());
+      print "---------------------\n";
+    }
+
+    // Output the body.
+    if ($this->confGet('showbody')) {
+      print "---------------------\n";
+      print "Body: \n";
+
+      $max = $this->confGet('maxbody');
+      $chunk = min($max, 1024);
+      if ($file->openForRead()) {
+        // Transfer file in 1024 byte chunks to save memory usage.
+        while ($max > 0 && $data = $file->readBytes($chunk)) {
+          print $data;
+          $max -= $chunk;
+        }
+        $file->close();
+      }
+      print "---------------------\n";
+    }
+
+    // Quick and dirty way to html format this output.
+    if ($this->confGet('format') == 'html') {
+      print '
'; + } + + exit; + } + + /** + * Get the default values for the plugin. + * + * @return \BackupMigrate\Core\Config\Config + */ + public function configDefaults() { + return new Config([ + 'showmeta' => TRUE, + 'showbody' => TRUE, + 'maxbody' => 1024 * 16, + 'format' => 'text', + ]); + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Destination/DestinationBase.php b/modules/backup_migrate/lib/backup_migrate_core/src/Destination/DestinationBase.php new file mode 100644 index 0000000..cc6223f --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Destination/DestinationBase.php @@ -0,0 +1,104 @@ +_saveFile($file); + $this->_saveFileMetadata($file); + } + + /** + * {@inheritdoc} + */ + public function loadFileMetadata(BackupFileInterface $file) { + // If this file is already loaded, simply return it. + // @TODO: fix this inappropriate use of file metadata. + if (!$file->getMeta('metadata_loaded')) { + $metadata = $this->_loadFileMetadataArray($file); + $file->setMetaMultiple($metadata); + $file->setMeta('metadata_loaded', TRUE); + } + return $file; + } + + /** + * {@inheritdoc} + */ + public function deleteFile($id) { + return $this->_deleteFile($id); + } + + /** + * {@inheritdoc} + */ + public function isRemote() { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function checkWritable() { + throw new DestinationNotWritableException('The specified destination cannot be written to.'); + } + + /** + * Do the actual delete for a file. + * + * @param string $id The id of the file to delete. + */ + abstract protected function _deleteFile($id); + + /** + * Do the actual file save. Should take care of the actual creation of a file + * in the destination without regard for metadata. + * + * @param \BackupMigrate\Core\File\BackupFileReadableInterface $file + */ + abstract protected function _saveFile(BackupFileReadableInterface $file); + + /** + * Do the metadata save. This function is called to save the data file AND + * the metadata sidecar file. + * + * @param \BackupMigrate\Core\File\BackupFileInterface $file + */ + abstract protected function _saveFileMetadata(BackupFileInterface $file); + + /** + * Load the actual metadata for the file. + * + * @param \BackupMigrate\Core\File\BackupFileInterface $file + */ + abstract protected function _loadFileMetadataArray(BackupFileInterface $file); + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Destination/DestinationInterface.php b/modules/backup_migrate/lib/backup_migrate_core/src/Destination/DestinationInterface.php new file mode 100644 index 0000000..df1f1a4 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Destination/DestinationInterface.php @@ -0,0 +1,18 @@ +_saveFile($file); + $this->_saveFileMetadata($file); + } + + /** + * {@inheritdoc} + */ + public function checkWritable() { + $this->checkDirectory(); + } + + /** + * Get a definition for user-configurable settings. + * + * @param array $params + * + * @return array + */ + public function configSchema($params = []) { + $schema = []; + + // Init settings. + if ($params['operation'] == 'initialize') { + $schema['fields']['directory'] = [ + 'type' => 'text', + 'title' => $this->t('Directory Path'), + ]; + } + + return $schema; + } + + + /** + * Do the actual file save. This function is called to save the data file AND + * the metadata sidecar file. + * + * @param \BackupMigrate\Core\File\BackupFileReadableInterface $file + * + * @throws \BackupMigrate\Core\Exception\BackupMigrateException + */ + function _saveFile(BackupFileReadableInterface $file) { + // Check if the directory exists. + $this->checkDirectory(); + + copy($file->realpath(), $this->_idToPath($file->getFullName())); + // @TODO: use copy/unlink if the temp file and the destination do not share a stream wrapper. + } + + /** + * Check that the directory can be used for backup. + * + * @throws \BackupMigrate\Core\Exception\BackupMigrateException + */ + protected function checkDirectory() { + $dir = $this->confGet('directory'); + + // Check if the directory exists. + if (!file_exists($dir)) { + throw new DestinationNotWritableException( + "The backup file could not be saved to '%dir' because it does not exist.", + ['%dir' => $dir] + ); + } + + // Check if the directory is writable. + if (!is_writable($this->confGet('directory'))) { + throw new DestinationNotWritableException( + "The backup file could not be saved to '%dir' because Backup and Migrate does not have write access to that directory.", + ['%dir' => $dir] + ); + } + } + + /** + * {@inheritdoc} + */ + public function getFile($id) { + if ($this->fileExists($id)) { + $out = new BackupFile(); + $out->setMeta('id', $id); + $out->setFullName($id); + return $out; + } + return NULL; + } + + /** + * {@inheritdoc} + */ + public function loadFileForReading(BackupFileInterface $file) { + // If this file is already readable, simply return it. + if ($file instanceof BackupFileReadableInterface) { + return $file; + } + + $id = $file->getMeta('id'); + if ($this->fileExists($id)) { + return new ReadableStreamBackupFile($this->_idToPath($id)); + } + return NULL; + } + + /** + * {@inheritdoc} + */ + public function listFiles() { + $dir = $this->confGet('directory'); + $out = []; + + // Get the entire list of filenames. + $files = $this->_getAllFileNames(); + + foreach ($files as $file) { + $filepath = $dir . '/' . $file; + $out[$file] = new ReadableStreamBackupFile($filepath); + } + + return $out; + } + + /** + * {@inheritdoc} + */ + public function queryFiles( + $filters = [], + $sort = 'datestamp', + $sort_direction = SORT_DESC, + $count = 100, + $start = 0 + ) { + + // Get the full list of files. + $out = $this->listFiles($count + $start); + foreach ($out as $key => $file) { + $out[$key] = $this->loadFileMetadata($file); + } + + // Filter the output. + if ($filters) { + $out = array_filter($out, function($file) use ($filters) { + foreach ($filters as $key => $value) { + if ($file->getMeta($key) !== $value) { + return FALSE; + } + } + return TRUE; + }); + } + + // Sort the files. + if ($sort && $sort_direction) { + uasort($out, function ($a, $b) use ($sort, $sort_direction) { + if ($sort_direction == SORT_DESC) { + return $b->getMeta($sort) < $b->getMeta($sort); + } + else { + return $b->getMeta($sort) > $b->getMeta($sort); + } + }); + } + + // Slice the return array. + if ($count || $start) { + $out = array_slice($out, $start, $count); + } + + return $out; + } + + + /** + * @return int The number of files in the destination. + */ + public function countFiles() { + $files = $this->_getAllFileNames(); + return count($files); + } + + + /** + * {@inheritdoc} + */ + public function fileExists($id) { + return file_exists($this->_idToPath($id)); + } + + /** + * {@inheritdoc} + */ + public function _deleteFile($id) { + if ($file = $this->getFile($id)) { + if ($file = $this->loadFileForReading($file)) { + return unlink($file->realpath()); + } + } + return FALSE; + } + + /** + * Return a file path for the given file id. + * + * @param $id + * + * @return string + */ + protected function _idToPath($id) { + return rtrim($this->confGet('directory'), '/') . '/' . $id; + } + + /** + * Get the entire file list from this destination. + * + * @return array + */ + protected function _getAllFileNames() { + $files = []; + + // Read the list of files from the directory. + $dir = $this->confGet('directory'); + + /** @var \Drupal\Core\File\FileSystemInterface $fileSystem */ + $fileSystem = \Drupal::service('file_system'); + $scheme = $fileSystem->uriScheme($dir); + + // Ensure the stream is configured. + if (!$fileSystem->validScheme($scheme)) { + drupal_set_message(t('Your @scheme stream is not configured.', [ + '@scheme' => $scheme . '://' + ]), 'warning'); + return $files; + } + + if ($handle = opendir($dir)) { + while (FALSE !== ($file = readdir($handle))) { + $filepath = $dir . '/' . $file; + // Don't show hidden, unreadable or metadata files. + if (substr($file, 0, 1) !== '.' && is_readable($filepath) && substr($file, strlen($file) - 5) !== '.info') { + $files[] = $file; + } + } + } + + return $files; + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Destination/ListableDestinationInterface.php b/modules/backup_migrate/lib/backup_migrate_core/src/Destination/ListableDestinationInterface.php new file mode 100644 index 0000000..db2c33f --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Destination/ListableDestinationInterface.php @@ -0,0 +1,60 @@ + 'text', + 'title' => $this->t('Secret Key'), + ]; + } + + return $schema; + } + + /** + * {@inheritdoc} + */ + public function checkWritable() { + return TRUE; + } + + /** + * Do the actual delete for a file. + * + * @param string $id The id of the file to delete. + */ + protected function _deleteFile($id) { + $this->getClient()->deleteFile($id); + } + + /** + * Do the actual file save. Should take care of the actual creation of a file + * in the destination without regard for metadata. + * + * @param \BackupMigrate\Core\File\BackupFileReadableInterface $file + * + * @return null + */ + protected function _saveFile(BackupFileReadableInterface $file) { + $this->getClient()->uploadFile($file); + } + + /** + * Do the metadata save. + * + * @param \BackupMigrate\Core\File\BackupFileInterface $file + */ + protected function _saveFileMetadata(BackupFileInterface $file) { + // Metadata is saved during the file upload process. Nothing to do here. + } + + /** + * Load the actual metadata for the file. + * + * @param \BackupMigrate\Core\File\BackupFileInterface $file + */ + protected function _loadFileMetadataArray(BackupFileInterface $file) { + // Metadata is fetched with the listing. There is no more to be fetched. + } + + /** + * Get a file object representing the file with the given ID from the destination. + * This file item will not necessarily be readable nor will it have extended + * metadata loaded. Use loadForReading and loadFileMetadata to get those. + * + * @TODO: Decide if extended metadata should ALWAYS be loaded here. + * + * @param string $id The unique identifier for the file. Usually the filename. + * + * @return \BackupMigrate\Core\File\BackupFileInterface + * The file if it exists or NULL if it doesn't + */ + public function getFile($id) { + // There is no way to fetch file info for a single file so we load them all. + $files = $this->listFiles(); + if (isset($files[$id])) { + return $files[$id]; + } + return NULL; + } + + /** + * Load the file with the given ID from the destination. + * + * @param \BackupMigrate\Core\File\BackupFileInterface $file + * + * @return \BackupMigrate\Core\File\BackupFileReadableInterface The file if it exists or NULL if it doesn't + */ + public function loadFileForReading(BackupFileInterface $file) { + // TODO: Implement loadFileForReading() method. + } + + /** + * Does the file with the given id (filename) exist in this destination. + * + * @param string $id The id (usually the filename) of the file. + * + * @return bool True if the file exists, false if it does not. + */ + public function fileExists($id) { + return (boolean) $this->getFile($id); + } + + /** + * Return a list of files from the destination. This list should be + * date ordered from newest to oldest. + * + * @param int $count + * @param int $start + * + * @return BackupFileInterface[] + * An array of BackupFileInterface objects representing the files with + * the file ids as keys. The file ids are usually file names but that + * is up to the implementing destination to decide. The returned files + * may not be readable. Use loadFileForReading to get a readable file. + */ + public function listFiles($count = 100, $start = 0) { + $file_list = $this->getClient()->listFiles(); + + $files = []; + foreach ((array) $file_list as $file) { + $out = new BackupFile(); + $out->setMeta('id', $file['filename']); + $out->setMetaMultiple($file); + $out->setFullName($file['filename']); + $files[$file['filename']] = $out; + } + return $files; + } + + /** + * @return int The number of files in the destination. + */ + public function countFiles() { + $file_list = $this->getClient()->listBackups(); + return count($file_list); + } + + /** + * Get the client class. + * + * @return \BackupMigrate\Core\Service\NodeSquirrelClient|null + * + * @throws \BackupMigrate\Core\Exception\BackupMigrateException + */ + protected function getClient() { + if ($this->client == NULL) { + $secret = $this->confGet('secret_key'); + if (!$secret) { + throw new BackupMigrateException('You must enter a secret key in order to use NodeSquirrel.'); + } + $this->client = new NodeSquirrelClient( + $this->confGet('secret_key'), + $this->confGet('api_endpoints', ['api.nodesquirrel.com']) + ); + } + return $this->client; + } + + /** + * Inject the client helper class. + * + * @param \BackupMigrate\Core\Service\NodeSquirrelClient $client + */ + public function setNodeSquirrelClient(NodeSquirrelClient $client) { + $this->client = $client; + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Destination/README.md b/modules/backup_migrate/lib/backup_migrate_core/src/Destination/README.md new file mode 100644 index 0000000..5dd8bc2 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Destination/README.md @@ -0,0 +1,13 @@ +# Destinations + +A destination in Backup and Migrate is the place where backup files are sent after they are created or where they are read from during a restore. The simplest example of a destination would be a directory on your web server. + +An object implementing the `\BackupMigrate\Core\Destination\DestinationInterface` can be used as a destination and is responsible for persisting a file using the given id (generally the filename). It is also responsible for returning the same file given the same file id. + +Destinations in Backup and Migrate are implemented as plugins and will have dependencies and configuration injected into them by the Plugin Manager. + +Like other plugins, destinations are passed to the Backup and Migrate object by the consuming application by calling the `add()` method on the plugin manager. + + $backup_migrate->destinations()->add('destination1', new MyDestinationPlugin()); + +A single Backup and Migrate instance can have more than one destination of a given type. Each destination will have a unique key that will be used to pass the configuration to the destination object as well as to specify the destination(s) when running a `backup()` or `restore()` operation. Only one destination will be used during each backup or restore operation. diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Destination/ReadableDestinationInterface.php b/modules/backup_migrate/lib/backup_migrate_core/src/Destination/ReadableDestinationInterface.php new file mode 100644 index 0000000..42c2e69 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Destination/ReadableDestinationInterface.php @@ -0,0 +1,59 @@ +getFullName(); + $filename = $id . '.info'; + if ($this->fileExists($filename)) { + $meta_file = $this->getFile($filename); + $meta_file = $this->loadFileForReading($meta_file); + $info = $this->_INIToArray($meta_file->readAll()); + } + return $info; + } + + /** + * {@inheritdoc} + */ + protected function _saveFileMetadata(BackupFileInterface $file) { + // Get the file metadata and convert to INI format. + $meta = $file->getMetaAll(); + $ini = $this->_arrayToINI($meta); + + // Create an info file. + $meta_file = $this->getTempFileManager()->pushExt($file, 'info'); + $meta_file->write($ini); + + // Save the metadata. + $this->_saveFile($meta_file); + } + + /** + * {@inheritdoc} + */ + public function deleteFile($id) { + $this->_deleteFile($id); + $this->_deleteFile($id . '.info'); + } + + /** + * Parse an INI file's contents. + * + * For simplification this function only parses the simple subset of INI + * syntax generated by SidecarMetadataDestinationTrait::_arrayToINI(); + * + * @param $ini + * + * @return array + */ + protected function _INIToArray($ini) { + $out = []; + $lines = explode("\n", $ini); + foreach ($lines as $line) { + $line = trim($line); + // Skip comments (even though there probably won't be any. + if (substr($line, 0, 1) == ';') { + continue; + } + + // Match the key and value using a simplified syntax. + $matches = []; + if (preg_match('/^([^=]+)\s?=\s?"(.*)"$/', $line, $matches)) { + $key = $matches[1]; + $val = $matches[2]; + + // Break up a key in the form a[b][c] + $keys = explode('[', $key); + $insert = &$out; + foreach ($keys as $part) { + $part = trim($part, ' ]'); + $insert[$part] = ''; + $insert = &$insert[$part]; + } + $insert = $val; + } + } + + return $out; + } + + /** + * @param $data + * @param string $prefix + * @return string + */ + protected function _arrayToINI($data, $prefix = '') { + $content = ""; + foreach ($data as $key => $val) { + if ($prefix) { + $key = $prefix . '[' . $key . ']'; + } + if (is_array($val)) { + $content .= $this->_arrayToINI($val, $key); + } + else { + $content .= $key . " = \"" . $val . "\"\n"; + } + } + return $content; + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Destination/StreamDestination.php b/modules/backup_migrate/lib/backup_migrate_core/src/Destination/StreamDestination.php new file mode 100644 index 0000000..905ada7 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Destination/StreamDestination.php @@ -0,0 +1,79 @@ +confGet('streamuri'); + if ($fp_out = fopen($stream_uri, 'w')) { + $file->openForRead(); + while ($data = $file->readBytes(1024 * 512)) { + fwrite($fp_out, $data); + } + fclose($fp_out); + $file->close(); + } + else { + throw new \Exception("Cannot open the file $stream_uri for writing"); + } + } + + /** + * {@inheritdoc} + */ + public function checkWritable() { + $stream_uri = $this->confGet('streamuri'); + + // The stream must exist. + if (!file_exists($stream_uri)) { + throw new DestinationNotWritableException('The file stream !uri does not exist.', ['%uri' => $stream_uri]); + } + + // The stream must be writable. + if (!file_exists($stream_uri)) { + throw new DestinationNotWritableException('The file stream !uri cannot be written to.', ['%uri' => $stream_uri]); + } + } + /** + * {@inheritdoc} + */ + public function getFile($id) { + return NULL; + } + + /** + * {@inheritdoc} + */ + public function loadFileMetadata(BackupFileInterface $file) { + return $file; + } + + /** + * {@inheritdoc} + */ + public function loadFileForReading(BackupFileInterface $file) { + return $file; + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Destination/WritableDestinationInterface.php b/modules/backup_migrate/lib/backup_migrate_core/src/Destination/WritableDestinationInterface.php new file mode 100644 index 0000000..13ca6e0 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Destination/WritableDestinationInterface.php @@ -0,0 +1,24 @@ +replacement = $replacement; + $this->message_raw = $message; + + // Send the replaced message to the parent constructor to act as normal in most cases. + parent::__construct(strtr($message, $replacement), $code); + } + + /** + * Get the unmodified message with replacement tokens. + * + * @return null|string + */ + public function getMessageRaw() { + return $this->message_raw; + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Exception/DestinationNotWritableException.php b/modules/backup_migrate/lib/backup_migrate_core/src/Exception/DestinationNotWritableException.php new file mode 100644 index 0000000..7da63ec --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Exception/DestinationNotWritableException.php @@ -0,0 +1,19 @@ +metadata[$key]) ? $this->metadata[$key] : NULL; + } + + /** + * Set a metadata value. + * + * @param string $key The key for the metadata item. + * @param mixed $value The value for the metadata item. + */ + public function setMeta($key, $value) { + $this->metadata[$key] = $value; + } + + /** + * Set a metadata value. + * + * @param array $values An array of key-value pairs for the file metadata. + */ + public function setMetaMultiple($values) { + foreach ((array) $values as $key => $value) { + $this->setMeta($key, $value); + } + } + + /** + * Get all metadata. + * + * @param array $values An array of key-value pairs for the file metadata. + * + * @return array + */ + public function getMetaAll() { + return $this->metadata; + } + + /** + * {@inheritdoc} + */ + public function setName($name) { + $this->name = $name; + } + + /** + * {@inheritdoc} + */ + public function getName() { + return $this->name; + } + + /** + * {@inheritdoc} + */ + public function getFullName() { + return rtrim($this->name . '.' . implode($this->getExtList(), '.')); + } + + /** + * {@inheritdoc} + */ + public function setFullName($fullname) { + // Break the file name into name and extension array. + $parts = explode('.', $fullname); + $this->setName(array_shift($parts)); + $this->setExtList($parts); + } + + + /** + * {@inheritdoc} + */ + public function getExtList() { + return $this->ext; + } + + /** + * {@inheritdoc} + */ + public function getExtLast() { + return end($this->ext); + } + + /** + * {@inheritdoc} + */ + public function getExt() { + return implode($this->getExtList(), '.'); + } + + /** + * @param array $ext + * The list of file extensions for the file* The list of file extensions for the file + */ + public function setExtList($ext) { + $this->ext = array_filter($ext); + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/File/BackupFileInterface.php b/modules/backup_migrate/lib/backup_migrate_core/src/File/BackupFileInterface.php new file mode 100644 index 0000000..61b43d3 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/File/BackupFileInterface.php @@ -0,0 +1,115 @@ + '~/mybackups']); + $file = $destination->getFile('databse.mysql'); + + // This object has metadata but the contents cannot neccessarily be read. + if ($file && $file->getMeta('filesize') > 1000) { + + // To read the file we must allow the destination to load it for us if needed. + $file = $destination->loadFileForReading($file); + + // The file contents should now be available to us. + if ($file) { + echo $file->readAll(); + } + } + +### BackupFileWriteableInterface +This subclass can be read from AND written to. Writable files in Backup and Migrate are always temporary files and must be created by the TempFileManager. Source plugins will create an empty temporary file to write the backup to while file filter plugins (like compression or encryption filters) will create a new temporary file and copy the contents from the input file to the new output file. The file that results at the end of the plugin chain will either be used to restore to the source (restore operation) or sent to a destination to be persisted (backup operation). Because plugins are responsible for creating new temporary writable files as needed, they should never require a writable file as input or promise one as a return value. + +## The Temporary File Manager +All writable files must be created by the Temporary File Manager. This class can create a new blank file with a given file extension. The standard flow of file filters is a chain where one filter hands a file to the next which copies the data to a new file and hands that on. For example, the MySQL source generates a new database dump file which gets handed to an encryption filter which copies the metadata to a new file containing the encrypted data. That file is then passed to a compression filter which creates a new compressed version of the file which is finally handed off to a destination for saving. At each step along the way a new file is created with an a new extension appended to the end: + + file.mysql -> file.mysql.aes -> file.mysql.aes.gz + +To facilitate this the Temporary File Manager takes care of the details of copying file metadata and provisioning a new temporary file with the new file extension to write the modified data to. A compressor plugin might do something like this: + + function afterBackup($file_in) { + // Get a new file with '.gz' added to the end of the filename. + $file_out = $this->getTempFileManager()->pushExt($file_in, 'gz'); + if ($this->doCompress($file_in, $file_out)) { + return $file_out; + } + // Compression failed, return the original + return $file_in; + } + +Similarly `$this->getTempFileManager()->popExt()` will pull the last item from the file extension and return a blank file for decompression prior to import. + +See [Plugins](https://github.com/backupmigrate/backup_migrate_core/tree/master/src/Plugin) for details on how to make the Temporary File Manager accessible within a plugin. + +### The Temporary File Adapter ### +While the file manager takes care of the metadata of temporary files, it cannot provision actual on-disk files to write to. That is because that operation will be different depending on where the code is run and is therefore the responsibility of the [Environment](https://github.com/backupmigrate/backup_migrate_core/tree/master/src/Environment) object. The environment provides a service called called the Temporary File Adaptor (an object whose class which implements `\BackupMigrate\Core\Services\TempFileAdapterInterface`). The job of this class is to provision actual temporary files in the host operating system that can be written to and read from. That service is also responsible for tracking all of the files that have been created during the running of an operation and deleting those files when the operation completes. Backup and Migrate core comes with a basic adapter which accepts any writable directory as an argument and creates new temporary files within that directory. This implementation should suffice for most consuming software but can be replaced with another adapter if needed. + +See: [Environment](https://github.com/backupmigrate/backup_migrate_core/tree/master/src/Environment) diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/File/ReadableStreamBackupFile.php b/modules/backup_migrate/lib/backup_migrate_core/src/File/ReadableStreamBackupFile.php new file mode 100644 index 0000000..680644c --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/File/ReadableStreamBackupFile.php @@ -0,0 +1,197 @@ +path = $filepath; + + // Get the basename and extensions. + $this->setFullName(basename($filepath)); + + // Get the basic file stats since this is probably a read-only file option and these won't change. + $this->_loadFileStats(); + } + + /** + * Destructor. + */ + function __destruct() { + // Close the handle if we've opened it. + $this->close(); + } + + /** + * Get the realpath of the file. + * + * @return string The path or stream URI to the file or NULL if the file does not exist. + */ + function realpath() { + if (file_exists($this->path)) { + return $this->path; + } + return NULL; + } + + /** + * Open a file for reading or writing. + * + * @param bool $binary If true open as a binary file + * + * @return resource + * + * @throws \Exception + */ + function openForRead($binary = FALSE) { + if (!$this->isOpen()) { + $path = $this->realpath(); + + if (!is_readable($path)) { + // @TODO: Throw better exception + throw new \Exception('Cannot read file.'); + } + + // Open the file. + $mode = "r" . ($binary ? "b" : ""); + $this->handle = fopen($path, $mode); + if (!$this->handle) { + throw new \Exception('Cannot open file.'); + } + } + // If the file is already open, rewind it. + $this->rewind(); + return $this->handle; + } + + /** + * Close a file when we're done reading/writing. + */ + function close() { + if ($this->isOpen()) { + fclose($this->handle); + $this->handle = NULL; + } + } + + /** + * Is this file open for reading/writing. + * + * Return bool True if the file is open, false if not. + */ + function isOpen() { + return !empty($this->handle) && get_resource_type($this->handle) == 'stream'; + } + + /** + * Read a line from the file. + * + * @param int $size The number of bites to read or 0 to read the whole file + * + * @return string The data read from the file or NULL if the file can't be read or is at the end of the file. + */ + function readBytes($size = 1024, $binary = FALSE) { + if (!$this->isOpen()) { + $this->openForRead($binary); + } + if ($this->handle && !feof($this->handle)) { + return fread($this->handle, $size); + } + return NULL; + } + + + /** + * Read a single line from the file. + * + * @return string The data read from the file or NULL if the file can't be read or is at the end of the file. + */ + public function readLine() { + if (!$this->isOpen()) { + $this->openForRead(); + } + return fgets($this->handle); + } + + /** + * Read a line from the file. + * + * @return string The data read from the file or NULL if the file can't be read. + */ + public function readAll() { + if (!$this->isOpen()) { + $this->openForRead(); + } + $this->rewind(); + return stream_get_contents($this->handle); + } + + /** + * Move the file pointer forward a given number of bytes. + * + * @param int $bytes + * + * @return int + * The number of bytes moved or -1 if the operation failed. + */ + public function seekBytes($bytes) { + if ($this->isOpen()) { + return fseek($this->handle, $bytes); + } + return -1; + } + + /** + * Rewind the file handle to the start of the file. + */ + function rewind() { + if ($this->isOpen()) { + rewind($this->handle); + } + } + + /** + * Get info about the file and load them as metadata. + */ + protected function _loadFileStats() { + clearstatcache(); + $this->setMeta('filesize', filesize($this->realpath())); + $this->setMeta('datestamp', filectime($this->realpath())); + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/File/TempFileAdapter.php b/modules/backup_migrate/lib/backup_migrate_core/src/File/TempFileAdapter.php new file mode 100644 index 0000000..c8d8b29 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/File/TempFileAdapter.php @@ -0,0 +1,115 @@ +dir = $dir; + $this->prefix = $prefix; + $this->tempfiles = []; + // @TODO: check that temp direcory is writeable or throw an exception. + } + + /** + * Destruct the manager. Delete all the temporary files when this manager is destroyed. + */ + public function __destruct() { + $this->deleteAllTempFiles(); + } + + /** + * {@inheritdoc} + */ + public function createTempFile($ext = '') { + // Add a dot to the file extension. + $ext = $ext ? '.' . $ext : ''; + + // Find an unused random file name. + $try = 5; + do { + $out = $this->dir . $this->prefix . mt_rand() . $ext; + $fp = @fopen($out, 'x'); + } while (!$fp && $try-- > 0); + if ($fp) { + fclose($fp); + } + else { + throw new \Exception('Could not create a temporary file to write to.'); + } + + $this->tempfiles[] = $out; + return $out; + } + + /** + * {@inheritdoc} + */ + public function deleteTempFile($filename) { + // Only delete files that were created by this manager. + if (in_array($filename, $this->tempfiles)) { + if (file_exists($filename)) { + if (is_writable($filename)) { + unlink($filename); + } + else { + throw new BackupMigrateException('Could not delete the temp file: %file because it is not writable', ['%file' => $filename]); + } + } + // Remove the item from the list. + $this->tempfiles = array_diff($this->tempfiles, [$filename]); + return; + } + throw new BackupMigrateException('Attempting to delete a temp file not managed by this codebase: %file', ['%file' => $filename]); + } + + /** + * {@inheritdoc} + */ + public function deleteAllTempFiles() { + foreach ($this->tempfiles as $file) { + $this->deleteTempFile($file); + } + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/File/TempFileAdapterInterface.php b/modules/backup_migrate/lib/backup_migrate_core/src/File/TempFileAdapterInterface.php new file mode 100644 index 0000000..f8c8e72 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/File/TempFileAdapterInterface.php @@ -0,0 +1,35 @@ +adapter = $adapter; + } + + /** + * Create a brand new temp file with the given extension (if specified). The + * new file should be writable. + * + * @param string $ext The file extension for this file (optional) + * + * @return BackupFileWritableInterface + */ + public function create($ext = '') { + $file = new WritableStreamBackupFile($this->adapter->createTempFile($ext)); + $file->setExtList(explode('.', $ext)); + return $file; + } + + /** + * Return a new file based on the passed in file with the given file extension. + * This should maintain the metadata of the file passed in with the new file + * extension added after the old one. + * For example: xxx.mysql would become xxx.mysql.gz. + * + * @param \BackupMigrate\Core\File\BackupFileInterface $file + * The file to add the extension to. + * @param $ext + * The new file extension. + * + * @return \BackupMigrate\Core\File\BackupFileWritableInterface + * A new writable backup file with the new extension and all of the metadata + * from the previous file. + */ + public function pushExt(BackupFileInterface $file, $ext) { + // Push the new extension on to the new file. + $parts = $file->getExtList(); + array_push($parts, $ext); + $new_ext = implode($parts, '.'); + + // Copy the file metadata to a new TempFile. + $out = new WritableStreamBackupFile($this->adapter->createTempFile($new_ext)); + + // Copy the file metadata to a new TempFile. + $out->setMetaMultiple($file->getMetaAll()); + $out->setName($file->getName()); + $out->setExtList($parts); + + return $out; + } + + /** + * Return a new file based on the one passed in but with the last part of the + * file extension removed. + * For example: xxx.mysql.gz would become xxx.mysql. + * + * @param \BackupMigrate\Core\File\BackupFileInterface $file + * + * @return \BackupMigrate\Core\File\BackupFileWritableInterface + * A new writable backup file with the last extension removed and + * all of the metadata from the previous file. + */ + public function popExt(BackupFileInterface $file) { + // Pop the last extension from the last of the file. + $parts = $file->getExtList(); + array_pop($parts); + $new_ext = implode($parts, '.'); + + // Create a new temp file with the new extension. + $out = new WritableStreamBackupFile($this->adapter->createTempFile($new_ext)); + + // Copy the file metadata to a new TempFile. + $out->setMetaMultiple($file->getMetaAll()); + $out->setName($file->getName()); + $out->setExtList($parts); + + return $out; + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/File/TempFileManagerInterface.php b/modules/backup_migrate/lib/backup_migrate_core/src/File/TempFileManagerInterface.php new file mode 100644 index 0000000..166ef2f --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/File/TempFileManagerInterface.php @@ -0,0 +1,64 @@ +isOpen()) { + $path = $this->realpath(); + + // Check if the file can be read/written. + if ((file_exists($path) && !is_writable($path)) || (!file_exists($path) && !is_writable(dirname($path)))) { + // @TODO: Throw better exception + throw new BackupMigrateException('Cannot write to file: %path', ['%path' => $path]); + } + + // Open the file. + $mode = "w" . ($binary ? "b" : ""); + $this->handle = fopen($path, $mode); + if (!$this->handle) { + throw new BackupMigrateException('Cannot open file: %path', ['%path' => $path]); + } + } + } + + /** + * Write a line to the file. + * + * @param string $data A string to write to the file. + * + * @throws \Exception + */ + function write($data) { + if (!$this->isOpen()) { + $this->openForWrite(); + } + + if ($this->handle) { + if (fwrite($this->handle, $data) === FALSE) { + throw new \Exception('Cannot write to file: ' . $this->realpath()); + } + else { + $this->dirty = TRUE; + } + } + else { + throw new \Exception('File not open for writing.'); + } + } + + + /** + * Update the file time and size when the file is closed. + */ + function close() { + parent::close(); + + // If the file has been modified, update the stats from disk. + if ($this->dirty) { + $this->_loadFileStats(); + $this->dirty = FALSE; + } + } + + /** + * A shorthand function to open the file, write the given contents and close + * the file. Used for small amounts of data that can fit in memory. + * + * @param $data + */ + public function writeAll($data) { + $this->openForWrite(); + $this->write($data); + $this->close(); + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Filter/CompressionFilter.php b/modules/backup_migrate/lib/backup_migrate_core/src/Filter/CompressionFilter.php new file mode 100644 index 0000000..8d417c9 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Filter/CompressionFilter.php @@ -0,0 +1,389 @@ + ['weight' => 100], + * 'restore' => ['weight' => -100], + * ]; + * + * @return array + */ + public function supportedOps() { + return [ + 'getFileTypes' => [], + 'backupSettings' => [], + 'afterBackup' => ['weight' => 100], + 'beforeRestore' => ['weight' => -100], + ]; + } + + /** + * Return the filetypes supported by this filter. + */ + public function getFileTypes() { + return [ + [ + "gzip" => [ + "extension" => "gz", + "filemime" => "application/x-gzip", + 'ops' => [ + 'backup', + 'restore' + ] + ], + "bzip" => [ + "extension" => "bz", + "filemime" => "application/x-bzip", + 'ops' => [ + 'backup', + 'restore' + ] + ], + "bzip2" => [ + "extension" => "bz2", + "filemime" => "application/x-bzip", + 'ops' => [ + 'backup', + 'restore' + ] + ], + "zip" => [ + "extension" => "zip", + "filemime" => "application/zip", + 'ops' => [ + 'backup', + 'restore' + ] + ], + ], + ]; + } + + + /** + * Get a definition for user-configurable settings. + * + * @return array + */ + public function configSchema($params = []) { + $schema = []; + + if ($params['operation'] == 'backup') { + $schema['groups']['file'] = [ + 'title' => 'Backup File', + ]; + $compression_options = $this->_availableCompressionAlgorithms(); + $schema['fields']['compression'] = [ + 'group' => 'file', + 'type' => 'enum', + 'title' => 'Compression', + 'options' => $compression_options, + 'actions' => ['backup'] + ]; + } + + return $schema; + } + + + /** + * Get the default values for the plugin. + * + * @return \BackupMigrate\Core\Config\Config + */ + public function configDefaults() { + return new Config([ + 'compression' => $this->_defaultCompressionAlgorithm(), + ]); + } + + + /** + * Run on a backup. + * + * @param \BackupMigrate\Core\File\BackupFileReadableInterface $file + * + * @return \BackupMigrate\Core\File\BackupFileReadableInterface + */ + public function afterBackup(BackupFileReadableInterface $file) { + $out = $success = FALSE; + if ($this->confGet('compression') == 'gzip') { + $out = $this->getTempFileManager()->pushExt($file, 'gz'); + $success = $this->_gzipEncode($file, $out); + } + if ($this->confGet('compression') == 'bzip') { + $out = $this->getTempFileManager()->pushExt($file, 'bz2'); + $success = $this->_bzipEncode($file, $out); + } + if ($this->confGet('compression') == 'zip') { + $out = $this->getTempFileManager()->pushExt($file, 'zip'); + $success = $this->_ZipEncode($file, $out); + } + + // If the file was successfully compressed. + if ($out && $success) { + $out->setMeta('filesize_uncompressed', $file->getMeta('filesize')); + $out->setMeta('compression', $this->confGet('compression')); + return $out; + } + + // Return the original if we were not able to compress it. + return $file; + } + + /** + * Run on a restore. + * + * @param \BackupMigrate\Core\File\BackupFileReadableInterface $file + * + * @return \BackupMigrate\Core\File\BackupFileReadableInterface + */ + public function beforeRestore(BackupFileReadableInterface $file) { + // If the file is not a supported compression type then simply return the same input file. + $out = $file; + + $type = $file->getExtLast(); + + switch (strtolower($type)) { + case "gz": + case "gzip": + $out = $this->getTempFileManager()->popExt($file); + $this->_gzipDecode($file, $out); + break; + + case "bz": + case "bz2": + case "bzip": + case "bzip2": + $out = $this->getTempFileManager()->popExt($file); + $this->_bzipDecode($file, $out); + break; + + case "zip": + $out = $this->getTempFileManager()->popExt($file); + $this->_ZipDecode($file, $out); + break; + + } + return $out; + } + + + /** + * Gzip encode a file. + * + * @param \BackupMigrate\Core\File\BackupFileReadableInterface $from + * @param \BackupMigrate\Core\File\BackupFileWritableInterface $to + * + * @return bool + */ + protected function _gzipEncode(BackupFileReadableInterface $from, BackupFileWritableInterface $to) { + $success = FALSE; + + if (!$success && function_exists("gzopen")) { + if (($fp_out = gzopen($to->realpath(), 'wb9')) && $from->openForRead()) { + while ($data = $from->readBytes(1024 * 512)) { + gzwrite($fp_out, $data); + } + $success = TRUE; + $from->close(); + gzclose($fp_out); + + // Get the compressed filesize and set it. + $fileszc = filesize(drupal_realpath($to->realpath())); + $to->setMeta('filesize', $fileszc); + } + } + + return $success; + } + + /** + * Gzip decode a file. + * + * @param \BackupMigrate\Core\File\BackupFileReadableInterface $from + * @param \BackupMigrate\Core\File\BackupFileWritableInterface $to + * + * @return bool + */ + protected function _gzipDecode(BackupFileReadableInterface $from, BackupFileWritableInterface $to) { + $success = FALSE; + + if (!$success && function_exists("gzopen")) { + if ($fp_in = gzopen($from->realpath(), 'rb9')) { + while (!feof($fp_in)) { + $to->write(gzread($fp_in, 1024 * 512)); + } + $success = TRUE; + gzclose($fp_in); + $to->close(); + } + } + + return $success; + } + + /** + * BZip encode a file. + * + * @param \BackupMigrate\Core\File\BackupFileReadableInterface $from + * @param \BackupMigrate\Core\File\BackupFileWritableInterface $to + * + * @return bool + */ + protected function _bzipEncode(BackupFileReadableInterface $from, BackupFileWritableInterface $to) { + $success = FALSE; + if (!$success && function_exists("bzopen")) { + if (($fp_out = bzopen($to->realpath(), 'w')) && $from->openForRead()) { + while ($data = $from->readBytes(1024 * 512)) { + bzwrite($fp_out, $data); + } + $success = TRUE; + $from->close(); + bzclose($fp_out); + + // Get the compressed filesize and set it. + $fileszc = filesize(drupal_realpath($to->realpath())); + $to->setMeta('filesize', $fileszc); + } + } + + return $success; + } + + /** + * BZip decode a file. + * + * @param \BackupMigrate\Core\File\BackupFileReadableInterface $from + * @param \BackupMigrate\Core\File\BackupFileWritableInterface $to + * + * @return bool + */ + protected function _bzipDecode(BackupFileReadableInterface $from, BackupFileWritableInterface $to) { + $success = FALSE; + + if (!$success && function_exists("bzopen")) { + if ($fp_in = bzopen($from->realpath(), 'r')) { + while (!feof($fp_in)) { + $to->write(bzread($fp_in, 1024 * 512)); + } + $success = TRUE; + bzclose($fp_in); + $to->close(); + } + } + + return $success; + } + + /** + * Zip encode a file. + * + * @param \BackupMigrate\Core\File\BackupFileReadableInterface $from + * + * @param \BackupMigrate\Core\File\BackupFileWritableInterface $to + * + * @return bool + */ + protected function _ZipEncode(BackupFileReadableInterface $from, BackupFileWritableInterface $to) { + $success = FALSE; + + if (class_exists('ZipArchive')) { + $zip = new \ZipArchive(); + $res = $zip->open(drupal_realpath($to->realpath()), constant("ZipArchive::CREATE")); + if ($res === TRUE) { + $zip->addFile(drupal_realpath($from->realpath()), $from->getFullName()); + } + $success = $zip->close(); + } + // Get the compressed filesize and set it. + $fileszc = filesize(drupal_realpath($to->realpath())); + $to->setMeta('filesize', $fileszc); + + return $success; + } + + /** + * Gzip decode a file. + * + * @param \BackupMigrate\Core\File\BackupFileReadableInterface $from + * @param \BackupMigrate\Core\File\BackupFileWritableInterface $to + * + * @return bool + */ + protected function _ZipDecode(BackupFileReadableInterface $from, BackupFileWritableInterface $to) { + $success = FALSE; + if (class_exists('ZipArchive')) { + $zip = new \ZipArchive(); + if ($zip->open(drupal_realpath($from->realpath()))) { + $filename = ($zip->getNameIndex(0)); + if ($fp_in = $zip->getStream($filename)) { + while (!feof($fp_in)) { + $to->write(fread($fp_in, 1024 * 512)); + } + fclose($fp_in); + $success = $to->close(); + } + } + return $success; + } + } + + /** + * Get the compression options as an options array for a form item. + * + * @return array + */ + protected function _availableCompressionAlgorithms() { + $compression_options = ["none" => ("No Compression")]; + if (function_exists("gzencode")) { + $compression_options['gzip'] = ("GZip"); + } + if (function_exists("bzcompress")) { + $compression_options['bzip'] = ("BZip"); + } + if (class_exists('ZipArchive')) { + $compression_options['zip'] = ("Zip"); + } + return $compression_options; + } + + /** + * Get the default compression algorithm based on those available. + * + * @return string + * The machine name of the algorithm. + */ + protected function _defaultCompressionAlgorithm() { + $available = array_keys($this->_availableCompressionAlgorithms()); + // Remove the 'none' option. + array_shift($available); + $out = array_shift($available); + // Return the first available algorithm or 'none' of none other exist. + return $out ? $out : 'none'; + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Filter/DBExcludeFilter.php b/modules/backup_migrate/lib/backup_migrate_core/src/Filter/DBExcludeFilter.php new file mode 100644 index 0000000..994a268 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Filter/DBExcludeFilter.php @@ -0,0 +1,122 @@ +confGet('exclude_tables'); + $nodata = $this->confGet('nodata_tables'); + if (in_array($table['name'], $exclude)) { + $table['exclude'] = TRUE; + } + if (in_array($table['name'], $nodata)) { + $table['nodata'] = TRUE; + } + return $table; + } + + /** + * Get the default values for the plugin. + * + * @return \BackupMigrate\Core\Config\Config + */ + public function configDefaults() { + return new Config([ + 'source' => '', + 'exclude_tables' => [], + 'nodata_tables' => [], + ]); + } + + /** + * Get a definition for user-configurable settings. + * + * @param array $params + * + * @return array + */ + public function configSchema($params = []) { + $schema = []; + + if ($params['operation'] == 'backup') { + $tables = []; + + foreach ($this->sources()->getAll() as $source_key => $source) { + if ($source instanceof DatabaseSourceInterface) { + $tables += $source->getTableNames(); + } + + if ($tables) { + // Backup settings. + $schema['groups']['default'] = [ + 'title' => $this->t('Exclude database tables'), + ]; + + $table_select = [ + 'type' => 'enum', + 'multiple' => TRUE, + 'options' => $tables, + 'actions' => ['backup'], + 'group' => 'default' + ]; + $schema['fields']['exclude_tables'] = $table_select + [ + 'title' => $this->t('Exclude these tables entirely'), + ]; + + $schema['fields']['nodata_tables'] = $table_select + [ + 'title' => $this->t('Exclude data from these tables'), + ]; + + } + } + } + return $schema; + } + + /** + * @return PluginManager + */ + public function sources() { + return $this->source_manager ? $this->source_manager : new PluginManager(); + } + + /** + * @param PluginManager $source_manager + */ + public function setSourceManager($source_manager) { + $this->source_manager = $source_manager; + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Filter/FileExcludeFilter.php b/modules/backup_migrate/lib/backup_migrate_core/src/Filter/FileExcludeFilter.php new file mode 100644 index 0000000..e127b30 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Filter/FileExcludeFilter.php @@ -0,0 +1,134 @@ +confGet('source'); + if ($source && $source == $params['source']) { + $exclude = $this->confGet('exclude_filepaths'); + $exclude = $this->compileExcludePatterns($exclude); + + if ($this->matchPath($path, $exclude, $params['base_path'])) { + return NULL; + } + } + return $path; + } + + /** + * Get the default values for the plugin. + * + * @return \BackupMigrate\Core\Config\Config + */ + public function configDefaults() { + return new Config([ + 'source' => '', + 'exclude_filepaths' => [], + ]); + } + + /** + * Convert an array of glob patterns to an array of regex patterns for file name exclusion. + * + * @param array $exclude + * A list of patterns with glob wildcards + * + * @return array + * A list of patterns as regular expressions + */ + private function compileExcludePatterns($exclude) { + if ($this->patterns !== NULL) { + return $this->patterns; + } + foreach ($exclude as $pattern) { + // Convert Glob wildcards to a regex per http://php.net/manual/en/function.fnmatch.php#71725 + $this->patterns[] = "#^" . strtr(preg_quote($pattern, '#'), ['\*' => '.*', '\?' => '.', '\[' => '[', '\]' => ']']) . "$#i"; + } + return $this->patterns; + } + + /** + * Match a path to the list of exclude patterns. + * + * @param string $path + * The path to match. + * @param array $exclude + * An array of regular expressions to match against. + * @param string $base_path + * + * @return bool + */ + private function matchPath($path, $exclude, $base_path = '') { + $path = substr($path, strlen($base_path)); + + if ($exclude) { + foreach ($exclude as $pattern) { + if (preg_match($pattern, $path)) { + return TRUE; + } + } + } + return FALSE; + } + + /** + * Get a definition for user-configurable settings. + * + * @param array $params + * + * @return array + */ + public function configSchema($params = []) { + $schema = []; + + $source = $this->confGet('source'); + + // Backup settings. + if (!empty($source) && $params['operation'] == 'backup') { + $schema['groups']['default'] = [ + 'title' => $this->t('Exclude Files from %source', ['%source' => $source->confGet('name')]), + ]; + // Backup settings. + if ($params['operation'] == 'backup') { + $schema['fields']['exclude_filepaths'] = [ + 'type' => 'text', + 'title' => $this->t('Exclude these files'), + 'multiple' => TRUE, + 'group' => 'default' + ]; + } + } + return $schema; + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Filter/FileNamer.php b/modules/backup_migrate/lib/backup_migrate_core/src/Filter/FileNamer.php new file mode 100644 index 0000000..b20a56f --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Filter/FileNamer.php @@ -0,0 +1,120 @@ +moduleExists('token')) { + $must_match = '/^[\w\-_:\[\]]+$/'; + $must_match_err = $this->t('%title must contain only letters, numbers, dashes (-) and underscores (_). And Site Tokens.'); + } + else { + $must_match = '/^[\w\-_:]+$/'; + $must_match_err = $this->t('%title must contain only letters, numbers, dashes (-) and underscores (_).'); + } + // Backup configuration. + if ($params['operation'] == 'backup') { + $schema['groups']['file'] = [ + 'title' => 'Backup File', + ]; + $schema['fields']['filename'] = [ + 'group' => 'file', + 'type' => 'text', + 'title' => 'File Name', + 'must_match' => $must_match, + 'must_match_error' => $must_match_err, + 'min_length' => 1, + // Allow a 200 character backup name leaving a generous 55 characters + // for timestamp and extension. + 'max_length' => 200, + 'required' => TRUE, + ]; + $schema['fields']['timestamp'] = [ + 'group' => 'file', + 'type' => 'boolean', + 'title' => 'Append a timestamp', + ]; + $schema['fields']['timestamp_format'] = [ + 'group' => 'file', + 'type' => 'text', + 'title' => 'Timestamp Format', + 'max_length' => 32, + 'dependencies' => ['timestamp' => TRUE], + 'description' => $this->t('Use PHP Date formatting.'), + ]; + } + return $schema; + } + + /** + * Get the default values for the plugin. + * + * @return \BackupMigrate\Core\Config\Config + */ + public function configDefaults() { + return new Config([ + 'filename' => 'backup', + 'timestamp' => TRUE, + 'timestamp_format' => 'Y-m-d\TH-i-s', + ]); + } + + /** + * Get a list of supported operations and their weight. + * + * @return array + */ + public function supportedOps() { + return [ + 'afterBackup' => [], + ]; + } + + /** + * Run on a backup. Name the backup file according to the configuration. + * + * @param \BackupMigrate\Core\File\BackupFileReadableInterface $file + * + * @return \BackupMigrate\Core\File\BackupFileReadableInterface + */ + public function afterBackup(BackupFileReadableInterface $file) { + if (\Drupal::moduleHandler()->moduleExists('token')) { + $token = \Drupal::token(); + $name = $token->replace($this->confGet('filename')); + } + else { + $name = $this->confGet('filename'); + } + if ($this->confGet('timestamp')) { + $name .= '-' . gmdate($this->confGet('timestamp_format')); + } + $file->setName($name); + return $file; + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Filter/MetadataWriter.php b/modules/backup_migrate/lib/backup_migrate_core/src/Filter/MetadataWriter.php new file mode 100644 index 0000000..0f27e72 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Filter/MetadataWriter.php @@ -0,0 +1,112 @@ + 'Advanced Settings', + ]; + $schema['fields']['description'] = [ + 'group' => 'advanced', + 'type' => 'text', + 'title' => 'Description', + 'multiline' => TRUE, + ]; + } + return $schema; + } + + /** + * Get the default values for the plugin. + * + * @return \BackupMigrate\Core\Config\Config + */ + public function configDefaults() { + return new Config([ + 'description' => '', + 'generator' => 'Backup and Migrate', + 'generatorversion' => defined('BACKUP_MIGRATE_CORE_VERSION') ? constant('BACKUP_MIGRATE_CORE_VERSION') : 'unknown', + 'generatorurl' => 'https://github.com/backupmigrate', + 'bam_sourceid' => '', + ]); + } + + /** + * Generate a list of metadata keys to be stored with the backup. + * + * @return array + */ + protected function getMetaKeys() { + return [ + 'description', + 'generator', + 'generatorversion', + 'generatorurl', + 'bam_sourceid', + 'bam_scheduleid', + ]; + } + + + /** + * Run before the backup/restore begins. + */ + public function setUp($operand, $options) { + if ($options['operation'] == 'backup' && $options['source_id']) { + $this->config()->set('bam_sourceid', $options['source_id']); + if ($source = $this->plugins()->get($options['source_id'])) { + // @TODO Query the source for it's type and name. + } + } + return $operand; + } + + /** + * Run after a backup. Add metadata to the file. + * + * @param \BackupMigrate\Core\File\BackupFileWritableInterface $file + * + * @return \BackupMigrate\Core\File\BackupFileWritableInterface + */ + public function afterBackup(BackupFileWritableInterface $file) { + // Add the various metadata. + foreach ($this->getMetaKeys() as $key) { + $value = $this->confGet($key); + $file->setMeta($key, $value); + } + return $file; + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Filter/Notify.php b/modules/backup_migrate/lib/backup_migrate_core/src/Filter/Notify.php new file mode 100644 index 0000000..e5f86a2 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Filter/Notify.php @@ -0,0 +1,102 @@ + ['weight' => -100000], + 'beforeRestore' => ['weight' => -100000], + ]; + } + + /** + * @var StashLogger + */ + protected $logstash; + + public function beforeBackup() { + $this->addLogger(); + } + + public function beforeRestore() { + $this->addLogger(); + } + + public function backupSucceed() { + $this->sendNotification('Backup finished sucessfully'); + } + + public function backupFail(Exception $e) { + + } + + public function restoreSucceed() { + } + + public function restoreFail() { + } + + /** + * @param $subject + * @param $body + * @param $messages + */ + protected function sendNotification($subject) { + $messages = $this->logstash->getAll(); + $body = $subject . "\n"; + if (count($messages)) { + + } + // $body .= + } + + /** + * add our stash logger to the service locator to capture all logged messages. + */ + protected function addLogger() { + $services = $this->plugins()->services(); + + // Get the current logger. + $logger = $services->get('Logger'); + + // Create a new stash logger to save messages. + $this->logstash = new StashLogger(); + + // Add a tee to send logs to both the regular logger and our stash. + $services->add('Logger', new TeeLogger([$logger, $this->logstash])); + + // Add the services back into the plugin manager to re-inject existing plugins + $this->plugins()->setServiceLocator($services); + } + + // @TODO: Add a tee to the logger to capture all messages. + // @TODO: Implement backup/restore fail/succeed ops and send a notification. +} \ No newline at end of file diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Main/BackupMigrate.php b/modules/backup_migrate/lib/backup_migrate_core/src/Main/BackupMigrate.php new file mode 100644 index 0000000..10f68a3 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Main/BackupMigrate.php @@ -0,0 +1,237 @@ +setServiceManager(new ServiceManager()); + $services = $this->services(); + + $services->add('PluginManager', new PluginManager($services)); + $services->add('SourceManager', new PluginManager($services)); + $services->add('DestinationManager', new PluginManager($services)); + + // Add these services back into this object using the service manager. + $services->addClient($this); + } + + /** + * {@inheritdoc} + */ + public function backup($source_id, $destination_id) { + try { + + // Allow the plugins to set up. + $this->plugins()->call('setUp', NULL, ['operation' => 'backup', 'source_id' => $source_id, 'destination_id' => $destination_id]); + + // Get the source and the destination to use. + $source = $this->sources()->get($source_id); + $destinations = []; + + // Allow a single destination or multiple destinations. + foreach ((array) $destination_id as $id) { + $destinations[$id] = $this->destinations()->get($id); + + // Check that the destination is valid. + if (!$destinations[$id]) { + throw new BackupMigrateException('The destination !id does not exist.', ['!id' => $destination_id]); + } + + // Check that the destination can be written to. + // @TODO: Catch exceptions and continue if at least one destination is valid. + $destinations[$id]->checkWritable(); + } + + // Check that the source is valid. + if (!$source) { + throw new BackupMigrateException('The source !id does not exist.', ['!id' => $source_id]); + } + + // Run each of the installed plugins which implements the 'beforeBackup' operation. + $this->plugins()->call('beforeBackup'); + + // Do the actual backup. + $file = $source->exportToFile(); + + // Run each of the installed plugins which implements the 'afterBackup' operation. + $file = $this->plugins()->call('afterBackup', $file); + + // Save the file to each destination. + foreach ($destinations as $destination) { + $destination->saveFile($file); + } + + // Let plugins react to a successful operation. + $this->plugins()->call('backupSucceed', $file); + } + catch (\Exception $e) { + // Let plugins react to a failed operation. + $this->plugins()->call('backupFail', $e); + + // The consuming software needs to deal with this. + throw $e; + } + + // Allow the plugins to tear down. + $this->plugins()->call('tearDown', NULL, ['operation' => 'backup', 'source_id' => $source_id, 'destination_id' => $destination_id]); + + } + + /** + * {@inheritdoc} + */ + public function restore($source_id, $destination_id, $file_id = NULL) { + try { + // Get the source and the destination to use. + $source = $this->sources()->get($source_id); + $destination = $this->destinations()->get($destination_id); + + if (!$source) { + throw new BackupMigrateException('The source !id does not exist.', ['!id' => $source_id]); + } + if (!$destination) { + throw new BackupMigrateException('The destination !id does not exist.', ['!id' => $destination_id]); + } + + // Load the file from the destination. + $file = $destination->getFile($file_id); + if (!$file) { + throw new BackupMigrateException('The file !id does not exist.', ['!id' => $file_id]); + } + + // Prepare the file for reading. + $file = $destination->loadFileForReading($file); + if (!$file) { + throw new BackupMigrateException('The file !id could not be opened for reading.', ['!id' => $file_id]); + } + + // Run each of the installed plugins which implements the 'backup' operation. + $file = $this->plugins()->call('beforeRestore', $file); + + // Do the actual source restore. + $import_result = $source->importFromFile($file); + if(!$import_result) { + throw new BackupMigrateException('The file could not be imported.'); + } + + // Run each of the installed plugins which implements the 'beforeBackup' operation. + $this->plugins()->call('afterRestore'); + + // Let plugins react to a successful operation. + $this->plugins()->call('restoreSucceed', $file); + } + catch (\Exception $e) { + // Let plugins react to a failed operation. + $this->plugins()->call('restoreFail', $e); + + // The consuming software needs to deal with this. + throw $e; + } + } + + /** + * Set the configuration for the service. This simply passes the configuration + * on to the plugin manager as all work is done by plugins. + * + * This can be called after the service is instantiated to pass new configuration + * to the plugins. + * + * @param \BackupMigrate\Core\Config\ConfigInterface $config + */ + public function setConfig(ConfigInterface $config) { + $this->plugins()->setConfig($config); + } + + /** + * Get the list of available destinations. + * + * @return PluginManagerInterface + */ + public function destinations() { + return $this->destinations; + } + + /** + * Set the destinations plugin manager. + * + * @param PluginManagerInterface $destinations + */ + public function setDestinationManager(PluginManagerInterface $destinations) { + $this->destinations = $destinations; + } + + /** + * Get the list of sources. + * + * @return PluginManagerInterface + */ + public function sources() { + return $this->sources; + } + + /** + * Set the sources plugin manager. + * + * @param PluginManagerInterface $sources + */ + public function setSourceManager(PluginManagerInterface $sources) { + $this->sources = $sources; + } + + /** + * Get the service locator. + * + * @return ServiceManager + */ + public function services() { + return $this->services; + } + + /** + * Set the service locator. + * + * @param ServiceManager $services + */ + public function setServiceManager($services) { + $this->services = $services; + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Main/BackupMigrateInterface.php b/modules/backup_migrate/lib/backup_migrate_core/src/Main/BackupMigrateInterface.php new file mode 100644 index 0000000..92849e6 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Main/BackupMigrateInterface.php @@ -0,0 +1,87 @@ +plugins()` method. The `add()` +method can then be used to add additional plugins. Each added plugin must be given a unique ID when added. This ID will be used +to configure the plugin and to specify which source and destination are used during the operation. + + + // ... + + // Create a Backup and Migrate Service object + $bam = new BackupMigrate($); + + // Create a service locator + $services = new ServiceManager(); + + // Add necessary services + $services->add('TempFileManager', + new TempFileManager(new TempFileAdapter('/tmp')) + ); + $services->add('Logger', + new Logger() + ); + + // Create a plugin manager + $plugins = new PluginManager($services); + + // Add a source: + $plugins->add('db1', new MySQLiSource()); + + // Add some destinations + $plugins->add('download', new BrowserDownloadDestination()); + $plugins->add('mydirectory', new DirectoryDestination()); + + // Add some filters + $plugins->add('compress', new CompressionFilter()); + $plugins->add('namer', new FileNamer()); + + $bam = new BackupMigrate($plugins); + +See: [Plugins](https://github.com/backupmigrate/backup_migrate_core/tree/master/src/Plugin) + +### Providing Services + +If the consuming application needs to use any plugins that must talk to the greater environment (saving state, emailing +users, creating temporary files) it must provide services to Backup and Migrate that allow it to do so. These services +are contained in an object called the environment. A new environment object should be created and passed to the service +constructor. If you do not pass an environment then a basic one will be created which should work in the simplest +environments. + +Providing an environment. + + use BackupMigrate\Core\Services\BackupMigrate; + use MyAPP\Environment\MyEnvironment; + + // Create a custom environment with whatever services or configuration are needed for the application + $env = new MyEnvironment(...); + + // Pass the environment to the service + $bam = new BackupMigrate($env); + +See: [Environment](https://github.com/backupmigrate/backup_migrate_core/tree/master/src/Environment) + +### Configuring the Object + +The `BackupMigrate` object does not have any configuration but the injected plugins and services may. Services should be configured before they are passed to the `ServiceManager`. Plugins can be configured when they are created and passed to the plugin manager or additional configuration can be passed in by calling `setConfig` on the plugin manager. Often combination of these techniques will be used. Base configuration is passed to the plugin when it is instantiated and run-time configuration is passed in later. + +See: [Configuration](https://github.com/backupmigrate/backup_migrate_core/tree/master/src/Config) + + +## Operations +The Backup and Migrate service provides two main operations: + +* `backup($source_id, $destination_id)` +* `restore($source_id, $destination_id, $file_id)` + +### The Backup Operation + +The `backup()` operation creates a backup file from the specified source, post-processes the file with all installed +filters and saves the file to the specified destination. The parameters for this operation are: + +* **$source_id** ***(string)*** - The id of the source as specified when it is added to the plugin manager. +* **$destination_id** ***(string|array)*** - The id of the destination as specified when it is added to the plugin manager. +This can also be an array of destination ids to send the backup to multiple destinations. + +There is no return value but it may throw an exception if there is an error. + + // ... + + // Create a Backup and Migrate Service object + $bam = new BackupMigrate($plugins); + + // Run the backup. + $bam->backup('db1', 'mydirectory'); + + +### The Restore Operation + +The `restore()` operation loads the specified file from the specified destination, pre-processes the file with all +installed filters and restores the data to the specified source. The parameters are: + +* **$source_id** ***(string)*** - The id of the source as specified when it is added to the plugin manager. +* **$destination_id** ***(string)*** - The id of the destination as specified when it is added to the plugin manager. +* **$file_id** ***(string)*** - The id of the file within the destination. This is usually the file name but can be any +unique string specified by the destination. + + + // ... + + // Create a Backup and Migrate Service object + $bam = new BackupMigrate($plugins); + + // Run the restore. + $bam->restore('db1', 'mydirectory', 'backup.mysql.gz'); diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Plugin/FileProcessorInterface.php b/modules/backup_migrate/lib/backup_migrate_core/src/Plugin/FileProcessorInterface.php new file mode 100644 index 0000000..cb5d93c --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Plugin/FileProcessorInterface.php @@ -0,0 +1,36 @@ +tempfilemanager = $tempfilemanager; + } + + /** + * Get the temp file manager. + * + * @return \BackupMigrate\Core\File\TempFileManagerInterface + */ + public function getTempFileManager() { + return $this->tempfilemanager; + } + + /** + * Provide the file mime for the given file extension if known. + * + * @param string $filemime + * The best guess so far for the file's mime type. + * @param array $params + * A list of parameters where + * 'ext' is the file extension we are testing. + * + * @return string + * The mime type of the file (or the passed in mime type if unknown) + */ + public function alterMime($filemime, $params) { + // Check all of the provided file types for the given extension. + if (method_exists($this, 'getFileTypes')) { + $file_types = $this->getFileTypes(); + foreach ($file_types as $info) { + if (isset($info['extension']) && $info['extension'] == $params['ext'] && isset($info['filemime'])) { + return $info['filemime']; + } + } + } + return $filemime; + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Plugin/PluginBase.php b/modules/backup_migrate/lib/backup_migrate_core/src/Plugin/PluginBase.php new file mode 100644 index 0000000..d10c8ac --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Plugin/PluginBase.php @@ -0,0 +1,66 @@ + ['weight' => 100], + * 'restore' => ['weight' => -100], + * ]; + * + * @return array + */ + public function supportedOps() { + return []; + } + + /** + * Does this plugin implement the given operation. + * + * @param $op string The name of the operation + * + * @return bool + */ + public function supportsOp($op) { + // If the function has the method then it supports the op. + if (method_exists($this, $op)) { + return TRUE; + } + // If the supported ops array contains the op then it is supported. + $ops = $this->supportedOps(); + return isset($ops[$op]); + } + + /** + * What is the weight of the given operation for this plugin. + * * @param $op string The name of the operation. + * + * @return int + */ + public function opWeight($op) { + $ops = $this->supportedOps(); + if (isset($ops[$op]['weight'])) { + return $ops[$op]['weight']; + } + return 0; + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Plugin/PluginCallerInterface.php b/modules/backup_migrate/lib/backup_migrate_core/src/Plugin/PluginCallerInterface.php new file mode 100644 index 0000000..786739a --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Plugin/PluginCallerInterface.php @@ -0,0 +1,34 @@ +plugins = $plugins; + } + + /** + * Get the plugin manager. + * + * @return \BackupMigrate\Core\Plugin\PluginManagerInterface + */ + public function plugins() { + // Return the list of plugins or a blank placeholder. + return $this->plugins ? $this->plugins : new PluginManager(); + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Plugin/PluginInterface.php b/modules/backup_migrate/lib/backup_migrate_core/src/Plugin/PluginInterface.php new file mode 100644 index 0000000..18f082e --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Plugin/PluginInterface.php @@ -0,0 +1,62 @@ + ['weight' => 100], + * 'restore' => ['weight' => -100], + * ]; + * + * @return array + */ + public function supportedOps(); + + /** + * Does this plugin implement the given operation. + * + * @param $op string The name of the operation + * + * @return bool + */ + public function supportsOp($op); + + /** + * What is the weight of the given operation for this plugin. + * * @param $op string The name of the operation. + * + * @return int + */ + public function opWeight($op); + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Plugin/PluginManager.php b/modules/backup_migrate/lib/backup_migrate_core/src/Plugin/PluginManager.php new file mode 100644 index 0000000..89bc789 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Plugin/PluginManager.php @@ -0,0 +1,199 @@ +setServiceManager($services ? $services : new ServiceManager()); + + // Set the configuration or a null object if no config was specified. + $this->setConfig($config ? $config : new Config()); + + // Create an array to store the plugins themselves. + $this->items = []; + } + + + /** + * Set the configuration. Reconfigure all of the installed plugins. + * + * @param \BackupMigrate\Core\Config\ConfigInterface $config + */ + public function setConfig(ConfigInterface $config) { + // Set the configuration object to the one passed in. + $this->config = $config; + + // Pass the appropriate configuration to each of the installed plugins. + foreach ($this->getAll() as $key => $plugin) { + $this->_configurePlugin($plugin, $key); + } + } + + /** + * {@inheritdoc} + */ + public function add($id, PluginInterface $item) { + $this->_preparePlugin($item, $id); + $this->items[$id] = $item; + } + + /** + * {@inheritdoc} + **/ + public function get($id) { + return isset($this->items[$id]) ? $this->items[$id] : NULL; + } + + /** + * {@inheritdoc} + */ + public function getAll() { + return empty($this->items) ? [] : $this->items; + } + + /** + * Get all plugins that implement the given operation. + * + * @param string $op The name of the operation. + * + * @return \BackupMigrate\Core\Plugin\PluginInterface[] + */ + public function getAllByOp($op) { + $out = []; + $weights = []; + + foreach ($this->getAll() as $key => $plugin) { + if ($plugin->supportsOp($op)) { + $out[$key] = $plugin; + $weights[$key] = $plugin->opWeight($op); + } + } + array_multisort($weights, $out); + return $out; + } + + /** + * {@inheritdoc} + */ + public function call($op, $operand = NULL, $params = []) { + + // Run each of the installed plugins which implements the given operation. + foreach ($this->getAllByOp($op) as $plugin) { + $operand = $plugin->{$op}($operand, $params); + } + + return $operand; + } + + /** + * {@inheritdoc} + */ + public function map($op, $params = []) { + $out = []; + + // Run each of the installed plugins which implements the given operation. + foreach ($this->getAllByOp($op) as $key => $plugin) { + $out[$key] = $plugin->{$op}($params); + } + + return $out; + } + + + /** + * Prepare the plugin for use. This is called when a plugin is added to the + * manager and it configures the plugin according to the config object + * injected into the manager. It also injects other dependencies as needed. + * + * @param \BackupMigrate\Core\Plugin\PluginInterface $plugin + * The plugin to prepare for use. + * @param string $id + * The id of the plugin (to extract the correct settings). + */ + protected function _preparePlugin(PluginInterface $plugin, $id) { + // If this plugin can be configured, then pass in the configuration. + $this->_configurePlugin($plugin, $id); + + // Inject the available services. + $this->services()->addClient($plugin); + } + + /** + * Set the configuration for the given plugin. + * + * @param $plugin + * @param $id + */ + protected function _configurePlugin(PluginInterface $plugin, $id) { + // If this plugin can be configured, then pass in the configuration. + if ($plugin instanceof ConfigurableInterface) { + // Configure the plugin with the appropriate subset of the configuration. + $config = $this->confGet($id); + + // Set the config for the plugin. + $plugin->setConfig(new Config($config)); + + // Get the configuration back from the plugin to populate defaults within the manager. + $this->config()->set($id, $plugin->config()); + } + } + + /** + * @return ServiceManagerInterface + */ + public function services() { + return $this->services; + } + + /** + * @param ServiceManagerInterface $services + */ + public function setServiceManager($services) { + $this->services = $services; + + // Inject or re-inject the services. + foreach ($this->getAll() as $key => $plugin) { + $this->services()->addClient($plugin); + } + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Plugin/PluginManagerInterface.php b/modules/backup_migrate/lib/backup_migrate_core/src/Plugin/PluginManagerInterface.php new file mode 100644 index 0000000..a87c171 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Plugin/PluginManagerInterface.php @@ -0,0 +1,107 @@ +plugins()->add('demoplugin', new MyPlugin()); + +To configure this plugin the consuming application would have a section called 'demoplugin' in the plugin manager configuration object: + + $conf = new Config([ + 'demoplugin' => ['foo => 'bar'] + ]); + + $plugins = new PluginManager(NULL, $conf); + $backup_migrate = new BackupMigrate($plugins); + +### Calling Plugins ### +Internally the plugin manager is used to run all plugins for a given operation. This is done using the `call()` method: + + $file = $this->plugins()->call('afterBackup', $file); + +The call method takes 3 parameters: + +* **Operation**: the name of the operation to call +* **Operand**: The object being operated on (optional) +* **Params**: An associative array of additional parameters + +Each plugin that implements the **operation** will be called in order. The **operand** will be passed to the plugin and will be overwritten by the return value from the plugin. In this way plugin operations are chained. A plugin is responsible for returning the operand that was passed in if it does not wish to overwrite it. The **params** array can contain additional information needed to run the operation but it cannot be modified by plugins. + +### Implementing Operations ### +If a plugin wishes to be called for a given operation it simply needs to define a method with the same name as the operation. For example, to compress a backup file after it has been created, the plugin must have a method called `afterBackup()` which takes a file as the operand and returns the a new, compressed file. + +#### Operation Weights #### +The order in which plugins are called cannot be guaranteed. However, if a plugin needs to run in a specific order it may specify a weight for each operation it implements. To specify a weight it must implement a `opWeight()` method which takes an operation name and returns a numerical weight. Plugins are called from lowest to highest and plugins which do not specify a weight are considered to have a weight of `0`. + +To specify the weight of may operations it may be easier to extend the `\BackupMigrate\Core\Plugin\PluginBase` class and override the `supportedOps()` method which returns an array of supported operations and their weight: + + public function supportedOps() { + return [ + 'afterBackup' => ['weight' => 100], + 'beforeRestore' => ['weight' => -100], + ]; + } + +### Calling Other Plugins ### +Plugins can call other plugins using the Plugin Manager. For example, a source plugin might want to expose a line-item filter operation to allow other plugins to alter single values before they are added to the backup file. An encryption plugin may want to delegate the actual work of encrypting to other sub-plugins for better code organization and extendability. + +By default plugins are not given access to the plugin manager. However, if a plugin implements the `\BackupMigrate\Core\Plugin\PluginCallerInterface` then the plugin manager will inject itself into the plugin for use when the plugin is prepared for use. The `\BackupMigrate\Core\Plugin\PluginCallerTrait` can be used to implement the actual requirements of the interface. Plugins with this interface and trait will be able to use `$this->plugins()` to access the plugin manager: + + class MyPlugin implements PluginCallerInterface { + use PluginCallerTrait; + + function someOperation() { + $this->plugins()->call(...); + } + } + +### Accessing Services ### +If a plugin requires the use of a cache, logger, state storage, mailer or any other backing service it must have the service injected into it by the plugin manger. To make a service avaible to the plugin manager it may be added to an object which implenents `ServiceManagerInterface`. That service locater may be passed to the plugin manager though the constructor or it can be passed in later using `setServiceManager()`. + +Any service provided by the service locator will be injected into a plugin when it is added to the plugin manager if the name of the service matches a setter present in the plugin. For example: if a plugin has a method called `setLogger` and the service locator has a service called 'Logger' then the logger service will be injected via the `setLogger` method: + + $services = new ServiceManager(); + $services->add('Logger', new FileLogger('/path/to/log.txt')); + + $plugins = new PluginManager($services); + + // If this plugin has a `setLogger` the logger will be injected. + $plugins->add('test', new TestPlugin()); + +See: [Services](https://github.com/backupmigrate/backup_migrate_core/tree/master/src/Service) + +### Creating New Temporary Files ### +If a plugin needs to create a new temporary file (for example to decompress a backup file). It may request that the TempFileManager be injected by implementing `\BackupMigrate\Core\Plugin\FileProcessorInterface` and using the `\BackupMigrate\Core\Plugin\FileProcessorTrait`. This will allow the following: + + class MyFilePlugin implements FileProcessorInterface { + use FileProcessorTrait; + + function someOperation($file_in) { + $file_out = $this->getTempFileManager()->popExt($file_in); + // ... + + // Return the new file and so it overwrites the old file + // during plugin chaining. + return $file_out; + } + } + + +See: [Backup Files](https://github.com/backupmigrate/backup_migrate_core/tree/master/src/File) + +## Sources and Destinations ## + +Sources and destinations are special case plugins. While they technically identical to filter plugins they are not called using the plugin manager's `call()` method. Only one source and one destination can be use for each backup or restore operation so they are called individually rather than being chained like most plugin operations. These plugin types are different by convention only and are injected and configured in the same way as filters. + +See: [Sources](https://github.com/backupmigrate/backup_migrate_core/tree/master/src/Source), [Destinations](https://github.com/backupmigrate/backup_migrate_core/tree/master/src/Destination) diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Service/ArchiveReaderInterface.php b/modules/backup_migrate/lib/backup_migrate_core/src/Service/ArchiveReaderInterface.php new file mode 100644 index 0000000..291236b --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Service/ArchiveReaderInterface.php @@ -0,0 +1,50 @@ +secret_key = $secret_key; + $this->api_endpoint = $api_endpoints; + } + + /** + * Get the list of backups from the API. + * + * @return \array[] An array of assocative arrays of the file info + */ + public function listFiles() { + return $this->call('backups.listFiles', [$this->getSiteID()]); + } + + /** + * Send a readable backup file to NodeSquirrel if the site limits allow it. + * + * @param \BackupMigrate\Core\File\BackupFileReadableInterface $file + * + * @return \BackupMigrate\Core\File\BackupFileReadableInterface + * + * @throws \BackupMigrate\Core\Exception\BackupMigrateException + */ + public function uploadFile(BackupFileReadableInterface $file) { + $site_id = $this->getSiteID(); + $filename = $file->getFullName(); + $filesize = $file->getMeta('filesize'); + + // Get an upload ticket. + try { + $ticket = $this->call('backups.getUploadTicket', [$site_id, $filename, $filesize, $file->getMetaAll()]); + } + catch (BackupMigrateException $e) { + throw new BackupMigrateException( + 'Could not initiate an upload to NodeSquirrel. Error: %err (code: %code)', + ['%err' => $e->getMessage(), '%code' => $e->getCode()] + ); + } + + // Post the file. + try { + $this->getHttpClient()->postFile($ticket['url'], $file, $ticket['params']); + } + catch (BackupMigrateException $e) { + throw new BackupMigrateException( + 'Could not upload to NodeSquirrel: %err (code: %code)', + ['%err' => $e->getMessage(), '%code' => $e->getCode()] + ); + } + + // Confirm the upload. + try { + $this->call('backups.confirmUpload', [$site_id, $filename, $filesize]); + } + catch (BackupMigrateException $e) { + throw new BackupMigrateException( + 'Could not confirm the upload to NodeSquirrel: %err (code: %code)', + ['%err' => $e->getMessage(), '%code' => $e->getCode()] + ); + } + } + + /** + * Send a delete call to the API. + * + * @param $id + * + * @return mixed + */ + public function deleteFile($id) { + return $this->call('backups.deleteFile', [$this->getSiteID(), $id]); + } + + /** + * Call a method on the API. + * + * @param string $method + * @param array $args + * + * @return mixed + * + * @throws \BackupMigrate\Core\Exception\BackupMigrateException + */ + public function call($method, $args) { + // Add the key authentication arguments if we can. + $args = $this->signRequest($args); + // Call the API using xmlrpc. + return $this->xmlrpcCall($method, $args, $this->getEndpoints()); + } + + /** + * Do the actual call. The args must be signed with a secret key already. + * + * It may call itself to fetch new endpoint URLS if needed. The retry argument + * prevents an infinite loop if new endpoints cannot be retrieved. + * + * @param $method + * @param $args + * @param $endpoints + * @param int $retry + * + * @return mixed + * + * @throws \BackupMigrate\Core\Exception\BackupMigrateException + */ + protected function xmlrpcCall($method, $args, $endpoints, $retry = 3) { + if ($endpoints && --$retry > 0) { + $endpoint = reset($endpoints); + + // Try each available server in order. + while ($endpoint) { + // Add the protocol to the url. + if (strpos($endpoint, 'http') !== 0) { + $endpoint = 'https://' . $endpoint; + } + + // Do the actual call. + try { + // Encode the request. + $post = xmlrpc_encode_request($method, $args); + // Post the request. + $out = $this->getHttpClient()->post($endpoint, $post); + // Decode the response. + $out = xmlrpc_decode($out); + + // Check for xml errors. + if (isset($out['faultCode'])) { + throw new BackupMigrateException($out['faultString'], [], $out['faultCode']); + } + + return $out; + } + catch (BackupMigrateException $e) { + // Deal with errors. + switch ($e->getCode()) { + case '500': + case '503': + case '404': + // Some sort of server error. Try the next one. + $endpoint = next($endpoints); + + // If we're at the end of the line then try refetching the urls. + if (!$endpoint) { + $endpoints = $this->fetchEndpoints(TRUE, $retry); + return $this->xmlrpcCall($method, $args, $endpoints, $retry); + } + break; + + case '300': + // 'Multiple Choices' means that the existing server list needs to be refreshed. + $servers = $this->fetchEndpoints(TRUE, $retry); + return $this->xmlrpcCall($method, $args, $servers, $retry); + + break; + case '401': + case '403': + // Authentication failed. + throw new BackupMigrateException('Couldn\'t log in to NodeSquirrel. The server error was: %err', + ['%err' => $e->getMessage()]); + + break; + default: + // Some sort of client error. Don't try the next server because it'll probably say the same thing. + throw new BackupMigrateException('The NodeSquirrel server returned the following error: %err', + ['%err' => $e->getMessage()]); + break; + } + } + } + } + } + + /** + * @param string $secret_key + */ + public function setSecretKey($secret_key) { + $this->secret_key = $secret_key; + } + + /** + * Do the actual XMLRPC call. + * + * @param $endpoint + * @param $method + * @param $args + * + * @return array + * + * @throws \BackupMigrate\Core\Exception\BackupMigrateException + */ + protected function doXmlrpcCall($endpoint, $method, $args) { + if (!function_exists('xmlrpc_encode')) { + throw new BackupMigrateException('NodeSquirrel requires the php XML-RPC extension.'); + } + + // Encode the request. + $post = xmlrpc_encode_request($method, $args); + // Post the request. + $out = $this->getHttpClient()->postData($endpoint, $post); + // Decode the response. + $out = xmlrpc_decode($out); + return $out; + } + + /** + * Sign a set of method arguments with our secret key. + * + * @param $args + * + * @return bool + */ + protected function signRequest($args) { + $crypto = $this->getCryptoValues(); + $hash = $this->getHash($crypto['time'], $crypto['nonce']); + if ($hash) { + array_unshift($args, $crypto['nonce']); + array_unshift($args, $crypto['time']); + array_unshift($args, $hash); + return $args; + } + else { + return FALSE; + } + } + + /** + * Get a hash to use as a secure 1-time signature for a request. + * + * @param $time + * @param $nonce + * + * @return string + * + * @throws \BackupMigrate\Core\Exception\BackupMigrateException + */ + protected function getHash($time, $nonce) { + if ($private_key = $this->getPrivateKey()) { + $message = $time . ':' . $nonce . ':' . $private_key; + // Use HMAC-SHA1 to authenticate the call. + $hash = base64_encode( + pack('H*', + sha1((str_pad($private_key, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), + 64))) . + pack('H*', + sha1((str_pad($private_key, 64, chr(0x00)) ^ (str_repeat(chr(0x36), + 64))) . + $message)))) + ); + return $hash; + } + throw new BackupMigrateException('You must enter a valid secret key to use NodeSquirrel.', + []); + } + + /** + * Get the variable inputs to the hash function. This let's us stub this with known values during testing. + * + * @return array + */ + protected function getCryptoValues() { + if ($this->crypto_values) { + return $this->crypto_values; + } + return [ + 'nonce' => md5(mt_rand()), + 'time' => time(), + ]; + } + + /** + * Can be used to fix the random/timebased signing values. + * + * Should only be used for testing purposes. + * + * @param $values + */ + public function setCryptoValues($values) { + $this->crypto_values = $values; + } + + /** + * Retrieve the list of servers by making an rpc call to the servers we know about. + */ + function refetchEndpoints($refresh = FALSE, $retry = 3) { + // TODO: Implement this as it needs local caching to be effective. + return []; + } + + /** + * @return \BackupMigrate\Core\Service\HttpClientInterface + * @throws \BackupMigrate\Core\Exception\BackupMigrateException + */ + public function getHttpClient() { + if (!$this->http_client) { + $this->http_client = new PhpCurlHttpClient(); + } + return $this->http_client; + } + + /** + * @param HttpClientInterface $http_client + */ + public function setHttpClient(HttpClientInterface $http_client) { + $this->http_client = $http_client; + } + + + /** + * Get a list of API endpoint urls (without the protocol). + * + * @return mixed + */ + protected function getEndpoints() { + return $this->api_endpoint; + } + + /** + * Get the secret key. + * + * @return string + */ + protected function getSecretKey() { + return $this->secret_key; + } + + /** + * Get the site id from the secret key. + * + * @return mixed + */ + protected function getSiteID() { + list($id,) = explode(':', $this->getSecretKey()); + return $id; + } + + /** + * Get the site id from the secret key. + * + * @return mixed + */ + protected function getPrivateKey() { + list(, $key) = explode(':', $this->getSecretKey()); + return $key; + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Service/PhpCurlHttpClient.php b/modules/backup_migrate/lib/backup_migrate_core/src/Service/PhpCurlHttpClient.php new file mode 100644 index 0000000..7532c27 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Service/PhpCurlHttpClient.php @@ -0,0 +1,98 @@ +getCurlResource($url); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data); + + return $this->curlExec($ch); + } + + /** + * Post a file along with other data (as an array). + * + * @param $url + * @param \BackupMigrate\Core\File\ReadableStreamBackupFile $file + * @param $data + * + * @return mixed + */ + public function postFile($url, ReadableStreamBackupFile $file, $data) { + $data['file'] = new \CURLFile($file->realpath()); + $data['file']->setPostFilename($file->getFullName()); + return $this->post($url, $data); + } + + /** + * Get the CURL Resource with default options. + * + * @param $url + * + * @return resource + */ + protected function getCurlResource($url) { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_FAILONERROR, TRUE); + return $ch; + } + + /** + * Perform the http action and return the body or throw an exception. + * + * @param $ch + * + * @return mixed + * + * @throws \BackupMigrate\Core\Exception\HttpClientException + */ + protected function curlExec($ch) { + $body = curl_exec($ch); + if ($msg = curl_error($ch)) { + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if (!$code) { + $info['code'] = curl_errno($ch); + } + throw new HttpClientException($msg, [], $code); + } + return $body; + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Service/README.md b/modules/backup_migrate/lib/backup_migrate_core/src/Service/README.md new file mode 100644 index 0000000..e1bbf8d --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Service/README.md @@ -0,0 +1,35 @@ +# Services # + +If a plugin needs to access the greater environment to write logs, store data, etc. it should rely on service objects which may be injected into the plugin at run time. + +## Service Manager ## + +The `ServiceManagerInterface` defines a very simple service container and dependency injector which stores a keyed list of services available to plugins which need them. The built in `ServiceManager` class implements this interface in the most basic way possible. A consuming application may choose to implement a manager using a more sophisticated dependency management and configuration solution such as [Pimple](http://pimple.sensiolabs.org/), [PHP-DI](http://php-di.org/) or [Symfony's DependencyInjection Component](http://symfony.com/doc/current/components/dependency_injection/introduction.html). The built in locator simply takes a list of already configured services and returns them when requested or automatically injects them as described below. + +## Service Injection ## + +It is not necessary to use automatic service injection. A consuming application can simply instantiate plugins and pass the necessary services directly to them. However, a simple service injection mechanism is provided by the service manager which can make dynamically creating plugins much simpler. + +Plugins can request that a service be injected by defining a setter with called `setServiceName` where 'ServiceName' is replaced with the name of the given service. Here is an pseudo-code example: + + class MyPlugin implements PluginInterface { + + // Logger service setter + public function setLogger(LoggerInterface $logger) { + $this->logger = $logger; + } + + // ... + } + +This plugin will have a logger injected if one is available: + + $bam = new BackupMigrate(); + + // The key 'Logger' must match 'setLogger' + $bam->services()->add('Logger', new MyLogger()); + + // The manager will inject the logger automatically. + $bam->plugins()->add('myplugin', new MyPlugin()); + + \ No newline at end of file diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Service/ServiceManager.php b/modules/backup_migrate/lib/backup_migrate_core/src/Service/ServiceManager.php new file mode 100644 index 0000000..c141f4a --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Service/ServiceManager.php @@ -0,0 +1,104 @@ +services = []; + + // Allow the locator to inject itself. + $this->services['ServiceManager'] = $this; + } + + /** + * Add a fully configured service to the service locator. + * + * @param string $type + * The service type identifier. + * @param mixed $service + * + * @return null + */ + public function add($type, $service) { + $this->services[$type] = $service; + + // Add this service as a client so it can have dependencies injected. + $this->addClient($service); + + // Update any plugins that have already had this service injected. + if (isset($this->clients[$type])) { + foreach ($this->clients[$type] as $client) { + $client->{'set' . $type}($service); + } + } + } + + /** + * Retrieve a service from the locator. + * + * @param string $type + * The service type identifier + * + * @return mixed + */ + public function get($type) { + return $this->services[$type]; + } + + /** + * Get an array of keys for all available services. + * + * @return array + */ + public function keys() { + return array_keys($this->services); + } + + /** + * Inject all available services into the give plugin. + * + * @param object $client + * + * @return mixed|void + */ + public function addClient($client) { + // Inject available services. + foreach ($this->keys() as $type) { + if (method_exists($client, 'set' . $type) && $service = $this->get($type)) { + // Save the plugin so it can be updated if this service is updated. + $this->clients[$type][] = $client; + + $client->{'set' . $type}($service); + } + } + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Service/ServiceManagerInterface.php b/modules/backup_migrate/lib/backup_migrate_core/src/Service/ServiceManagerInterface.php new file mode 100644 index 0000000..1bbcee6 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Service/ServiceManagerInterface.php @@ -0,0 +1,43 @@ +logs[] = ['level' => $level, 'message' => $message, 'context' => $context]; + } + + /** + * Get all of the log messages that were saved to this stash. + * + * @return array + */ + public function getAll() { + return $this->logs; + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Service/TarArchiveReader.php b/modules/backup_migrate/lib/backup_migrate_core/src/Service/TarArchiveReader.php new file mode 100644 index 0000000..fe311b6 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Service/TarArchiveReader.php @@ -0,0 +1,400 @@ +archive = $out; + } + + + /** + * Extract all files to the given directory. + * + * @param $directory + * + * @return mixed + */ + public function extractTo($directory) { + $this->archive->openForRead(TRUE); + + $result = $this->extractAllToDirectory($directory); + + $this->archive->close(); + + return $result; + } + + /** + * @param $directory + * The directory to extract the files to. + * @return bool + * @throws \BackupMigrate\Core\Exception\BackupMigrateException + */ + private function extractAllToDirectory($directory) { + clearstatcache(); + + // Read a header block. + while (strlen($block = $this->archive->readBytes(512)) != 0) { + $header = $this->readHeader($block); + if (!$header) { + return FALSE; + } + + if ($header['filename'] == '') { + continue; + } + + // Check for potentially malicious files (containing '..' etc.). + if ($this->maliciousFilename($header['filename'])) { + throw new BackupMigrateException( + 'Malicious .tar detected, file %filename. Will not install in desired directory tree', + ['%filename' => $header['filename']] + ); + } + + // ignore extended / pax headers. + if ($header['typeflag'] == 'x' || $header['typeflag'] == 'g') { + $this->archive->seekBytes(ceil(($header['size'] / 512))); + continue; + } + + // Add the destination directory to the path. + if (substr($header['filename'], 0, 1) == '/') { + $header['filename'] = $directory . $header['filename']; + } + else { + $header['filename'] = $directory . '/' . $header['filename']; + } + + // If the file already exists, make sure we can overwrite it. + if (file_exists($header['filename'])) { + // Cannot overwrite a directory with a file. + if ((@is_dir($header['filename'])) + && ($header['typeflag'] == '') + ) { + throw new BackupMigrateException( + 'File %filename already exists as a directory', + ['%filename' => $header['filename']] + ); + } + // Cannot overwrite a file with a directory. + if (@is_file($header['filename']) && !@is_link($header['filename']) + && ($header['typeflag'] == "5") + ) { + throw new BackupMigrateException( + 'Directory %filename already exists as file', + ['%filename' => $header['filename']] + ); + } + // Cannot overwrite a read-only file. + if (!is_writeable($header['filename'])) { + throw new BackupMigrateException( + 'File %filename already exists and is write protected', + ['%filename' => $header['filename']] + ); + } + } + + // Extract a directory. + if ($header['typeflag'] == "5") { + if (!$this->createDir($header['filename'])) { + throw new BackupMigrateException( + 'Unable to create directory %filename', + ['%filename' => $header['filename']] + ); + } + } + // Extract a file/symlink + else { + if (!$this->createDir(dirname($header['filename']))) { + throw new BackupMigrateException( + 'Unable to create directory for %filename', + ['%filename' => $header['filename']] + ); + } + + // Symlink. + if ($header['typeflag'] == "2") { + if (@file_exists($header['filename'])) { + @unlink($header['filename']); + } + if (!@symlink($header['link'], $header['filename'])) { + throw new BackupMigrateException( + 'Unable to extract symbolic link: %filename', + ['%filename' => $header['filename']] + ); + } + } + // Regular file. + else { + // Open the file for writing. + if (($dest_file = @fopen($header['filename'], "wb")) == 0) { + throw new BackupMigrateException( + 'Error while opening %filename in write binary mode', + ['%filename' => $header['filename']] + ); + } + + // Write the file. + $n = floor($header['size'] / 512); + for ($i = 0; $i < $n; $i++) { + $content = $this->archive->readBytes(512); + fwrite($dest_file, $content, 512); + } + if (($header['size'] % 512) != 0) { + $content = $this->archive->readBytes(512); + fwrite($dest_file, $content, ($header['size'] % 512)); + } + + @fclose($dest_file); + + // Change the file mode, mtime. + @touch($header['filename'], $header['mtime']); + if ($header['mode'] & 0111) { + // make file executable, obey umask. + $mode = fileperms($header['filename']) | (~umask() & 0111); + @chmod($header['filename'], $mode); + } + + clearstatcache(); + + // Check if the file exists. + if (!is_file($header['filename'])) { + throw new BackupMigrateException( + 'Extracted file %filename does not exist. Archive may be corrupted.', + ['%filename' => $header['filename']] + ); + } + + // Check the file size. + $file_size = filesize($header['filename']); + if ($file_size != $header['size']) { + throw new BackupMigrateException( + 'Extracted file %filename does not have the correct file size. File is %actual bytes (%expected bytes expected). Archive may be corrupted', + ['%filename' => $header['filename'], '%expected' => (int) $header['size'], (int) '%actual' => $file_size] + ); + } + } + } + } + + return TRUE; + } + + /** + * Create a directory or return true if it already exists. + * + * @param $directory + * + * @return boolean + */ + private function createDir($directory) { + if ((@is_dir($directory)) || ($directory == '')) { + return TRUE; + } + $parent = dirname($directory); + + if ( + ($parent != $directory) && + ($parent != '') && + (!$this->createDir($parent)) + ) { + return FALSE; + } + if (@!mkdir($directory, 0777)) { + return FALSE; + } + return TRUE; + } + + /** + * Read a tar file header block. + * + * @param $block + * @param array $header + * + * @return array + * + * @throws \BackupMigrate\Core\Exception\BackupMigrateException + */ + private function readHeader($block, $header = []) { + if (strlen($block) == 0) { + $header['filename'] = ''; + return TRUE; + } + + if (strlen($block) != 512) { + $header['filename'] = ''; + throw new BackupMigrateException( + 'Invalid block size: %size bytes', + ['%size' => strlen($block)] + ); + } + + if (!is_array($header)) { + $header = []; + } + + // Calculate the checksum. + $checksum = 0; + // First part of the header. + for ($i = 0; $i < 148; $i++) { + $checksum += ord(substr($block, $i, 1)); + } + // Ignore the checksum value and replace it by ' ' (space). + for ($i = 148; $i < 156; $i++) { + $checksum += ord(' '); + } + // Last part of the header. + for ($i = 156; $i < 512; $i++) { + $checksum += ord(substr($block, $i, 1)); + } + + if (version_compare(PHP_VERSION, "5.5.0-dev") < 0) { + $fmt = "a100filename/a8mode/a8uid/a8gid/a12size/a12mtime/" . + "a8checksum/a1typeflag/a100link/a6magic/a2version/" . + "a32uname/a32gname/a8devmajor/a8devminor/a131prefix"; + } + else { + $fmt = "Z100filename/Z8mode/Z8uid/Z8gid/Z12size/Z12mtime/" . + "Z8checksum/Z1typeflag/Z100link/Z6magic/Z2version/" . + "Z32uname/Z32gname/Z8devmajor/Z8devminor/Z131prefix"; + } + $data = unpack($fmt, $block); + + if (strlen($data["prefix"]) > 0) { + $data["filename"] = "$data[prefix]/$data[filename]"; + } + + // Extract the checksum. + $header['checksum'] = octdec(trim($data['checksum'])); + if ($header['checksum'] != $checksum) { + $header['filename'] = ''; + + // Look for last block (empty block). + if (($checksum == 256) && ($header['checksum'] == 0)) { + return $header; + } + + throw new BackupMigrateException( + 'Invalid checksum for file %filename', + ['%filename' => $data['filename']] + ); + } + + // Extract the properties. + $header['filename'] = rtrim($data['filename'], "\0"); + $header['mode'] = octdec(trim($data['mode'])); + $header['uid'] = octdec(trim($data['uid'])); + $header['gid'] = octdec(trim($data['gid'])); + $header['size'] = octdec(trim($data['size'])); + $header['mtime'] = octdec(trim($data['mtime'])); + if (($header['typeflag'] = $data['typeflag']) == "5") { + $header['size'] = 0; + } + $header['link'] = trim($data['link']); + + // Look for long filename. + if ($header['typeflag'] == 'L') { + $header = $this->readLongHeader($header); + } + + return $header; + } + + /** + * Read a tar file header block for files with long names. + * + * @param $header + * + * @return array + * + * @throws \BackupMigrate\Core\Exception\BackupMigrateException + */ + private function readLongHeader($header) { + $filename = ''; + $filesize = $header['size']; + $n = floor($header['size'] / 512); + for ($i = 0; $i < $n; $i++) { + $content = $this->archive->readBytes(512); + $filename .= $content; + } + if (($header['size'] % 512) != 0) { + $content = $this->archive->readBytes(512); + $filename .= $content; + } + + $filename = rtrim(substr($filename, 0, $filesize), "\0"); + + // Read the next header. + $data = $this->archive->readBytes(512); + $header = $this->readHeader($data, $header); + $header['filename'] = $filename; + + return $header; + } + + /** + * Detect and report a malicious file name. + * + * @param string $file + * + * @return bool + */ + private function maliciousFilename($file) { + if (strpos($file, '/../') !== FALSE) { + return TRUE; + } + if (strpos($file, '../') === 0) { + return TRUE; + } + return FALSE; + } + + /** + * This will be called when all files have been added. It gives the implementation + * a chance to clean up and commit the changes if needed. + * + * @return mixed + */ + public function closeArchive() { + if ($this->archive) { + $this->archive->close(); + } + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Service/TarArchiveWriter.php b/modules/backup_migrate/lib/backup_migrate_core/src/Service/TarArchiveWriter.php new file mode 100644 index 0000000..efcd36b --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Service/TarArchiveWriter.php @@ -0,0 +1,212 @@ +archive = $out; + } + + /** + * {@inheritdoc} + */ + public function addFile($real_path, $new_path = '') { + $this->archive->openForWrite(TRUE); + + $new_path = $new_path ? $new_path : $real_path; + + $this->writeHeader($real_path, $new_path); + + $fp = @fopen($real_path, "rb"); + while (($v_buffer = fread($fp, 512)) != '') { + $v_binary_data = pack("a512", "$v_buffer"); + $this->archive->write($v_binary_data); + } + fclose($fp); + } + + /** + * @param $real_path + * @param $new_path + * @return bool + */ + protected function writeHeader($real_path, $new_path) { + if (strlen($new_path) > 99) { + $this->writeLongHeader($new_path); + } + + $v_info = lstat($real_path); + + $v_uid = sprintf("%6s ", decoct($v_info[4])); + $v_gid = sprintf("%6s ", decoct($v_info[5])); + $v_perms = sprintf("%6s ", decoct($v_info['mode'])); + $v_mtime = sprintf("%11s", decoct($v_info['mtime'])); + + $v_linkname = ''; + + if (@is_link($real_path)) { + $v_typeflag = '2'; + $v_linkname = readlink($real_path); + $v_size = sprintf("%11s ", decoct(0)); + } + elseif (@is_dir($real_path)) { + $v_typeflag = "5"; + $v_size = sprintf("%11s ", decoct(0)); + } + else { + $v_typeflag = ''; + clearstatcache(TRUE, $real_path); + $v_size = sprintf("%11s ", decoct($v_info['size'])); + } + + $v_magic = ''; + $v_version = ''; + $v_uname = ''; + $v_gname = ''; + $v_devmajor = ''; + $v_devminor = ''; + $v_prefix = ''; + + $v_binary_data_first = pack("a100a8a8a8a12A12", + $new_path, $v_perms, $v_uid, + $v_gid, $v_size, $v_mtime); + $v_binary_data_last = pack("a1a100a6a2a32a32a8a8a155a12", + $v_typeflag, $v_linkname, $v_magic, + $v_version, $v_uname, $v_gname, + $v_devmajor, $v_devminor, $v_prefix, ''); + + // ----- Calculate the checksum. + $v_checksum = 0; + // ..... First part of the header. + for ($i = 0; $i < 148; $i++) { + $v_checksum += ord(substr($v_binary_data_first, $i, 1)); + } + // ..... Ignore the checksum value and replace it by ' ' (space). + for ($i = 148; $i < 156; $i++) { + $v_checksum += ord(' '); + } + // ..... Last part of the header. + for ($i = 156, $j = 0; $i < 512; $i++, $j++) { + $v_checksum += ord(substr($v_binary_data_last, $j, 1)); + } + + // ----- Write the first 148 bytes of the header in the archive. + $this->archive->write($v_binary_data_first, 148); + + // ----- Write the calculated checksum. + $v_checksum = sprintf("%6s ", decoct($v_checksum)); + $v_binary_data = pack("a8", $v_checksum); + $this->archive->write($v_binary_data, 8); + + // ----- Write the last 356 bytes of the header in the archive. + $this->archive->write($v_binary_data_last, 356); + } + + /** + * @param $new_path + * @return bool + */ + function writeLongHeader($new_path) { + $v_size = sprintf("%11s ", decoct(strlen($new_path))); + + $v_typeflag = 'L'; + $v_linkname = ''; + $v_magic = ''; + $v_version = ''; + $v_uname = ''; + $v_gname = ''; + $v_devmajor = ''; + $v_devminor = ''; + $v_prefix = ''; + + $v_binary_data_first = pack("a100a8a8a8a12A12", + '././@LongLink', 0, 0, 0, $v_size, 0); + $v_binary_data_last = pack("a1a100a6a2a32a32a8a8a155a12", + $v_typeflag, $v_linkname, $v_magic, + $v_version, $v_uname, $v_gname, + $v_devmajor, $v_devminor, $v_prefix, ''); + + // ----- Calculate the checksum. + $v_checksum = 0; + // ..... First part of the header. + for ($i = 0; $i < 148; $i++) { + $v_checksum += ord(substr($v_binary_data_first, $i, 1)); + } + // ..... Ignore the checksum value and replace it by ' ' (space). + for ($i = 148; $i < 156; $i++) { + $v_checksum += ord(' '); + } + // ..... Last part of the header. + for ($i = 156, $j = 0; $i < 512; $i++, $j++) { + $v_checksum += ord(substr($v_binary_data_last, $j, 1)); + } + + // ----- Write the first 148 bytes of the header in the archive. + $this->archive->write($v_binary_data_first, 148); + + // ----- Write the calculated checksum. + $v_checksum = sprintf("%6s ", decoct($v_checksum)); + $v_binary_data = pack("a8", $v_checksum); + $this->archive->write($v_binary_data, 8); + + // ----- Write the last 356 bytes of the header in the archive. + $this->archive->write($v_binary_data_last, 356); + + // ----- Write the filename as content of the block. + $i = 0; + while (($v_buffer = substr($new_path, (($i++) * 512), 512)) != '') { + $v_binary_data = pack("a512", "$v_buffer"); + $this->archive->write($v_binary_data); + } + } + + /** + * Write a footer to mark the end of the archive. + */ + private function writeFooter() { + // ----- Write the last 0 filled block for end of archive. + $v_binary_data = pack('a1024', ''); + $this->archive->write($v_binary_data); + } + + /** + * {@inheritdoc} + */ + public function closeArchive() { + $this->writeFooter(); + $this->archive->close(); + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Service/TeeLogger.php b/modules/backup_migrate/lib/backup_migrate_core/src/Service/TeeLogger.php new file mode 100644 index 0000000..f4d1582 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Service/TeeLogger.php @@ -0,0 +1,69 @@ +setLoggers($loggers); + } + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * + * @return null + */ + public function log($level, $message, array $context = []) { + foreach ($this->getLoggers() as $logger) { + $logger->log($level, $message, $context); + } + } + + /** + * @return \Psr\Log\LoggerInterface[] + */ + public function getLoggers() { + return $this->loggers; + } + + /** + * @param \Psr\Log\LoggerInterface[] $loggers + */ + public function setLoggers($loggers) { + $this->loggers = $loggers; + } + + /** + * @param \Psr\Log\LoggerInterface $logger + */ + public function addLogger(LoggerInterface $logger) { + $this->loggers[] = $logger; + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Source/DatabaseSource.php b/modules/backup_migrate/lib/backup_migrate_core/src/Source/DatabaseSource.php new file mode 100644 index 0000000..594e308 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Source/DatabaseSource.php @@ -0,0 +1,123 @@ + 'text', + 'title' => 'Hostname' + ]; + $schema['fields']['database'] = [ + 'type' => 'text', + 'title' => 'Database' + ]; + $schema['fields']['username'] = [ + 'type' => 'text', + 'title' => 'Username', + ]; + $schema['fields']['password'] = [ + 'type' => 'password', + 'title' => 'Password' + ]; + $schema['fields']['port'] = [ + 'type' => 'number', + 'min' => 1, + 'max' => 65535, + 'title' => 'Port', + ]; + } + + return $schema; + } + + /** + * Get the default values for the plugin. + * + * @return \BackupMigrate\Core\Config\Config + */ + public function configDefaults() { + return new Config([ + 'generator' => 'Backup and Migrate Core', + ]); + } + + /** + * Get a list of tables in this source. + */ + public function getTableNames() { + try { + return $this->_getTableNames(); + } + catch (\Exception $e) { + // Todo: Log this exception. + return []; + } + } + + /** + * Get an array of tables with some info. Each entry must have at least a + * 'name' key containing the table name. + * + * @return array + */ + public function getTables() { + try { + return $this->_getTables(); + } + catch (\Exception $e) { + // Todo: Log this exception. + return []; + } + } + + + /** + * Get the list of tables from this db. + * + * @return array + */ + protected function _getTableNames() { + $out = []; + foreach ($this->_getTables() as $table) { + $out[$table['name']] = $table['name']; + } + return $out; + } + + /** + * Internal overridable function to actually generate table info. + * + * @return array + */ + abstract protected function _getTables(); + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Source/DatabaseSourceInterface.php b/modules/backup_migrate/lib/backup_migrate_core/src/Source/DatabaseSourceInterface.php new file mode 100644 index 0000000..9655b56 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Source/DatabaseSourceInterface.php @@ -0,0 +1,28 @@ + [], + 'importFromFile' => [] + ]; + } + + /** + * {@inheritdoc} + */ + public function exportToFile() { + if ($directory = $this->confGet('directory')) { + // Make sure the directory ends in exactly 1 slash: + if (substr($directory, -1) !== '/') { + $directory = $directory . '/'; + } + + if (!$writer = $this->getArchiveWriter()) { + throw new BackupMigrateException('A file directory source requires an archive writer object.'); + } + $ext = $writer->getFileExt(); + $file = $this->getTempFileManager()->create($ext); + + if ($files = $this->getFilesToBackup($directory)) { + $writer->setArchive($file); + foreach ($files as $new => $real) { + $writer->addFile($real, $new); + } + $writer->closeArchive(); + return $file; + } + throw new BackupMigrateException('The directory %dir does not not have any files to be backed up.', + ['%dir' => $directory]); + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function importFromFile(BackupFileReadableInterface $file) { + if ($directory = $this->confGet('directory')) { + // Make sure the directory ends in exactly 1 slash: + if (substr($directory, -1) !== '/') { + $directory = $directory . '/'; + } + + if (!file_exists($directory)) { + throw new BackupMigrateException('The directory %dir does not exist to restore to.', + ['%dir' => $directory]); + } + if (!is_writable($directory)) { + throw new BackupMigrateException('The directory %dir cannot be written to because of the operating system file permissions.', + ['%dir' => $directory]); + } + + if (!$reader = $this->getArchiveReader()) { + throw new BackupMigrateException('A file directory source requires an archive reader object.'); + } + // Check that the file endings match. + if ($reader->getFileExt() !== $file->getExtLast()) { + throw new BackupMigrateException('This source expects a .%ext file.', ['%ext' => $reader->getFileExt()]); + } + + $reader->setArchive($file); + $reader->extractTo($directory); + $reader->closeArchive(); + + return TRUE; + } + return FALSE; + } + + /** + * Get a list if files to be backed up from the given directory. + * + * @param string $dir The name of the directory to list. + * + * @return array + * + * @throws \BackupMigrate\Core\Exception\BackupMigrateException + * @throws \BackupMigrate\Core\Exception\IgnorableException + * + * @internal param $directory + */ + protected function getFilesToBackup($dir) { + // Add a trailing slash if there is none. + if (substr($dir, -1) !== '/') { + $dir .= '/'; + } + + if (!file_exists($dir)) { + throw new BackupMigrateException('Directory %dir does not exist.', + ['%dir' => $dir]); + } + if (!is_dir($dir)) { + throw new BackupMigrateException('The file %dir is not a directory.', + ['%dir' => $dir]); + } + if (!is_readable($dir)) { + throw new BackupMigrateException('Directory %dir could not be read from.', + ['%dir' => $dir]); + } + + // Get a filtered list if files from the directory. + list($out, $errors) = $this->_getFilesFromDirectory($dir); + + // Alert the user to any errors there might have been. + if ($errors) { + $count = count($errors); + $file_list = implode(', ', array_slice($errors, 0, 5)); + if ($count > 5) { + $file_list .= ', ...'; + } + + if (!$this->confGet('ignore_errors')) { + throw new IgnorableException('The backup could not be completed because !count files could not be read: (!files).', + ['!count' => $count, '!files' => $file_list]); + } + else { + // throw new IgnorableException('!count files could not be read: (!files).', ['!files' => $filesmsg]); + // @TODO: Log the ignored files. + } + } + + return $out; + } + + /** + * @param $base_path + * The name of the directory to list. This must always end in '/'. + * @param string $subdir + * @return array + * @internal param string $dir + */ + protected function _getFilesFromDirectory($base_path, $subdir = '') { + $out = $errors = []; + + // Open the directory. + if (!$handle = opendir($base_path . $subdir)) { + $errors[] = $base_path . $subdir; + } + else { + while (($file = readdir($handle)) !== FALSE) { + // If not a dot file and the file name isn't excluded. + if ($file != '.' && $file != '..') { + + // Get the full path of the file. + $path = $base_path . $subdir . $file; + + // Allow filters to modify or exclude this path. + $path = $this->plugins()->call('beforeFileBackup', $path, ['source' => $this, 'base_path' => $base_path]); + if ($path) { + if (is_dir($path)) { + list($sub_files, $sub_errors) = + $this->_getFilesFromDirectory($base_path, $subdir . $file . '/'); + + // Add the directory if it is empty. + if (empty($sub_files)) { + $out[$subdir . $file] = $path; + } + + // Add the sub-files to the output. + $out = array_merge($out, $sub_files); + $errors = array_merge($errors, $sub_errors); + } + else { + if (is_readable($path)) { + $out[$subdir . $file] = $path; + } + else { + $errors[] = $path; + } + } + } + } + } + closedir($handle); + } + + return [$out, $errors]; + } + + /** + * @param \BackupMigrate\Core\Service\ArchiveWriterInterface $writer + */ + public function setArchiveWriter(ArchiveWriterInterface $writer) { + $this->archive_writer = $writer; + } + + /** + * @return \BackupMigrate\Core\Service\ArchiveWriterInterface + */ + public function getArchiveWriter() { + return $this->archive_writer; + } + + /** + * @return \BackupMigrate\Core\Service\ArchiveReaderInterface + */ + public function getArchiveReader() { + return $this->archive_reader; + } + + /** + * @param \BackupMigrate\Core\Service\ArchiveReaderInterface $archive_reader + */ + public function setArchiveReader($archive_reader) { + $this->archive_reader = $archive_reader; + } + + /** + * Get a definition for user-configurable settings. + * + * @param array $params + * + * @return array + */ + public function configSchema($params = []) { + $schema = []; + + // Init settings. + if ($params['operation'] == 'initialize') { + $schema['fields']['directory'] = [ + 'type' => 'text', + 'title' => $this->t('Directory Path'), + ]; + } + + return $schema; + } + + /** + * Get the default values for the plugin. + * + * @return \BackupMigrate\Core\Config\Config + */ + public function configDefaults() { + return new Config([ + 'directory' => '', + ]); + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Source/MySQLiSource.php b/modules/backup_migrate/lib/backup_migrate_core/src/Source/MySQLiSource.php new file mode 100644 index 0000000..d51bc03 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Source/MySQLiSource.php @@ -0,0 +1,455 @@ + [], + 'importFromFile' => [] + ]; + } + + /** + * Export this source to the given temp file. This should be the main + * back up function for this source. + * + * @return \BackupMigrate\Core\File\BackupFileReadableInterface $file + * A backup file with the contents of the source dumped to it.. + */ + public function exportToFile() { + if ($connection = $this->_getConnection()) { + $file = $this->getTempFileManager()->create('mysql'); + + $exclude = (array) $this->confGet('exclude_tables'); + $nodata = (array) $this->confGet('nodata_tables'); + + $file->write($this->_getSQLHeader()); + $tables = $this->_getTables(); + + $lines = 0; + foreach ($tables as $table) { + // @TODO reenable this. + // if (_backup_migrate_check_timeout()) { + // return FALSE; + // } + $table = $this->plugins()->call('beforeDBTableBackup', $table, ['source' => $this]); + if ($table['name'] && !isset($exclude[$table['name']]) && empty($table['exclude'])) { + $file->write($this->_getTableCreateSQL($table)); + $lines++; + if (empty($table['nodata']) && !in_array($table['name'], $nodata)) { + $lines += $this->_dumpTableSQLToFile($file, $table); + } + } + } + + $file->write($this->_getSQLFooter()); + $file->close(); + return $file; + } + else { + // @TODO: Throw exception + return $this->getTempFileManager()->create('mysql'); + } + + } + + /** + * Import to this source from the given backup file. This is the main restore + * function for this source. + * + * @param \BackupMigrate\Core\File\BackupFileReadableInterface $file + * The file to read the backup from. It will not be opened for reading + * + * @return bool|int + */ + public function importFromFile(BackupFileReadableInterface $file) { + $num = 0; + + if ($conn = $this->_getConnection()) { + // Open (or rewind) the file. + $file->openForRead(); + + // Read one line at a time and run the query. + while ($line = $this->_readSQLCommand($file)) { + // if (_backup_migrate_check_timeout()) { + // return FALSE; + // } + if ($line) { + // Execute the sql query from the file. + $conn->query($line); + $num++; + } + } + // Close the file, we're done reading it. + $file->close(); + } + return $num; + } + + + /** + * Get the db connection for the specified db. + * + * @return \mysqli Connection object. + * + * @throws \Exception + */ + protected function _getConnection() { + if (!$this->connection) { + if (!function_exists('mysqli_init') && !extension_loaded('mysqli')) { + throw new BackupMigrateException('Cannot connect to the database becuase the MySQLi extension is missing.'); + } + $this->connection = new \mysqli( + $this->confGet('host'), + $this->confGet('username'), + $this->confGet('password'), + $this->confGet('database'), + $this->confGet('port'), + $this->confGet('socket') + ); + // Throw an error on fail. + if ($this->connection->connect_errno || !$this->connection->ping()) { + throw new BackupMigrateException("Failed to connect to MySQL server."); + } + // Ensure, that the character set is UTF8. + if (!$this->connection->set_charset('utf8mb4')) { + if (!$this->connection->set_charset('utf8')) { + throw new BackupMigrateException('UTF8 is not supported by the MySQL server.'); + } + } + } + return $this->connection; + } + + + /** + * Get the header for the top of the SQL file. + * + * @return string + */ + protected function _getSQLHeader() { + $info = $this->_dbInfo(); + $version = $info['version']; + $host = $this->confGet('host'); + $db = $this->confGet('database'); + $timestamp = gmdate('r'); + $generator = $this->confGet('generator'); + + return <<
readLine()) { + $first2 = substr($line, 0, 2); + $first3 = substr($line, 0, 2); + + // Ignore single line comments. This function doesn't support multiline comments or inline comments. + if ($first2 != '--' && ($first2 != '/*' || $first3 == '/*!')) { + $out .= ' ' . trim($line); + // If a line ends in ; or */ it is a sql command. + if (substr($out, strlen($out) - 1, 1) == ';') { + return trim($out); + } + } + } + return trim($out); + } + + /** + * Lock the list of given tables in the database. + */ + protected function _lockTables($tables) { + if ($tables) { + $tables_escaped = []; + foreach ($tables as $table) { + $tables_escaped[] = '`' . $table . '` WRITE'; + } + $this->query('LOCK TABLES ' . implode(', ', $tables_escaped)); + } + } + + /** + * Unlock all tables in the database. + */ + protected function _unlockTables($settings) { + $this->query('UNLOCK TABLES'); + } + + /** + * Get a list of tables in the db. + */ + protected function _getTables() { + $out = []; + // get auto_increment values and names of all tables. + $tables = $this->query("SHOW TABLE STATUS"); + while ($tables && $table = $tables->fetch_assoc()) { + // Lowercase the keys for consistency. + $table = array_change_key_case($table); + $out[$table['name']] = $table; + } + return $out; + } + + /** + * Get the sql for the structure of the given table. + * + * @param array $table + * + * @return string + */ + protected function _getTableCreateSQL($table) { + $out = ""; + + // If this is a view. + if (empty($table['engine'])) { + // Switch SQL mode to for a simpler version of the create view syntax. + $sql_mode = $this->_fetchValue("SELECT @@SESSION.sql_mode"); + // @TODO: Setting the sql_mode does not seem to work. + $this->query("SET sql_mode = 'ANSI'"); + $create = $this->_fetchAssoc("SHOW CREATE VIEW `" . $table['name'] . "`"); + if ($create) { + // Lowercase the keys for consistency. + $create = array_change_key_case($create); + $out .= "DROP VIEW IF EXISTS `" . $table['name'] . "`;\n"; + $out .= "SET sql_mode = 'ANSI';\n"; + $out .= strtr($create['create view'], "\n", " ") . ";\n"; + $out .= "SET sql_mode = '$sql_mode';\n"; + } + + // Set the SQL_mode back to the original value. + $this->query("SET SQL_mode = '$sql_mode'"); + } + + // This is a regular table. + else { + $create = $this->_fetchAssoc("SHOW CREATE TABLE `" . $table['name'] . "`"); + if ($create) { + // Lowercase the keys for consistency. + $create = array_change_key_case($create); + $out .= "DROP TABLE IF EXISTS `" . $table['name'] . "`;\n"; + // Remove newlines. + $out .= strtr($create['create table'], ["\n" => ' ']); + if ($table['auto_increment']) { + $out .= " AUTO_INCREMENT=" . $table['auto_increment']; + } + $out .= ";\n"; + } + } + + return $out; + } + + /** + * Get the sql to insert the data for a given table. + */ + protected function _dumpTableSQLToFile(BackupFileWritableInterface $file, $table) { + + // If this is a view, do not export any data. + if (empty($table['engine'])) { + return 0; + } + + // Otherwise export the table data. + $rows_per_line = 30; + // $this->confGet('rows_per_line');//variable_get('backup_migrate_data_rows_per_line', 30); + $bytes_per_line = 2000; + // $this->confGet('bytes_per_line'); variable_get('backup_migrate_data_bytes_per_line', 2000); + $lines = 0; + $result = $this->query("SELECT * FROM `" . $table['name'] . "`"); + $rows = $bytes = 0; + + // Escape backslashes, PHP code, special chars. + $search = ['\\', "'", "\x00", "\x0a", "\x0d", "\x1a"]; + $replace = ['\\\\', "''", '\0', '\n', '\r', '\Z']; + + while ($result && $row = $result->fetch_assoc()) { + // DB Escape the values. + $items = []; + foreach ($row as $key => $value) { + $items[] = is_null($value) ? "null" : "'" . str_replace($search, $replace, $value) . "'"; + // @TODO: escape binary data + } + + // If there is a row to be added. + if ($items) { + // Start a new line if we need to. + if ($rows == 0) { + $file->write("INSERT INTO `" . $table['name'] . "` VALUES "); + $bytes = $rows = 0; + } + // Otherwise add a comma to end the previous entry. + else { + $file->write(","); + } + + // Write the data itself. + $sql = implode(',', $items); + $file->write('(' . $sql . ')'); + $bytes += strlen($sql); + $rows++; + + // Finish the last line if we've added enough items. + if ($rows >= $rows_per_line || $bytes >= $bytes_per_line) { + $file->write(";\n"); + $lines++; + $bytes = $rows = 0; + } + } + } + // Finish any unfinished insert statements. + if ($rows > 0) { + $file->write(";\n"); + $lines++; + } + + return $lines; + } + + + /** + * Run a db query on this destination's db. + * + * @param $query + * + * @return bool|\mysqli_result + * + * @throws \Exception + */ + protected function query($query) { + if ($conn = $this->_getConnection()) { + return $conn->query($query); + } + else { + throw new \Exception('Could not run any queries on the database as a connection could not be established'); + } + } + + /** + * Return the first result of the query as an associated array. + * + * @param string $query A SQL query. + * + * @return array + * + * @throws \Exception + */ + protected function _fetchAssoc($query) { + $result = $this->query($query); + if ($result) { + return $result->fetch_assoc(); + } + return []; + } + + + /** + * Return the first field of the first result of a query. + * + * @param string $query A SQL query. + * + * @return null|object + * + * @throws \Exception + */ + protected function _fetchValue($query) { + $result = $this->_fetchAssoc($query); + return reset($result); + } + + + /** + * Get the version info for the given DB. + */ + protected function _dbInfo() { + $conn = $this->_getConnection(); + return [ + 'type' => 'mysql', + 'version' => $conn->server_version, + ]; + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Source/README.md b/modules/backup_migrate/lib/backup_migrate_core/src/Source/README.md new file mode 100644 index 0000000..a70f558 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Source/README.md @@ -0,0 +1,11 @@ +# Sources + +A source in Backup and Migrate is a thing that can be backed up. This could be a database or a file directory. An object that implements the `\BackupMigrate\Core\Source\SourceInterface` is responsible for creating a single backup file that represents the specified source. It is also responsible for restoring the to that source from a backup file. + +Sources in Backup and Migrate are implemented as plugins and will have dependencies and configuration injected into them by the Plugin Manager. + +A single Backup and Migrate instance can have more than one source of a given type. Each source will have a unique key that will be used to pass the configuration to the source object and to specify the source when running a `backup()` or `restore()` operation. + +Like other plugins, sources are passed to the Backup and Migrate object by the consuming application by calling the `add()` method on the sources plugin manager. + + $backup_migrate->sources()->add('source1', new MySourcePlugin()); \ No newline at end of file diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Source/SourceBase.php b/modules/backup_migrate/lib/backup_migrate_core/src/Source/SourceBase.php new file mode 100644 index 0000000..54f6a12 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Source/SourceBase.php @@ -0,0 +1,32 @@ + [], + 'importFromFile' => [] + ]; + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Source/SourceInterface.php b/modules/backup_migrate/lib/backup_migrate_core/src/Source/SourceInterface.php new file mode 100644 index 0000000..b11c6c9 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Source/SourceInterface.php @@ -0,0 +1,35 @@ + $value) { + switch (substr($key, 0, 1)) { + case '@': + case '%': + $replacements[$key] = strip_tags($value); + break; + } + } + + return strtr($string, $replacements); + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Translation/TranslatableInteface.php b/modules/backup_migrate/lib/backup_migrate_core/src/Translation/TranslatableInteface.php new file mode 100644 index 0000000..2977694 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Translation/TranslatableInteface.php @@ -0,0 +1,30 @@ +t(...); + * to translate a string (if a translator is available). + * + * Class TranslatableTrait + * + * @package BackupMigrate\Core\Translation + */ +trait TranslatableTrait { + /** + * @var TranslatorInterface; + */ + protected $translator; + + /** + * @param TranslatorInterface $translator + */ + public function setTranslator($translator) { + $this->translator = $translator; + } + + /** + * Translate the given string if there is a translator service available. + * + * @param $string + * @param $replacements + * @param $context + * + * @return mixed + */ + public function t($string, $replacements = [], $context = []) { + // If there is no translation service available use a passthrough to send + // back the original (en-us) string. + if (empty($this->translator)) { + $this->translator = new PassthroughTranslator(); + } + return $this->translator->translate($string, $replacements, $context); + } + +} diff --git a/modules/backup_migrate/lib/backup_migrate_core/src/Translation/TranslatorInterface.php b/modules/backup_migrate/lib/backup_migrate_core/src/Translation/TranslatorInterface.php new file mode 100644 index 0000000..1057b75 --- /dev/null +++ b/modules/backup_migrate/lib/backup_migrate_core/src/Translation/TranslatorInterface.php @@ -0,0 +1,31 @@ +getAll() as $plugin_key => $plugin) { + $schema = $plugin->configSchema(['operation' => $operation]); + $config = $plugin->config(); + + DrupalConfigHelper::addFieldsFromSchema($form, $schema, $config, array_merge($parents, [$plugin_key])); + } + return $form; + + } + + /** + * Build the configuration form for a single plugin, source or destination. + * + * @param \BackupMigrate\Core\Config\ConfigurableInterface $plugin + * The plugin, source or destination to build the form for. + * @param string $operation + * 'backup', 'restore', or 'initialize' depending on the operation being configured for. + * @param array $parents + * + * @return array + */ + static public function buildPluginForm($plugin, $operation, $parents = ['config']) { + $schema = $plugin->configSchema(['operation' => $operation]); + $config = $plugin->config(); + + return DrupalConfigHelper::buildFormFromSchema($schema, $config, $parents); + } + + /** + * @param array $schema + * A configuration schema from one or more Backup and Migrate plugins. + * @param \BackupMigrate\Core\Config\ConfigInterface $config + * The configuration object containing the default values. + * @param array $parents + * The form parents array. + * @return array + * A drupal forms api array. + */ + static public function buildFormFromSchema($schema, ConfigInterface $config, $parents = [], $form = []) { + $form = []; + DrupalConfigHelper::addFieldsFromSchema($form, $schema, $config, $parents); + return $form; + } + + /** + * Add the schema fields to the given form array. + * + * @param array $schema + * A configuration schema from one or more Backup and Migrate plugins. + * @param \BackupMigrate\Core\Config\ConfigInterface $config + * The configuration object containing the default values. + * @param array $parents + * The form parents array. + */ + static public function addFieldsFromSchema(&$form, $schema, ConfigInterface $config, $parents = []) { + // Add the specified groups. + if (isset($schema['groups'])) { + foreach ($schema['groups'] as $group_key => $item) { + // If the group is just called 'default' then use the key from the plugin as the group key. + // @TODO: make this less ugly. + if ($group_key == 'default' && $parents) { + $group_key = end($parents); + } + if (!isset($form[$group_key])) { + $form[$group_key] = [ + '#type' => 'fieldset', + '#title' => $item['title'], + '#tree' => FALSE, + ]; + } + } + } + + // Add each of the fields. + if (isset($schema['fields'])) { + foreach ($schema['fields'] as $field_key => $item) { + $form_item = []; + $value = $config->get($field_key); + + switch ($item['type']) { + case 'text': + $form_item['#type'] = 'textfield'; + if (!empty($item['multiple'])) { + $form_item['#type'] = 'textarea'; + if (!isset($form_item['#description'])) { + $form_item['#description'] = ''; + } + $form_item['#description'] .= ' ' . t('Add one item per line.'); + $form_item['#element_validate'] = ['BackupMigrate\Drupal\Config\DrupalConfigHelper::validateMultiText']; + $value = implode("\n", $value); + } + if (!empty($item['multiline'])) { + $form_item['#type'] = 'textarea'; + } + break; + + case 'password': + $form_item['#type'] = 'password'; + $form_item['#value_callback'] = 'BackupMigrate\Drupal\Config\DrupalConfigHelper::valueCallbackSecret'; + break; + + case 'number': + $form_item['#type'] = 'textfield'; + $form_item['#size'] = 5; + if (!empty($item['max'])) { + $form_item['#size'] = strlen((string) $item['max']) + 3; + } + break; + + case 'boolean': + $form_item['#type'] = 'checkbox'; + break; + + case 'enum': + $form_item['#type'] = 'select'; + $form_item['#multiple'] = !empty($item['multiple']); + if (empty($item['#required']) && empty($item['multiple'])) { + $item['options'] = ['' => '--' . t('None') . '--'] + $item['options']; + } + $form_item['#options'] = $item['options']; + break; + } + + // If there is a form item add it to the form. + if ($form_item) { + // Add the common form elements. + $form_item['#title'] = $item['title']; + $form_item['#parents'] = array_merge($parents, [$field_key]); + $form_item['#required'] = !empty($item['required']); + $form_item['#default_value'] = $value; + + if (!empty($item['description'])) { + $form_item['#description'] = $item['description']; + } + + // Add the field to it's group or directly to the top level of the form. + if (!empty($item['group'])) { + $group_key = $item['group']; + if ($group_key == 'default' && $parents) { + $group_key = end($parents); + } + $form[$group_key][$field_key] = $form_item; + } + else { + $form[$field_key] = $form_item; + } + } + } + } + } + + /** + * Break a multi-line text value into an array. + * + * @param $element + * @param $form_state + */ + public static function validateMultiText(&$element, FormStateInterface &$form_state) { + $form_state->setValueForElement($element, array_map('trim', explode("\n", $element['#value']))); + } + + /** + * A value mapping callback that replaces missing secrets because the Form API + * does not preserve the default values of password inputs. + * + * @param $element + * @param $input + * @param \Drupal\Core\Form\FormStateInterface $form_state + */ + public static function valueCallbackSecret(&$element, $input, FormStateInterface $form_state) { + if (empty($input)) { + return $element['#default_value']; + } + return $input; + } + + /** + * Get a pulldown for the given list of plugins. + * + * @param \BackupMigrate\Core\Config\ConfigurableInterface[]|\BackupMigrate\Core\Plugin\PluginManagerInterface $plugins + * @param $title + * @param null $default_value + * + * @return array + */ + public static function getPluginSelector(PluginManagerInterface $plugins, $title, $default_value = NULL) { + $options = []; + foreach ($plugins->getAll() as $key => $plugin) { + $options[$key] = $plugin->confGet('name', $key); + } + return [ + '#type' => 'select', + '#title' => $title, + '#options' => $options, + '#default_value' => $default_value + ]; + } + + /** + * Get a select form item for the given list of sources. + * + * @param \BackupMigrate\Core\Main\BackupMigrateInterface $bam + * @param $title + * @param null $default_value + * + * @return array + */ + public static function getSourceSelector(BackupMigrateInterface $bam, $title, $default_value = NULL) { + return DrupalConfigHelper::getPluginSelector($bam->sources(), $title, $default_value); + } + + /** + * Get a select form item for the given list of sources. + * + * @param \BackupMigrate\Core\Main\BackupMigrateInterface $bam + * @param $title + * @param null $default_value + * + * @return array + */ + public static function getDestinationSelector(BackupMigrateInterface $bam, $title, $default_value = NULL) { + return DrupalConfigHelper::getPluginSelector($bam->destinations(), $title, $default_value); + } + + + /** + * GEt a pulldown for the list of all settings profiles. + * + * @param $title + * + * @return array + */ + public static function getSettingsProfileSelector($title, $default_value = NULL) { + $options = []; + foreach (SettingsProfile::loadMultiple() as $key => $profile) { + $options[$key] = $profile->get('label'); + } + if ($options) { + return [ + '#type' => 'select', + '#title' => $title, + '#options' => $options, + '#default_value' => $default_value + ]; + } + } + +} diff --git a/modules/backup_migrate/src/Controller/BackupController.php b/modules/backup_migrate/src/Controller/BackupController.php new file mode 100644 index 0000000..5d4de65 --- /dev/null +++ b/modules/backup_migrate/src/Controller/BackupController.php @@ -0,0 +1,202 @@ +getStorage('backup_migrate_destination'); + + $out = []; + foreach ($storage->getQuery()->execute() as $key) { + $entity = $storage->load($key); + $destination = $entity->getObject(); + $label = $destination->confGet('name'); + + $out[$key] = [ + 'title' => [ + '#markup' => '

' . $this->t('Most recent backups in %dest', ['%dest' => $label]) . '

' + ], + 'list' => $this::listDestinationBackups($destination, $key, 5), + ]; + // Add the more link. + if ($entity->access('backups') && $entity->hasLinkTemplate('backups')) { + $out[$key]['link'] = $entity->toLink( + $this->t('View all backups in %dest', ['%dest' => $label]), 'backups' + )->toRenderable(); + } + + } + return $out; + } + + /** + * Get the title for the listing page of a destination entity. + * + * @param \Drupal\backup_migrate\Entity\Destination $backup_migrate_destination + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup + */ + public function listDestinationEntityBackupsTitle(Destination $backup_migrate_destination) { + return $this->t('Backups in @destination_name', + ['@destination_name' => $backup_migrate_destination->label()]); + } + + /** + * List the backups in the given destination. + * + * @param \Drupal\backup_migrate\Entity\Destination $backup_migrate_destination + * + * @return mixed + */ + public function listDestinationEntityBackups(Destination $backup_migrate_destination) { + $destination = $backup_migrate_destination->getObject(); + return $this->listDestinationBackups($destination, + $backup_migrate_destination->id()); + } + + /** + * List the backups in the given destination. + * + * @param \BackupMigrate\Core\Destination\ListableDestinationInterface $destination + * + * @return mixed + */ + public function listDestinationBackups( + ListableDestinationInterface $destination, + $backup_migrate_destination_id, + $count = NULL + ) { + + // Get a sorted list of files. + $rows = []; + $header = [ + [ + 'data' => $this->t('Name'), + 'class' => [RESPONSIVE_PRIORITY_MEDIUM], + 'field' => 'name' + ], + [ + 'data' => $this->t('Date'), + 'class' => [RESPONSIVE_PRIORITY_MEDIUM], + 'field' => 'datestamp', + 'sort' => 'desc' + ], + [ + 'data' => $this->t('Size'), + 'class' => [RESPONSIVE_PRIORITY_MEDIUM], + 'field' => 'filesize', + 'sort' => 'desc' + ], + [ + 'data' => $this->t('Operations'), + 'class' => [RESPONSIVE_PRIORITY_LOW] + ], + ]; + + $order = tablesort_get_order($header); + $sort = tablesort_get_sort($header); + $php_sort = $sort == 'desc' ? SORT_DESC : SORT_ASC; + + $backups = $destination->queryFiles([], $order['sql'], $php_sort, $count); + + foreach ($backups as $backup_id => $backup) { + $rows[] = [ + 'data' => [ + // Cells. + $backup->getFullName(), + \Drupal::service('date.formatter') + ->format($backup->getMeta('datestamp')), + format_size($backup->getMeta('filesize')), + [ + 'data' => [ + '#type' => 'operations', + '#links' => [ + 'restore' => [ + 'title' => $this->t('Restore'), + 'url' => Url::fromRoute( + 'entity.backup_migrate_destination.backup_restore', + [ + 'backup_migrate_destination' => $backup_migrate_destination_id, + 'backup_id' => $backup_id, + ] + ), + ], + 'download' => [ + 'title' => $this->t('Download'), + 'url' => Url::fromRoute( + 'entity.backup_migrate_destination.backup_download', + [ + 'backup_migrate_destination' => $backup_migrate_destination_id, + 'backup_id' => $backup_id, + ] + ), + ], + 'delete' => [ + 'title' => $this->t('Delete'), + 'url' => Url::fromRoute( + 'entity.backup_migrate_destination.backup_delete', + [ + 'backup_migrate_destination' => $backup_migrate_destination_id, + 'backup_id' => $backup_id, + ] + ), + ], + ], + ], + ], + ], + ]; + } + + $build['backups_table'] = [ + '#type' => 'table', + '#header' => $header, + '#rows' => $rows, + '#empty' => $this->t('There are no backups in this destination.'), + ]; + + return $build; + } + + /** + * Download a backup via the browser. + * + * @param \Drupal\backup_migrate\Entity\Destination $backup_migrate_destination + * + * @param $backup_id + */ + public function download( + Destination $backup_migrate_destination, + $backup_id + ) { + $destination = $backup_migrate_destination->getObject(); + $file = $destination->getFile($backup_id); + $file = $destination->loadFileForReading($file); + + $browser = new DrupalBrowserDownloadDestination(); + $browser->saveFile($file); + } + +} diff --git a/modules/backup_migrate/src/Controller/DestinationListBuilder.php b/modules/backup_migrate/src/Controller/DestinationListBuilder.php new file mode 100644 index 0000000..dc4f469 --- /dev/null +++ b/modules/backup_migrate/src/Controller/DestinationListBuilder.php @@ -0,0 +1,61 @@ +t('Backup Destination'); + $header['id'] = $this->t('Machine name'); + $header['type'] = $this->t('Type'); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + $row['label'] = $entity->label(); + $row['id'] = $entity->id(); + $row['type'] = $entity->get('type'); + if ($plugin = $entity->getPlugin()) { + $info = $plugin->getPluginDefinition(); + $row['type'] = $info['title']; + } + + return $row + parent::buildRow($entity); + } + + /** + * Gets this list's default operations. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity the operations are for. + * + * @return array + * The array structure is identical to the return value of + * self::getOperations(). + */ + public function getDefaultOperations(EntityInterface $entity) { + $operations = parent::getDefaultOperations($entity); + if ($entity->access('backups') && $entity->hasLinkTemplate('backups')) { + $operations['backups'] = [ + 'title' => $this->t('List Backups'), + 'weight' => 100, + 'url' => $entity->toUrl('backups'), + ]; + } + + return $operations; + } + +} diff --git a/modules/backup_migrate/src/Controller/ScheduleListBuilder.php b/modules/backup_migrate/src/Controller/ScheduleListBuilder.php new file mode 100644 index 0000000..64e8192 --- /dev/null +++ b/modules/backup_migrate/src/Controller/ScheduleListBuilder.php @@ -0,0 +1,82 @@ +t('Schedule Name'); + $header['enabled'] = $this->t('Enabled'); + $header['period'] = $this->t('Frequency'); + $header['last_run'] = $this->t('Last Run'); + $header['next_run'] = $this->t('Next Run'); + $header['keep'] = $this->t('Keep'); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + * + * ScheduleListBuilder save implementation requires instance of Schedule. + * Signature enforced by EntityListBuilder. + * + * @throw InvalidArgumentException + */ + public function buildRow(EntityInterface $entity) { + if (!$entity instanceof Schedule) { + throw new Exception(); + } + $row['label'] = $entity->label(); + $row['enabled'] = $entity->get('enabled') ? $this->t('Yes') : $this->t('No'); + $row['period'] = $entity->getPeriodFormatted(); + + $row['last_run'] = $this->t('Never'); + if ($last_run = $entity->getLastRun()) { + $row['last_run'] = \Drupal::service('date.formatter')->format($last_run, 'small'); + $row['last_run'] .= ' (' . $this->t('@time ago', ['@time' => \Drupal::service('date.formatter')->formatInterval(REQUEST_TIME - $last_run)]) . ')'; + } + + $row['next_run'] = $this->t('Not Scheduled'); + if (!$entity->get('enabled')) { + $row['next_run'] = $this->t('Disabled'); + } + elseif ($next_run = $entity->getNextRun()) { + $interval = \Drupal::service('date.formatter')->formatInterval(abs($next_run - REQUEST_TIME)); + if ($next_run > REQUEST_TIME) { + $row['next_run'] = \Drupal::service('date.formatter')->format($next_run, 'small'); + $row['next_run'] .= ' (' . $this->t('in @time', ['@time' => $interval]) . ')'; + } + else { + $row['next_run'] = $this->t('Next cron run'); + if ($last_run) { + $row['next_run'] .= ' (' . $this->t('was due @time ago', ['@time' => $interval]) . ')'; + } + } + } + + $row['keep'] = \Drupal::translation()->formatPlural($entity->get('keep'), 'Last 1 backup', 'Last @count backups'); + + return $row + parent::buildRow($entity); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + parent::submitForm($form, $form_state); + + drupal_set_message(t('The schedule settings have been updated.')); + } + +} diff --git a/modules/backup_migrate/src/Controller/SettingsProfileListBuilder.php b/modules/backup_migrate/src/Controller/SettingsProfileListBuilder.php new file mode 100644 index 0000000..9a0fb6c --- /dev/null +++ b/modules/backup_migrate/src/Controller/SettingsProfileListBuilder.php @@ -0,0 +1,30 @@ +t('Profile Name'); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + $row['label'] = $entity->label(); + // You probably want a few more properties here... + return $row + parent::buildRow($entity); + } + +} diff --git a/modules/backup_migrate/src/Controller/SourceListBuilder.php b/modules/backup_migrate/src/Controller/SourceListBuilder.php new file mode 100644 index 0000000..d7b6d68 --- /dev/null +++ b/modules/backup_migrate/src/Controller/SourceListBuilder.php @@ -0,0 +1,37 @@ +t('Backup Source'); + $header['id'] = $this->t('Machine name'); + $header['type'] = $this->t('Type'); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + $row['label'] = $entity->label(); + $row['id'] = $entity->id(); + $row['type'] = $entity->get('type'); + if ($info = $entity->getPluginDefinition()) { + $row['type'] = $info['title']; + } + + return $row + parent::buildRow($entity); + } + +} diff --git a/modules/backup_migrate/src/Destination/DrupalBrowserDownloadDestination.php b/modules/backup_migrate/src/Destination/DrupalBrowserDownloadDestination.php new file mode 100644 index 0000000..87b45c6 --- /dev/null +++ b/modules/backup_migrate/src/Destination/DrupalBrowserDownloadDestination.php @@ -0,0 +1,30 @@ +files->get("files", NULL, TRUE)[$id]; + // Make sure there's an upload to process. + if (!empty($file_upload)) { + $out = new ReadableStreamBackupFile($file_upload->getRealPath()); + $out->setFullName($file_upload->getClientOriginalName()); + return $out; + } + } + + /** + * Load the metadata for the given file however it may be stored. + * + * @param \BackupMigrate\Core\File\BackupFileInterface $file + * + * @return \BackupMigrate\Core\File\BackupFileInterface + */ + public function loadFileMetadata(BackupFileInterface $file) { + return $file; + } + + /** + * Load the file with the given ID from the destination. + * + * @param \BackupMigrate\Core\File\BackupFileInterface $file + * + * @return \BackupMigrate\Core\File\BackupFileReadableInterface The file if it exists or NULL if it doesn't + */ + public function loadFileForReading(BackupFileInterface $file) { + return $file; + } + + + /** + * Does the file with the given id (filename) exist in this destination. + * + * @param string $id The id (usually the filename) of the file. + * + * @return bool True if the file exists, false if it does not. + */ + public function fileExists($id) { + return (boolean) \Drupal::request()->files->has("files[$id]"); + } + +} diff --git a/modules/backup_migrate/src/Destination/DrupalDirectoryDestination.php b/modules/backup_migrate/src/Destination/DrupalDirectoryDestination.php new file mode 100644 index 0000000..1487636 --- /dev/null +++ b/modules/backup_migrate/src/Destination/DrupalDirectoryDestination.php @@ -0,0 +1,145 @@ +checkDirectory(); + + // @TODO Decide what the appropriate file_exists strategy should be. + file_unmanaged_move($file->realpath(), $this->_idToPath($file->getFullName()), FILE_EXISTS_REPLACE); + } + + + /** + * Check that the directory can be used for backup. + * + * @throws \BackupMigrate\Core\Exception\BackupMigrateException + */ + protected function checkDirectory() { + + // @TODO: Figure out if the file is or might be accessible via the web. + $dir = $this->confGet('directory'); + + $is_private = strpos($dir, 'private://') === 0; + + // Attempt to create/prepare the directory if it is in the private directory. + if ($is_private) { + if (!PrivateStream::basePath()) { + throw new BackupMigrateException( + "The backup file could not be saved to '%dir' because your private files system path has not been set.", + ['%dir' => $dir] + ); + } + if (!file_prepare_directory($dir, FILE_CREATE_DIRECTORY && FILE_MODIFY_PERMISSIONS)) { + throw new BackupMigrateException( + "The backup file could not be saved to '%dir' because the directory could not be created or cannot be written to. Please make sure your private files directory is writable by the web server.", + ['%dir' => $dir] + ); + } + } + // Not a private directory. Make sure it is outside the web root. + else { + // If the file is local to the server. + $real = \Drupal::service('file_system')->realpath($dir); + + if ($real) { + // If the file is within the docroot. + $in_root = strpos($real, DRUPAL_ROOT) === 0; + if ($in_root) { + throw new BackupMigrateException( + "The backup file could not be saved to '%dir' because that directory may be publicly accessible via the web. Please save your backups to the private file directory or a directory outside of the web root.", + ['%dir' => $dir] + ); + } + } + } + + // Do the regular exists/writable checks. + parent::checkDirectory(); + + // @TODO: Warn if the realpath cannot be resolved (because we cannot determine if the file is publicly accessible) + } + + + /** + * {@inheritdoc} + */ + public function queryFiles( + $filters = [], + $sort = 'datestamp', + $sort_direction = SORT_DESC, + $count = 100, + $start = 0 + ) { + + // Get the full list of files. + $out = $this->listFiles($count + $start); + foreach ($out as $key => $file) { + $out[$key] = $this->loadFileMetadata($file); + } + + // Filter the output. + if ($filters) { + $out = array_filter($out, function($file) use ($filters) { + foreach ($filters as $key => $value) { + if ($file->getMeta($key) !== $value) { + return FALSE; + } + } + return TRUE; + }); + } + + // Sort the files. + if ($sort && $sort_direction) { + uasort($out, function ($a, $b) use ($sort, $sort_direction) { + if ($sort_direction == SORT_DESC) { + if ($sort == 'name') { + return $a->getFullName() < $b->getFullName(); + } + // @TODO: fix this in core + return $a->getMeta($sort) < $b->getMeta($sort); + } + else { + if ($sort == 'name') { + return $a->getFullName() > $b->getFullName(); + } + // @TODO: fix this in core + return $a->getMeta($sort) > $b->getMeta($sort); + } + }); + } + + // Slice the return array. + if ($count || $start) { + $out = array_slice($out, $start, $count); + } + + return $out; + } + +} diff --git a/modules/backup_migrate/src/Entity/Destination.php b/modules/backup_migrate/src/Entity/Destination.php new file mode 100644 index 0000000..7e8f8e3 --- /dev/null +++ b/modules/backup_migrate/src/Entity/Destination.php @@ -0,0 +1,49 @@ +getNextRun(); + $should_run_now = (REQUEST_TIME >= $next_run_at); + $enabled = $this->get('enabled'); + if ($force || ($should_run_now && $enabled)) { + // Set the last run time before attempting backup. + // This will prevent a failing schedule from retrying on every cron run. + $this->setLastRun(REQUEST_TIME); + + try { + $config = []; + if ($settings_profile_id = $this->get('settings_profile_id')) { + // Load the settings profile if one is selected. + $profile = SettingsProfile::load($settings_profile_id); + if (!$profile) { + throw new BackupMigrateException( + "The settings profile '%profile' does not exist", + ['%profile' => $settings_profile_id]); + } + $config = $profile->get('config'); + } + + \Drupal::logger('backup_migrate')->info( + "Running schedule %name", ['%name' => $this->get('label')]); + // TODO: Set the config (don't just use the defaults). + // Run the backup. + // Set the schedule id in file metadata so that we can delete our own backups later. + // This requires the metadata writer to have knowledge of 'bam_scheduleid' which is + // a somewhat tight coupling that I'd like to unwind. + $config['metadata']['bam_scheduleid'] = $this->id; + $bam->setConfig(new Config($config)); + + $bam->backup($this->get('source_id'), $this->get('destination_id')); + + // Delete old backups. + if ($keep = $this->get('keep')) { + $destination = $bam->destinations()->get($this->get('destination_id')); + + // If the destination can be listed then get the list of files. + if ($destination instanceof ListableDestinationInterface) { + // Get a list of files to delete. Don't attempt to delete more + // than 10 files in one go. + $delete = $destination->queryFiles( + ['bam_scheduleid' => $this->id], + 'datestamp', + SORT_DESC, + 10, + $keep + ); + + foreach ($delete as $file) { + $destination->deleteFile($file->getFullName()); + } + } + } + } + catch (BackupMigrateException $e) { + \Drupal::logger('backup_migrate')->error( + "Scheduled backup '%name' failed: @err", + ['%name' => $this->get('label'), '@err' => $e->getMessage()] + ); + } + } + } + + /** + * @param $timestamp + * The unix time this schedule was last run. + */ + public function setLastRun($timestamp) { + \Drupal::keyValue('backup_migrate_schedule:last_run')->set($this->id(), $timestamp); + } + + /** + * @return int $timestamp + * The unix time this schedule was last run. + */ + public function getLastRun() { + return \Drupal::keyValue('backup_migrate_schedule:last_run')->get($this->id()); + } + + /** + * Get the next time this schedule should run. + * + * @return int + */ + public function getNextRun() { + $last_run_at = $this->getLastRun(); + if ($last_run_at) { + return $last_run_at + $this->get('period'); + } + return REQUEST_TIME - 1; + } + + /** + * Return the schedule frequency formatted for display in human language. + * + * @return \Drupal\Core\StringTranslation\PluralTranslatableMarkup + * + * @throws \BackupMigrate\Core\Exception\BackupMigrateException + */ + public function getPeriodFormatted() { + return Schedule::formatPeriod(Schedule::secondsToPeriod($this->get('period'))); + } + + /** + * Convert a number of of seconds into a period array. + * + * @param int $seconds + * + * @return array An array containing the period definition and the number of them. + * ['number' => 123, 'type' => [...]] + * + * @throws \BackupMigrate\Core\Exception\BackupMigrateException + */ + public static function secondsToPeriod($seconds) { + foreach (array_reverse(Schedule::getPeriodTypes()) as $type) { + if (($seconds % $type['seconds']) === 0) { + return ['number' => $seconds / $type['seconds'], 'type' => $type]; + } + } + + throw new BackupMigrateException('Invalid period.'); + } + + /** + * Convert a period array into seconds. + * + * @param array $period A period array + * + * @return mixed + * + * @throws \BackupMigrate\Core\Exception\BackupMigrateException + */ + public static function periodToSeconds($period) { + return $period['number'] * $period['type']['seconds']; + } + + /** + * Convert a period array into seconds. + * + * @param $period + * + * @return \Drupal\Core\StringTranslation\PluralTranslatableMarkup + */ + public static function formatPeriod($period) { + return \Drupal::translation()->formatPlural($period['number'], $period['type']['singular'], $period['type']['plural']); + } + + /** + * Get a list of available backup periods. Only returns time periods which have a + * (reasonably) consistent number of seconds (ie: no months). + * + * @return array + */ + public static function getPeriodTypes() { + return [ + 'seconds' => ['type' => 'seconds', 'seconds' => 1, 'title' => 'Seconds', 'singular' => 'Once a second', 'plural' => 'Every @count seconds'], + 'minutes' => ['type' => 'minutes', 'seconds' => 60, 'title' => 'Minutes', 'singular' => 'Once a minute', 'plural' => 'Every @count minutes'], + 'hours' => ['type' => 'hours', 'seconds' => 3600, 'title' => 'Hours', 'singular' => 'Hourly', 'plural' => 'Every @count hours'], + 'days' => ['type' => 'days', 'seconds' => 86400, 'title' => 'Days', 'singular' => 'Daily', 'plural' => 'Every @count days'], + 'weeks' => ['type' => 'weeks', 'seconds' => 604800, 'title' => 'Weeks', 'singular' => 'Weekly', 'plural' => 'Every @count weeks'], + ]; + } + + /** + * Get a backup period type given it's key. + * + * @param string $type + * + * @return array + */ + public static function getPeriodType($type) { + return Schedule::getPeriodTypes()[$type]; + } + +} diff --git a/modules/backup_migrate/src/Entity/SettingsProfile.php b/modules/backup_migrate/src/Entity/SettingsProfile.php new file mode 100644 index 0000000..9190d96 --- /dev/null +++ b/modules/backup_migrate/src/Entity/SettingsProfile.php @@ -0,0 +1,56 @@ +getPlugin()) { + return $plugin->getObject(); + } + } + + /** + * Get the type plugin for this source. + * + * @return mixed + * + * @throws \BackupMigrate\Core\Exception\BackupMigrateException + */ + public function getPlugin() { + if ($this->get('type')) { + return $this->getPluginCollection()->get($this->get('type')); + } + return NULL; + } + + /** + * Get the type plugin for this source. + * + * @return mixed + * + * @throws \BackupMigrate\Core\Exception\BackupMigrateException + */ + public function getPluginDefinition() { + if ($plugin = $this->getPlugin()) { + return $plugin->getPluginDefinition(); + } + return []; + } + + /** + * Gets the plugin collections used by this entity. + * + * @return \Drupal\Component\Plugin\LazyPluginCollection[] + * An array of plugin collections, keyed by the property name they use to + * store their configuration. + */ + public function getPluginCollections() { + if ($config = $this->getPluginCollection()) { + return ['config' => $config]; + } + return []; + } + + /** + * @return \Drupal\block\BlockPluginCollection + */ + public function getPluginCollection() { + if ($this->get('type')) { + if (!$this->pluginCollection) { + $config = ['name' => $this->get('label')] + (array) $this->get('config'); + $this->pluginCollection = new DefaultSingleLazyPluginCollection( + $this->getPluginManager(), $this->get('type'), $config); + } + return $this->pluginCollection; + } + return []; + } + + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + if ($operation == "update" || $operation == "delete") { + $info = $this->getPluginDefinition(); + if (!empty($info['locked'])) { + return FALSE; + } + } + + return parent::access($operation, $account, $return_as_object); + } + + /** + * Return the plugin manager. + * + * @return PluginManagerInterface + */ + abstract public function getPluginManager(); + +} diff --git a/modules/backup_migrate/src/EntityPlugins/Annotation/BackupMigrateDestinationPlugin.php b/modules/backup_migrate/src/EntityPlugins/Annotation/BackupMigrateDestinationPlugin.php new file mode 100644 index 0000000..acc7421 --- /dev/null +++ b/modules/backup_migrate/src/EntityPlugins/Annotation/BackupMigrateDestinationPlugin.php @@ -0,0 +1,55 @@ +destinations()->add($key, $this->getObject()); + } + +} diff --git a/modules/backup_migrate/src/EntityPlugins/DestinationPluginInterface.php b/modules/backup_migrate/src/EntityPlugins/DestinationPluginInterface.php new file mode 100644 index 0000000..68451a5 --- /dev/null +++ b/modules/backup_migrate/src/EntityPlugins/DestinationPluginInterface.php @@ -0,0 +1,10 @@ +alterInfo('backup_migrate_destination_info'); + $this->setCacheBackend($cache_backend, 'backup_migrate_destination_plugins'); + } + +} diff --git a/modules/backup_migrate/src/EntityPlugins/SourcePluginBase.php b/modules/backup_migrate/src/EntityPlugins/SourcePluginBase.php new file mode 100644 index 0000000..f0d7597 --- /dev/null +++ b/modules/backup_migrate/src/EntityPlugins/SourcePluginBase.php @@ -0,0 +1,21 @@ +sources()->add($key, $this->getObject()); + } + +} diff --git a/modules/backup_migrate/src/EntityPlugins/SourcePluginInterface.php b/modules/backup_migrate/src/EntityPlugins/SourcePluginInterface.php new file mode 100644 index 0000000..2c418e3 --- /dev/null +++ b/modules/backup_migrate/src/EntityPlugins/SourcePluginInterface.php @@ -0,0 +1,10 @@ +alterInfo('backup_migrate_source_info'); + $this->setCacheBackend($cache_backend, 'backup_migrate_source_plugins'); + } + +} diff --git a/modules/backup_migrate/src/EntityPlugins/WrapperPluginBase.php b/modules/backup_migrate/src/EntityPlugins/WrapperPluginBase.php new file mode 100644 index 0000000..e709d86 --- /dev/null +++ b/modules/backup_migrate/src/EntityPlugins/WrapperPluginBase.php @@ -0,0 +1,81 @@ +setConfiguration($configuration); + } + + /** + * Get the Backup and Migrate plugin object. + * + * @return BackupMigrate\Core\Plugin\PluginInterface; + */ + public function getObject() { + // If the class to wrap was specified in the annotation then add that class. + $info = $this->getPluginDefinition(); + if ($info['wrapped_class']) { + return new $info['wrapped_class']($this->getConfig()); + } + } + + /** + * {@inheritdoc} + */ + abstract public function alterBackupMigrate(BackupMigrateInterface $bam, $key, $options = []); + + /** + * {@inheritdoc} + */ + public function getConfiguration() { + return $this->configuration; + } + + /** + * {@inheritdoc} + */ + public function setConfiguration(array $configuration) { + $this->configuration = $configuration; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return []; + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + return []; + } + + /** + * Return a Backup and Migrate Config object with the plugin configuration. + * + * @return \BackupMigrate\Core\Config\Config + */ + public function getConfig() { + return new Config($this->getConfiguration()); + } + +} diff --git a/modules/backup_migrate/src/EntityPlugins/WrapperPluginInterface.php b/modules/backup_migrate/src/EntityPlugins/WrapperPluginInterface.php new file mode 100644 index 0000000..994f069 --- /dev/null +++ b/modules/backup_migrate/src/EntityPlugins/WrapperPluginInterface.php @@ -0,0 +1,33 @@ +filesystem = $filesystem; + } + + /** + * {@inheritdoc} + */ + public function createTempFile($ext = '') { + // Add a dot to the file extension. + $ext = $ext ? '.' . $ext : ''; + + $file = $this->filesystem->tempnam($this->dir, $this->prefix); + if (!$file) { + throw new \Exception('Could not create a temporary file to write to.'); + } + + $this->tempfiles[] = $file; + return $file; + } + + /** + * {@inheritdoc} + */ + public function deleteTempFile($filename) { + // Only delete files that were created by this manager. + if (in_array($filename, $this->tempfiles)) { + if (file_exists($filename)) { + if (!$this->filesystem->unlink($filename)) { + throw new \Exception('Could not delete a temporary file.'); + } + } + } + } + +} diff --git a/modules/backup_migrate/src/Filter/DrupalPublicFileExcludeFilter.php b/modules/backup_migrate/src/Filter/DrupalPublicFileExcludeFilter.php new file mode 100644 index 0000000..206d9f3 --- /dev/null +++ b/modules/backup_migrate/src/Filter/DrupalPublicFileExcludeFilter.php @@ -0,0 +1,37 @@ + [ + 'js', + 'css', + 'php', + 'styles', + 'config_*', + '.htaccess', + ], + ]; + + // @TODO: Allow modules to add their own excluded defaults. + return new Config($config); + } + +} diff --git a/modules/backup_migrate/src/Filter/DrupalUtils.php b/modules/backup_migrate/src/Filter/DrupalUtils.php new file mode 100644 index 0000000..345c06e --- /dev/null +++ b/modules/backup_migrate/src/Filter/DrupalUtils.php @@ -0,0 +1,108 @@ + 'Advanced Settings', + ]; + $schema['fields']['site_offline'] = [ + 'group' => 'advanced', + 'type' => 'boolean', + 'title' => $this->t('Take site offline'), + 'description' => $this->t('Take the site offline during backup and show a maintenance message. Site will be taken back online once the backup is complete.'), + ]; + } + return $schema; + } + + /** + * Get the default values for the plugin. + * + * @return \BackupMigrate\Core\Config\Config + */ + public function configDefaults() { + return new Config([ + 'disable_query_log' => TRUE, + 'site_offline' => FALSE, + ]); + } + + + /** + * Run before the backup/restore begins. + */ + public function setUp() { + $this->takeSiteOffline(); + } + + /** + * Run after the operation is complete. + */ + public function tearDown() { + $this->takeSiteOnline(); + } + + /** + * Take the site offline if we need to. + */ + protected function takeSiteOffline() { + // Take the site offline. + if ($this->confGet('site_offline') && !\Drupal::state()->get('system.maintenance_mode')) { + \Drupal::state()->set('system.maintenance_mode', TRUE); + $this->maintenance_mode = TRUE; + } + } + + /** + * Take the site online if it was taken offline for this operation. + */ + protected function takeSiteOnline() { + // Take the site online again. + if ($this->maintenance_mode) { + \Drupal::state()->set('system.maintenance_mode', FALSE); + } + } + + /** + * Ensure, that the restore file does not exceed the server's upload_limit. + * + * @param BackupFileReadableInterface $file + * + * @return BackupFileReadableInterface + */ + public function beforeRestore(BackupFileReadableInterface $file) { + if ($file->getMeta('filesize') > file_upload_max_size()) { + throw new BackupMigrateException('The input file exceeds the servers upload_max_filesize or post_max_size limit.', ['!id' => $file->getMeta('id')]); + } + + return $file; + } + +} diff --git a/modules/backup_migrate/src/Form/BackupDeleteForm.php b/modules/backup_migrate/src/Form/BackupDeleteForm.php new file mode 100644 index 0000000..808ec31 --- /dev/null +++ b/modules/backup_migrate/src/Form/BackupDeleteForm.php @@ -0,0 +1,93 @@ +t('Are you sure you want to delete this backup?'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->t('This will permanently remove %backup_id from %destination_name.', + [ + '%backup_id' => $this->backup_id, + '%destination_name' => $this->destination->label() + ] + ); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * Returns the route to go to if the user cancels the action. + * + * @return \Drupal\Core\Url + * A URL object. + */ + public function getCancelUrl() { + return $this->destination->toUrl('backups'); + } + + /** + * Returns a unique string identifying the form. + * + * @return string + * The unique string identifying the form. + */ + public function getFormId() { + return 'backup_migrate_backup_delete_confirm'; + } + + public function buildForm(array $form, FormStateInterface $form_state, $backup_migrate_destination = NULL, $backup_id = NULL) { + $this->destination = $backup_migrate_destination; + $this->backup_id = $backup_id; + + return parent::buildForm($form, $form_state); + } + + /** + * Form submission handler. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $destination = $this->destination->getObject(); + $destination->deleteFile($this->backup_id); + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/modules/backup_migrate/src/Form/BackupMigrateAdvancedBackupForm.php b/modules/backup_migrate/src/Form/BackupMigrateAdvancedBackupForm.php new file mode 100644 index 0000000..550fd8f --- /dev/null +++ b/modules/backup_migrate/src/Form/BackupMigrateAdvancedBackupForm.php @@ -0,0 +1,107 @@ + 'fieldset', + "#title" => $this->t("Source"), + "#collapsible" => TRUE, + "#collapsed" => FALSE, + "#tree" => FALSE, + ]; + $form['source']['source_id'] = DrupalConfigHelper::getSourceSelector($bam, t('Backup Source')); + $form['source']['source_id']['#default_value'] = \Drupal::config('backup_migrate.settings')->get('backup_migrate_source_id'); + + $form += DrupalConfigHelper::buildAllPluginsForm($bam->plugins(), 'backup'); + if (\Drupal::moduleHandler()->moduleExists('token')) { + $filename_token = [ + '#theme' => 'token_tree_link', + '#token_types' => ['site'], + '#dialog' => TRUE, + '#click_insert' => TRUE, + '#show_restricted' => TRUE, + '#group' => 'file', + ]; + } + else { + $filename_token = [ + '#type' => 'markup', + '#markup' => 'In order to use tokens for File Name, please install & enable Token module.

' + ]; + } + array_splice($form['file'], 4, 0, ['filename_token' => $filename_token]); + + $form['destination'] = [ + '#type' => 'fieldset', + "#title" => $this->t("Destination"), + "#collapsible" => TRUE, + "#collapsed" => FALSE, + "#tree" => FALSE, + ]; + + $form['destination']['destination_id'] = DrupalConfigHelper::getDestinationSelector($bam, t('Backup Destination')); + $form['destination']['destination_id']['#default_value'] = \Drupal::config('backup_migrate.settings')->get('backup_migrate_destination_id'); + + $form['quickbackup']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Backup now'), + '#weight' => 1, + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + + $bam = backup_migrate_get_service_object($form_state->getValues()); + + // Let the plugins validate their own config data. + if ($plugin_errors = $bam->plugins()->map('configErrors', ['operation' => 'backup'])) { + foreach ($plugin_errors as $plugin_key => $errors) { + foreach ($errors as $error) { + $form_state->setErrorByName($plugin_key . '][' . $error->getFieldKey(), $this->t($error->getMessage(), $error->getReplacement())); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $config = $form_state->getValues(); + backup_migrate_perform_backup($config['source_id'], $config['destination_id'], $config); + } + +} diff --git a/modules/backup_migrate/src/Form/BackupMigrateQuickBackupForm.php b/modules/backup_migrate/src/Form/BackupMigrateQuickBackupForm.php new file mode 100644 index 0000000..eaa1860 --- /dev/null +++ b/modules/backup_migrate/src/Form/BackupMigrateQuickBackupForm.php @@ -0,0 +1,82 @@ + 'fieldset', + "#title" => $this->t("Quick Backup"), + "#collapsible" => FALSE, + "#collapsed" => FALSE, + "#tree" => FALSE, + ]; + + $form['quickbackup']['source_id'] = DrupalConfigHelper::getSourceSelector($bam, t('Backup Source')); + $form['quickbackup']['destination_id'] = DrupalConfigHelper::getDestinationSelector($bam, t('Backup Destination')); + $form['quickbackup']['settings_profile_id'] = DrupalConfigHelper::getSettingsProfileSelector(t('Settings Profile')); + unset($form['quickbackup']['destination_id']['#options']['upload']); + // Create the service + // $bam = backup_migrate_get_service_object(); + // $bam->setConfig($config); + // $bam->plugins()->get('namer')->confGet('filename'); + // $form['quickbackup']['source_id'] = _backup_migrate_get_source_pulldown(\Drupal::config('backup_migrate.settings')->get('backup_migrate_source_id')); + // $form['quickbackup']['destination'] = _backup_migrate_get_destination_pulldown('manual backup', \Drupal::config('backup_migrate.settings')->get('backup_migrate_destination_id'), \Drupal::config('backup_migrate.settings')->get('backup_migrate_copy_destination_id')); + $form['quickbackup']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Backup now'), + '#weight' => 1, + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $values = $form_state->getValues(); + $config = []; + + // Load the settings profile if one is selected. + if (!empty($values['settings_profile_id'])) { + $config = SettingsProfile::load($values['settings_profile_id'])->get('config'); + } + + backup_migrate_perform_backup($values['source_id'], $values['destination_id'], $config); + } + +} diff --git a/modules/backup_migrate/src/Form/BackupMigrateRestoreForm.php b/modules/backup_migrate/src/Form/BackupMigrateRestoreForm.php new file mode 100644 index 0000000..10cf25b --- /dev/null +++ b/modules/backup_migrate/src/Form/BackupMigrateRestoreForm.php @@ -0,0 +1,68 @@ + $this->t('Upload a Backup File'), + '#type' => 'file', + '#description' => $this->t("Upload a backup file created by Backup + and Migrate. For other database or file backups please use another + tool for import. Max file size: %size", + ["%size" => format_size(file_upload_max_size())] + ), + ]; + + $form['source_id'] = DrupalConfigHelper::getPluginSelector( + $bam->sources(), $this->t('Restore To')); + + $form += DrupalConfigHelper::buildAllPluginsForm($bam->plugins(), 'restore'); + + $form['quickbackup']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Restore now'), + '#weight' => 1, + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $config = $form_state->getValues(); + backup_migrate_perform_restore($config['source_id'], 'upload', 'backup_migrate_restore_upload', $config); + } + +} diff --git a/modules/backup_migrate/src/Form/BackupRestoreForm.php b/modules/backup_migrate/src/Form/BackupRestoreForm.php new file mode 100644 index 0000000..4e4a461 --- /dev/null +++ b/modules/backup_migrate/src/Form/BackupRestoreForm.php @@ -0,0 +1,97 @@ +t('Are you sure you want to restore this backup?'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Restore'); + } + + /** + * Returns the route to go to if the user cancels the action. + * + * @return \Drupal\Core\Url + * A URL object. + */ + public function getCancelUrl() { + return $this->destination->toUrl('backups'); + } + + /** + * Returns a unique string identifying the form. + * + * @return string + * The unique string identifying the form. + */ + public function getFormId() { + return 'backup_migrate_backup_restore_confirm'; + } + + /** + * @param array $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + * @param null $backup_migrate_destination + * @param null $backup_id + * @return array + */ + public function buildForm(array $form, FormStateInterface $form_state, $backup_migrate_destination = NULL, $backup_id = NULL) { + $this->destination = $backup_migrate_destination; + $this->backup_id = $backup_id; + + $bam = backup_migrate_get_service_object(); + $form['source_id'] = DrupalConfigHelper::getPluginSelector($bam->sources(), $this->t('Restore To')); + + $conf_schema = $bam->plugins()->map('configSchema', ['operation' => 'restore']); + $form += DrupalConfigHelper::buildFormFromSchema($conf_schema, $bam->plugins()->config()); + + return parent::buildForm($form, $form_state); + } + + /** + * Form submission handler. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $config = $form_state->getValues(); + backup_migrate_perform_restore($config['source_id'], $this->destination->id(), $this->backup_id, $config); + + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/modules/backup_migrate/src/Form/DestinationForm.php b/modules/backup_migrate/src/Form/DestinationForm.php new file mode 100644 index 0000000..0f03963 --- /dev/null +++ b/modules/backup_migrate/src/Form/DestinationForm.php @@ -0,0 +1,26 @@ +t("Label for the Backup Destination."); + $form['id']['#machine_name']['exists'] = '\Drupal\backup_migrate\Entity\Destination::load'; + + return $form; + } + +} diff --git a/modules/backup_migrate/src/Form/EntityDeleteForm.php b/modules/backup_migrate/src/Form/EntityDeleteForm.php new file mode 100644 index 0000000..caef545 --- /dev/null +++ b/modules/backup_migrate/src/Form/EntityDeleteForm.php @@ -0,0 +1,50 @@ +t( + 'Are you sure you want to delete %name?', + ['%name' => $this->entity->label()] + ); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return $this->entity->toUrl('collection'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->entity->delete(); + + drupal_set_message( + $this->t('Deleted @label.', ['@label' => $this->entity->label()]) + ); + + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/modules/backup_migrate/src/Form/ScheduleDeleteForm.php b/modules/backup_migrate/src/Form/ScheduleDeleteForm.php new file mode 100644 index 0000000..59bb4e6 --- /dev/null +++ b/modules/backup_migrate/src/Form/ScheduleDeleteForm.php @@ -0,0 +1,53 @@ +t('Are you sure you want to delete %name?', ['%name' => $this->entity->label()]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('entity.backup_migrate_schedule.collection'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->entity->delete(); + + drupal_set_message( + $this->t('content @type: deleted @label.', + [ + '@type' => $this->entity->bundle(), + '@label' => $this->entity->label() + ] + ) + ); + + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/modules/backup_migrate/src/Form/ScheduleForm.php b/modules/backup_migrate/src/Form/ScheduleForm.php new file mode 100644 index 0000000..d449912 --- /dev/null +++ b/modules/backup_migrate/src/Form/ScheduleForm.php @@ -0,0 +1,148 @@ +entity; + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Schedule Name'), + '#maxlength' => 255, + '#default_value' => $backup_migrate_schedule->label(), + '#required' => TRUE, + ]; + + $form['id'] = [ + '#type' => 'machine_name', + '#default_value' => $backup_migrate_schedule->id(), + '#machine_name' => [ + 'exists' => '\Drupal\backup_migrate\Entity\Schedule::load', + ], + '#disabled' => !$backup_migrate_schedule->isNew(), + ]; + + $form['enabled'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Schedule enabled'), + '#default_value' => $backup_migrate_schedule->get('enabled'), + ]; + + $bam = backup_migrate_get_service_object([], ['nobrowser' => TRUE]); + $form['source_id'] = DrupalConfigHelper::getSourceSelector( + $bam, + t('Backup Source'), + $backup_migrate_schedule->get('source_id') + ); + $form['destination_id'] = DrupalConfigHelper::getDestinationSelector( + $bam, + t('Backup Destination'), + $backup_migrate_schedule->get('destination_id') + ); + + $form['settings_profile_id'] = DrupalConfigHelper::getSettingsProfileSelector( + t('Settings Profile'), + $backup_migrate_schedule->get('settings_profile_id') + ); + + $period = Schedule::secondsToPeriod($backup_migrate_schedule->get('period')); + $form['period_container'] = [ + // Reset #parents so the additional container does not appear. + '#parents' => [], + '#type' => 'fieldset', + '#title' => $this->t('Frequency'), + '#field_prefix' => $this->t('Run every'), + '#attributes' => [ + 'class' => [ + 'container-inline', + 'fieldgroup', + 'form-composite' + ] + ], + ]; + $form['period_container']['period_number'] = [ + '#type' => 'number', + '#default_value' => $period['number'], + '#min' => 1, + '#title' => $this->t('Period number'), + '#title_display' => 'invisible', + '#size' => 2, + ]; + $form['period_container']['period_type'] = [ + '#type' => 'select', + '#title' => $this->t('Period type'), + '#title_display' => 'invisible', + '#options' => [], + '#default_value' => $period['type'], + ]; + foreach (Schedule::getPeriodTypes() as $key => $type) { + $form['period_container']['period_type']['#options'][$key] = $type['title']; + } + + $form['keep'] = [ + '#type' => 'textfield', + '#title' => $this->t('Number to keep'), + '#default_value' => $backup_migrate_schedule->get('keep'), + '#description' => $this->t('The number of backups to retain. Once this number is reached, the oldest backup will be deleted to make room for the most recent backup. Leave blank to keep all backups.'), + '#size' => 10, + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function buildEntity(array $form, FormStateInterface $form_state) { + // Save period. + $type = Schedule::getPeriodType($form_state->getValue('period_type')); + $seconds = Schedule::periodToSeconds([ + 'number' => $form_state->getValue('period_number'), + 'type' => $type + ]); + + $form_state->setValue('period', $seconds); + + return parent::buildEntity($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $backup_migrate_schedule = $this->entity; + $status = $backup_migrate_schedule->save(); + + switch ($status) { + case SAVED_NEW: + drupal_set_message($this->t('Created the %label Schedule.', [ + '%label' => $backup_migrate_schedule->label(), + ])); + break; + + default: + drupal_set_message($this->t('Saved the %label Schedule.', [ + '%label' => $backup_migrate_schedule->label(), + ])); + } + $form_state->setRedirectUrl($backup_migrate_schedule->toUrl('collection')); + } + +} diff --git a/modules/backup_migrate/src/Form/SettingsProfileDeleteForm.php b/modules/backup_migrate/src/Form/SettingsProfileDeleteForm.php new file mode 100644 index 0000000..c9aadd0 --- /dev/null +++ b/modules/backup_migrate/src/Form/SettingsProfileDeleteForm.php @@ -0,0 +1,53 @@ +t('Are you sure you want to delete %name?', ['%name' => $this->entity->label()]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('entity.backup_migrate_settings.collection'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->entity->delete(); + + drupal_set_message( + $this->t('content @type: deleted @label.', + [ + '@type' => $this->entity->bundle(), + '@label' => $this->entity->label() + ] + ) + ); + + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/modules/backup_migrate/src/Form/SettingsProfileForm.php b/modules/backup_migrate/src/Form/SettingsProfileForm.php new file mode 100644 index 0000000..a27b2fe --- /dev/null +++ b/modules/backup_migrate/src/Form/SettingsProfileForm.php @@ -0,0 +1,70 @@ +entity; + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => $backup_migrate_settings->label(), + '#required' => TRUE, + ]; + + $form['id'] = [ + '#type' => 'machine_name', + '#default_value' => $backup_migrate_settings->id(), + '#machine_name' => [ + 'exists' => '\Drupal\backup_migrate\Entity\SettingsProfile::load', + ], + '#disabled' => !$backup_migrate_settings->isNew(), + ]; + + $bam = backup_migrate_get_service_object($backup_migrate_settings->get('config')); + + $form['config'] = DrupalConfigHelper::buildAllPluginsForm($bam->plugins(), 'backup', ['config']); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $backup_migrate_settings = $this->entity; + + $status = $backup_migrate_settings->save(); + + switch ($status) { + case SAVED_NEW: + drupal_set_message($this->t('Created the %label Settings Profile.', [ + '%label' => $backup_migrate_settings->label(), + ])); + break; + + default: + drupal_set_message($this->t('Saved the %label Settings Profile.', [ + '%label' => $backup_migrate_settings->label(), + ])); + } + $form_state->setRedirectUrl($backup_migrate_settings->toUrl('collection')); + } + +} diff --git a/modules/backup_migrate/src/Form/SourceForm.php b/modules/backup_migrate/src/Form/SourceForm.php new file mode 100644 index 0000000..60c0218 --- /dev/null +++ b/modules/backup_migrate/src/Form/SourceForm.php @@ -0,0 +1,26 @@ +t("Label for the Backup Source."); + $form['id']['#machine_name']['exists'] = '\Drupal\backup_migrate\Entity\Source::load'; + + return $form; + } + +} diff --git a/modules/backup_migrate/src/Form/WrapperEntityForm.php b/modules/backup_migrate/src/Form/WrapperEntityForm.php new file mode 100644 index 0000000..21314d3 --- /dev/null +++ b/modules/backup_migrate/src/Form/WrapperEntityForm.php @@ -0,0 +1,123 @@ + 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => $this->entity->label(), + '#required' => TRUE, + ]; + + $form['id'] = [ + '#type' => 'machine_name', + '#default_value' => $this->entity->id(), + '#machine_name' => [], + '#disabled' => !$this->entity->isNew(), + ]; + + if (!$this->entity->get('type')) { + $form['type'] = [ + '#type' => 'radios', + '#title' => $this->t('Type'), + ]; + foreach ($this->entity->getPluginManager()->getDefinitions() as $type) { + if (empty($type['locked'])) { + $form['type']['#options'][$type['id']] = $type['title']; + $form['type'][$type['id']]['#description'] = $type['description']; + } + } + } + else { + $type = $this->entity->getPlugin()->getPluginDefinition(); + $form['type'] = [ + '#type' => 'value', + '#value' => $type['id'], + '#markup' => $this->t("Type: @type", ['@type' => $type['title']]), + ]; + + if ($bam_plugin = $this->entity->getObject()) { + $form['config'] = DrupalConfigHelper::buildPluginForm($bam_plugin, 'initialize', ['config']); + } + } + return $form; + } + + /** + * {@inheritdoc} + */ + protected function actions(array $form, FormStateInterface $form_state) { + $actions = parent::actions($form, $form_state); + + if ($this->entity->isNew()) { + $actions['submit']['#value'] = $this->t('Save and edit'); + } + + return $actions; + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $entity = $this->entity; + + $status = $entity->save(); + + switch ($status) { + case SAVED_NEW: + drupal_set_message($this->t('Created %label.', [ + '%label' => $entity->label(), + ])); + $form_state->setRedirectUrl($entity->toUrl('edit-form')); + break; + + default: + drupal_set_message($this->t('Saved %label.', [ + '%label' => $entity->label(), + ])); + $form_state->setRedirectUrl($entity->toUrl('collection')); + break; + } + } + + /** + * Override this function. + * + * Let it store the config which would otherwise be removed for some reason. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity the current form should operate upon. + * @param array $form + * A nested array of form elements comprising the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) { + $values = $form_state->getValues(); + + foreach ($values as $key => $value) { + $entity->set($key, $value); + } + } + +} diff --git a/modules/backup_migrate/src/Plugin/BackupMigrateDestination/DirectoryDestinationPlugin.php b/modules/backup_migrate/src/Plugin/BackupMigrateDestination/DirectoryDestinationPlugin.php new file mode 100644 index 0000000..269f682 --- /dev/null +++ b/modules/backup_migrate/src/Plugin/BackupMigrateDestination/DirectoryDestinationPlugin.php @@ -0,0 +1,17 @@ +getConfig(); + foreach ($info as $key => $value) { + $conf->set($key, $value); + } + return new DrupalMySQLiSource($conf); + } + + return NULL; + } + + /** + * {@inheritdoc} + */ + public function alterBackupMigrate(BackupMigrateInterface $bam, $key, $options = []) { + if ($source = $this->getObject()) { + $bam->sources()->add($key, $source); + // @TODO: This needs a better solution. + $config = [ + 'exclude_tables' => [], + 'nodata_tables' => [ + 'cache_advagg_minify', + 'cache_bootstrap', + 'cache_config', + 'cache_container', + 'cache_data', + 'cache_default', + 'cache_discovery', + 'cache_discovery_migration', + 'cache_dynamic_page_cache', + 'cache_entity', + 'cache_menu', + 'cache_migrate', + 'cache_render', + 'cache_rest', + 'cache_toolbar', + 'sessions', + 'watchdog', + 'webprofiler', + ], + ]; + + // @TODO: Allow modules to add their own excluded tables. + $bam->plugins()->add('db_exclude', new DBExcludeFilter(new Config($config))); + } + } + +} diff --git a/modules/backup_migrate/src/Plugin/BackupMigrateSource/DrupalFilesSourcePlugin.php b/modules/backup_migrate/src/Plugin/BackupMigrateSource/DrupalFilesSourcePlugin.php new file mode 100644 index 0000000..f0f3d7a --- /dev/null +++ b/modules/backup_migrate/src/Plugin/BackupMigrateSource/DrupalFilesSourcePlugin.php @@ -0,0 +1,66 @@ +getObject(); + $bam->sources()->add($key, $source); + + $config = [ + 'exclude_filepaths' => [], + 'source' => $source + ]; + + switch ($this->getConfig()->get('directory')) { + case 'public://': + $config['exclude_filepaths'] = [ + 'js', + 'css', + 'php', + 'styles', + 'config_*', + '.htaccess', + ]; + break; + + case 'private://': + $config['exclude_filepaths'] = [ + 'backup_migrate', + ]; + break; + } + + // @TODO: Allow modules to add their own excluded defaults. + $bam->plugins()->add($key . '_exclude', new FileExcludeFilter(new Config($config))); + } + +} diff --git a/modules/backup_migrate/src/Plugin/BackupMigrateSource/EntireSiteSourcePlugin.php b/modules/backup_migrate/src/Plugin/BackupMigrateSource/EntireSiteSourcePlugin.php new file mode 100644 index 0000000..e96017c --- /dev/null +++ b/modules/backup_migrate/src/Plugin/BackupMigrateSource/EntireSiteSourcePlugin.php @@ -0,0 +1,54 @@ +getConfig(); + $conf->set('directory', DRUPAL_ROOT); + $this->db_source = new DrupalMySQLiSource(new Config($info)); + return new DrupalSiteArchiveSource($conf, $this->db_source); + } + + return NULL; + } + + /** + * {@inheritdoc} + */ + public function alterBackupMigrate(BackupMigrateInterface $bam, $key, $options = []) { + if ($source = $this->getObject()) { + $bam->sources()->add($key, $source); + $bam->sources()->add('default_db', $this->db_source); + } + } + +} diff --git a/modules/backup_migrate/src/Plugin/BackupMigrateSource/FileDirectorySourcePlugin.php b/modules/backup_migrate/src/Plugin/BackupMigrateSource/FileDirectorySourcePlugin.php new file mode 100644 index 0000000..f053eb6 --- /dev/null +++ b/modules/backup_migrate/src/Plugin/BackupMigrateSource/FileDirectorySourcePlugin.php @@ -0,0 +1,17 @@ +_getConnection()) { + // Open (or rewind) the file. + $file->openForRead(); + + // Read one line at a time and run the query. + while ($line = $this->_readSQLCommand($file)) { + // if (_backup_migrate_check_timeout()) { + // return FALSE; + // } + if ($line) { + // Execute the sql query from the file. + $stmt = $conn->prepare($line); + if(!$stmt) return false; + $stmt->execute(); + $num++; + } + } + // Close the file, we're done reading it. + $file->close(); + } + return $num; + } + +} diff --git a/modules/backup_migrate/src/Source/DrupalPublicFilesSource.php b/modules/backup_migrate/src/Source/DrupalPublicFilesSource.php new file mode 100644 index 0000000..fb92f4c --- /dev/null +++ b/modules/backup_migrate/src/Source/DrupalPublicFilesSource.php @@ -0,0 +1,28 @@ + 'public://', + ]; + + return new Config($config); + } + +} diff --git a/modules/backup_migrate/src/Source/DrupalSiteArchiveSource.php b/modules/backup_migrate/src/Source/DrupalSiteArchiveSource.php new file mode 100644 index 0000000..3d25c23 --- /dev/null +++ b/modules/backup_migrate/src/Source/DrupalSiteArchiveSource.php @@ -0,0 +1,146 @@ +db_source = $db; + } + + /** + * Get a list if files to be backed up from the given directory. Do not + * include files that match the 'exclude_filepaths' setting. + * + * @param string $dir The name of the directory to list. + * + * @return array + * + * @throws \BackupMigrate\Core\Exception\BackupMigrateException + * @throws \BackupMigrate\Core\Exception\IgnorableException + * + * @internal param $directory + */ + protected function getFilesToBackup($dir) { + $files = []; + + // Add the database dump. + // @TODO: realpath contains the wrong filename and the PEAR archiver cannot rename files. + $db = $this->getDbSource()->exportToFile(); + $files['database.sql'] = $db->realpath(); + + // Add the manifest file. + $manifest = $this->getManifestFile(); + $files['MANIFEST.ini'] = $manifest->realpath(); + + // Get all the files in the site. + foreach (parent::getFilesToBackup($dir) as $new => $real) { + // Prepend 'docroot' onto the local path. + $files['docroot/' . $new] = $real; + } + + return $files; + } + + /** + * Import to this source from the given backup file. This is the main restore + * function for this source. + * + * @param BackupFileReadableInterface $file + * The file to read the backup from. It will not be opened for reading + * + * @return bool|void + */ + public function importFromFile(BackupFileReadableInterface $file) { + // TODO: Implement importFromFile() method. + } + + /** + * Get a file which contains the file. + * + * @return \BackupMigrate\Core\File\BackupFileWritableInterface + */ + protected function getManifestFile() { + $out = $this->getTempFileManager()->create('ini'); + + $info = [ + 'Global' => [ + 'datestamp' => time(), + "formatversion" => "2011-07-02", + "generator" => "Backup and Migrate (http://drupal.org/project/backup_migrate)", + "generatorversion" => BACKUP_MIGRATE_MODULE_VERSION, + ], + 'Site 0' => [ + 'version' => \Drupal::VERSION, + 'name' => "Example.com", + 'docroot' => "docroot", + 'sitedir' => "docroot/sites/default", + 'database-file-default' => "database.sql", + 'database-file-driver' => "mysql", + 'files-private' => "docroot/sites/default/private", + 'files-public' => "docroot/sites/default/files" + ] + ]; + + $out->writeAll($this->arrayToINI($info)); + return $out; + } + + + /** + * Translate a 2d array to an INI string which can be written to a file. + * + * @param array $info + * The array to convert. Must be an array of sections each of which is an array of field/value pairs. + * + * @return string + * The data in INI format. + */ + private function arrayToINI($info) { + $content = ""; + foreach ($info as $section => $data) { + $content .= '[' . $section . ']' . "\n"; + foreach ($data as $key => $val) { + $content .= $key . " = \"" . $val . "\"\n"; + } + $content .= "\n"; + } + return $content; + + } + + /** + * @return SourceInterface + */ + public function getDbSource() { + return $this->db_source; + } + +} diff --git a/modules/backup_migrate/tests/src/Functional/BackupMigrateEnablingTest.php b/modules/backup_migrate/tests/src/Functional/BackupMigrateEnablingTest.php new file mode 100644 index 0000000..1a0d696 --- /dev/null +++ b/modules/backup_migrate/tests/src/Functional/BackupMigrateEnablingTest.php @@ -0,0 +1,39 @@ +drupalGet(''); + $this->assertSession()->statusCodeEquals(200); + } + +} diff --git a/modules/backup_migrate/tests/src/Functional/BackupMigratePageLoadTest.php b/modules/backup_migrate/tests/src/Functional/BackupMigratePageLoadTest.php new file mode 100644 index 0000000..9fd9bb6 --- /dev/null +++ b/modules/backup_migrate/tests/src/Functional/BackupMigratePageLoadTest.php @@ -0,0 +1,72 @@ +container->get('router.builder')->rebuild(); + + // Ensure backup_migrate folder exists, the + // `admin/config/development/backup_migrate/backups` path will fail without + // this. + $path = 'private://backup_migrate/'; + file_prepare_directory($path, FILE_CREATE_DIRECTORY); + } + + /** + * Tests if site quick backup function loads. + */ + public function testPages() { + $account = $this->drupalCreateUser([ + 'access backup files', + 'administer backup and migrate', + 'perform backup', + 'restore from backup', + ]); + $this->drupalLogin($account); + + $paths = [ + 'admin/config/development/backup_migrate' => ['text' => 'Quick Backup'], + 'admin/config/development/backup_migrate/advanced' => ['text' => 'Advanced Backup'], + 'admin/config/development/backup_migrate/restore' => ['text' => 'Restore'], + 'admin/config/development/backup_migrate/backups' => ['text' => 'Backups'], + 'admin/config/development/backup_migrate/schedule' => ['text' => 'Schedule'], + 'admin/config/development/backup_migrate/schedule/add' => ['text' => 'Add schedule'], + 'admin/config/development/backup_migrate/settings' => ['text' => 'Settings'], + 'admin/config/development/backup_migrate/settings/add' => ['text' => 'Add settings profile'], + 'admin/config/development/backup_migrate/settings/destination' => ['text' => 'Backup Destination'], + 'admin/config/development/backup_migrate/settings/destination/add' => ['text' => 'Add destination'], + 'admin/config/development/backup_migrate/settings/source' => ['text' => 'Backup sources'], + 'admin/config/development/backup_migrate/settings/source/add' => ['text' => 'Add Backup Source'], + ]; + + foreach ($paths as $path => $settings) { + $this->drupalGet($path); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->pageTextContains($settings['text']); + } + } + +} diff --git a/modules/backup_migrate/tests/src/Functional/BackupMigratePermissionsTest.php b/modules/backup_migrate/tests/src/Functional/BackupMigratePermissionsTest.php new file mode 100644 index 0000000..c13ec52 --- /dev/null +++ b/modules/backup_migrate/tests/src/Functional/BackupMigratePermissionsTest.php @@ -0,0 +1,188 @@ +drupalLogin($this->drupalCreateUser($permissions)); + + // Run the path tests. + $this->checkPaths($ok_paths); + } + + /** + * Check a set of paths to see if they are accessible. + * + * @param array $ok_paths + * The paths that are expected return a 200 response, all others are + * expected to return a 403 response. + */ + private function checkPaths(array $ok_paths = []) { + foreach ($this->allPaths as $path) { + $this->drupalGet($path); + if (in_array($path, $ok_paths)) { + $this->assertSession()->statusCodeEquals(200); + } + else { + $this->assertSession()->statusCodeEquals(403); + } + } + } + + /** + * Tests access for anonymous users. + */ + public function testAnonymous() { + // Run the tests without any $ok_paths as they should all be 403. + $this->checkPaths([]); + } + + /** + * Tests access for an authenticated user without any permissions. + */ + public function testAuthenticated() { + // No permissions as the visitor can't do anything. + $permissions = []; + // No paths should be ok. + $ok_paths = []; + + // Run the tests. + $this->checkPathsWithUser($ok_paths, $permissions); + } + + /** + * Tests access for 'administer backup and migrate' permission. + */ + public function testAdminister() { + // The permission(s) to test. + $permissions = [ + 'administer backup and migrate', + ]; + // Only settings pages should work. + $ok_paths = [ + 'admin/config/development/backup_migrate/schedule', + 'admin/config/development/backup_migrate/schedule/add', + 'admin/config/development/backup_migrate/settings', + 'admin/config/development/backup_migrate/settings/add', + 'admin/config/development/backup_migrate/settings/destination', + 'admin/config/development/backup_migrate/settings/destination/add', + 'admin/config/development/backup_migrate/settings/source', + 'admin/config/development/backup_migrate/settings/source/add', + 'admin/config/development/backup_migrate/settings/destination/backups/private_files/delete/none.mysql.gz', + ]; + + // Run the tests. + $this->checkPathsWithUser($ok_paths, $permissions); + } + + /** + * Tests access for 'perform backup' permission. + */ + public function testPerformBackup() { + // The permission(s) to test. + $permissions = [ + 'perform backup', + ]; + // The paths to check. + $ok_paths = [ + 'admin/config/development/backup_migrate', + 'admin/config/development/backup_migrate/advanced', + ]; + + // Run the tests. + $this->checkPathsWithUser($ok_paths, $permissions); + } + + /** + * Tests access for 'restore from backup' permission. + */ + public function testRestoreFromBackup() { + // The permission(s) to test. + $permissions = [ + 'restore from backup', + ]; + // The paths to check. + $ok_paths = [ + 'admin/config/development/backup_migrate/restore', + ]; + + // Run the tests. + $this->checkPathsWithUser($ok_paths, $permissions); + } + + /** + * Tests access for 'access backup files' permission. + */ + public function testAccessBackupFiles() { + // The permission(s) to test. + $permissions = [ + 'access backup files', + ]; + // The paths to check. + $ok_paths = [ + 'admin/config/development/backup_migrate/backups', + ]; + + // Run the tests. + $this->checkPathsWithUser($ok_paths, $permissions); + } + +} diff --git a/modules/backup_migrate/tests/src/Functional/BackupMigrateQuickBackupTest.php b/modules/backup_migrate/tests/src/Functional/BackupMigrateQuickBackupTest.php new file mode 100644 index 0000000..4bb4515 --- /dev/null +++ b/modules/backup_migrate/tests/src/Functional/BackupMigrateQuickBackupTest.php @@ -0,0 +1,65 @@ +drupalLogin($this->drupalCreateUser([ + 'perform backup', + 'access backup files', + 'administer backup and migrate', + ])); + $this->drupalGet('admin/config/development/backup_migrate'); + $this->assertSession()->statusCodeEquals(200); + + // Submit the quick backup form. + $data = [ + 'source_id' => 'default_db', + 'destination_id' => 'private_files', + ]; + $this->submitForm($data, t('Backup now')); + + // Get backups page. + $this->drupalGet('admin/config/development/backup_migrate/backups'); + $this->assertSession()->statusCodeEquals(200); + + // Searching for the existing backups. + $page = $this->getSession()->getPage(); + $table = $page->find('css', 'table'); + $row = $table->find('css', sprintf('tbody tr:contains("%s")', '.mysql.gz')); + $this->assertNotNull($row); + } + +} diff --git a/modules/fontawesome-8.x-2.14.zip b/modules/fontawesome-8.x-2.14.zip new file mode 100644 index 0000000..1b6f446 Binary files /dev/null and b/modules/fontawesome-8.x-2.14.zip differ diff --git a/web/modules/fontawesome/LICENSE.txt b/web/modules/fontawesome/LICENSE.txt new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/web/modules/fontawesome/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/web/modules/fontawesome/README.txt b/web/modules/fontawesome/README.txt new file mode 100644 index 0000000..a65dfaf --- /dev/null +++ b/web/modules/fontawesome/README.txt @@ -0,0 +1,104 @@ + +CONTENTS OF THIS FILE +--------------------- + + * Introduction + * Installation + * Usage + * Credits + + +INTRODUCTION 8.2.x version +------------ +Font Awesome (http://fontawesome.com) is the web's most popular icon set and +toolkit. This release of the Font Awesome Icons module supports Font Awesome +versions higher than 5.0. For older versions of Font Awesome, you should +download and install Font Awesome Icons 8.1.x. See the Font Awesome Icons +page on Drupal.org for more information. + +"fontawesome" provides integration of "Font Awesome" with Drupal. Once enabled +"Font Awesome" icon fonts could be used as: + +1. Directly inside of any HTML (node/block/view/panel). Inside HTML you can + place Font Awesome icons just about anywhere with an tag. + + Example for an info icon: + + See more examples of using "Font Awesome" within HTML at: + https://fontawesome.com/how-to-use/on-the-web/referencing-icons/basic-use + + +INSTALLATION +------------ + +1. Using Drush (https://github.com/drush-ops/drush#readme) + + $ drush en fontawesome + + Upon enabling, this will also attempt to download and install the library + in `/libraries/fontawesome`. If, for whatever reason, this process + fails, you can re-run the library install manually by first clearing Drush + caches: + + $ drush cc drush + + and then using another drush command:- + + (Drush 8) + $ drush fa-download + (Drush 9) + $ drush fa:download + +2. Manually + + a. Install the "Font Awesome" library following one of these 2 options: + - run "drush fa-download" (recommended, it will download the right + package and extract it at the right place for you.) + - manual install: Download & extract "Font Awesome" + (http://fontawesome.com) and place inside + "/libraries/fontawesome" directory. The JS file should + be at /libraries/fontawesome/js/all.js + Direct link for downloading latest version (current is v5.10.0) is: + https://use.fontawesome.com/releases/v5.10.0/fontawesome-free-5.10.0-web.zip + b. Enable the module at Administer >> Site building >> Modules. + + +USAGE +_____ +Font Awesome can be used in many ways - you can manually insert Font Awesome +tags wherever you see fit after enabling the module, but there are other ways +as well. See + https://fontawesome.com/how-to-use/on-the-web/referencing-icons/basic-use +for information on basic usage. + +CSS Pseudo-elements - if you are using the older version of Font Awesome, CSS +with webfonts, you can use CSS Pseudo-elements for inserting your icons rather +than the default method. See + https://fontawesome.com/how-to-use/on-the-web/advanced/css-pseudo-elements +for more information on how to add the icons through CSS. + +Font Awesome icon field - this module includes the option to add a Font Awesome +icon field to any of your content types without the need for coding. + +Font Awesome CKEditor plugin - this module includes a CKEditor plugin which +will allow you to insert Font Awesome icons into any CKEditor text field with +the plugin enabled. It can be enabled under + Configuration -> Content authoring -> Text formats and editors +From here, simply add the icon to your active toolbar (it looks like a flag). +Please note that in order to use SVG with JS version of Font Awesome, you will +need to either disable the "Correct faulty and chopped off HTML" filter, or you +will have to add the required SVG tags to the exception list. A list of SVG +tags can be found here: + https://www.w3.org/TR/SVG11/eltindex.html + or + https://developer.mozilla.org/en-US/docs/Web/SVG/Element + + +CREDITS +------- +* Rob Loach (RobLoach) http://robloach.net +* Inder Singh (inders) http://indersingh.com | https://www.drupal.org/u/inders +* Mark Carver https://www.drupal.org/u/mark-carver +* Brian Gilbert https://drupal.org/u/realityloop +* Daniel Moberly https://drupal.org/u/danielmoberly +* Truls S. Yggeseth https://drupal.org/u/truls1502 diff --git a/web/modules/fontawesome/composer.json b/web/modules/fontawesome/composer.json new file mode 100644 index 0000000..88aeda7 --- /dev/null +++ b/web/modules/fontawesome/composer.json @@ -0,0 +1,15 @@ +{ + "name": "drupal/fontawesome", + "description": "The web's most popular icon set and toolkit.", + "type": "drupal-module", + "license": "GPL-2.0+", + "homepage": "https://www.drupal.org/project/fontawesome", + "minimum-stability": "dev", + "extra": { + "drush": { + "services": { + "drush.services.yml": "^9" + } + } + } +} diff --git a/web/modules/fontawesome/config/install/fontawesome.settings.yml b/web/modules/fontawesome/config/install/fontawesome.settings.yml new file mode 100644 index 0000000..fbdde23 --- /dev/null +++ b/web/modules/fontawesome/config/install/fontawesome.settings.yml @@ -0,0 +1,10 @@ +tag: 'i' +method: 'svg' +use_cdn: true +external_svg_location: 'https://use.fontawesome.com/releases/v5.10.0/js/all.js' +use_shim: true +external_shim_location: 'https://use.fontawesome.com/releases/v5.10.0/js/v4-shims.js' +use_solid_file: true +use_regular_file: true +use_light_file: true +use_brands_file: true diff --git a/web/modules/fontawesome/config/schema/fontawesome.schema.yml b/web/modules/fontawesome/config/schema/fontawesome.schema.yml new file mode 100644 index 0000000..39f479a --- /dev/null +++ b/web/modules/fontawesome/config/schema/fontawesome.schema.yml @@ -0,0 +1,37 @@ +fontawesome.settings: + type: config_object + label: 'Font Awesome settings' + mapping: + tag: + type: string + label: 'Tag used for Font Awesome elements' + method: + type: string + label: 'Method used for delivering Font Awesome' + use_cdn: + type: boolean + label: 'Use CDN to serve Font Awesome' + external_svg_location: + type: string + label: 'Location of the external CDN for Font Awesome' + use_shim: + type: boolean + label: 'Use Shim File for v4 compatibility' + external_shim_location: + type: string + label: 'Location of the external CDN for Font Awesome shim file' + allow_pseudo_elements: + type: boolean + label: 'Allow CSS pseudo elements with webfonts' + use_solid_file: + type: boolean + label: 'Use the Font Awesome solid icons file' + use_regular_file: + type: boolean + label: 'Use the Font Awesome regular icons file' + use_light_file: + type: boolean + label: 'Use the Font Awesome light icons file' + use_brands_File: + type: boolean + label: 'Use the Font Awesome brands icons file' diff --git a/web/modules/fontawesome/drush.services.yml b/web/modules/fontawesome/drush.services.yml new file mode 100644 index 0000000..faa5af1 --- /dev/null +++ b/web/modules/fontawesome/drush.services.yml @@ -0,0 +1,5 @@ +services: + fontawesome.commands: + class: \Drupal\fontawesome\Commands\FontawesomeCommands + tags: + - { name: drush.command } diff --git a/web/modules/fontawesome/fontawesome.info.yml b/web/modules/fontawesome/fontawesome.info.yml new file mode 100644 index 0000000..1b68b91 --- /dev/null +++ b/web/modules/fontawesome/fontawesome.info.yml @@ -0,0 +1,12 @@ +name: 'Font Awesome' +type: module +description: 'The most popular icon set and toolkit on the web.' + +# core: 8.x +configure: fontawesome.admin_settings + +# Information added by Drupal.org packaging script on 2019-08-02 +version: '8.x-2.14' +core: '8.x' +project: 'fontawesome' +datestamp: 1564762409 diff --git a/web/modules/fontawesome/fontawesome.install b/web/modules/fontawesome/fontawesome.install new file mode 100644 index 0000000..28fc3f8 --- /dev/null +++ b/web/modules/fontawesome/fontawesome.install @@ -0,0 +1,105 @@ + t('Font Awesome 5'), + ]; + + // Load the configuration settings. + $configuration_settings = \Drupal::config('fontawesome.settings'); + + // Check if Font Awesome is installed. + if (fontawesome_check_installed()) { + // Get the version. + if ($configuration_settings->get('method') == 'webfonts') { + $version = t('Webfonts with CSS'); + } + elseif ($configuration_settings->get('use_cdn')) { + $version = t('CDN SVG with JS'); + } + else { + $version = t('SVG with JS'); + } + + // First check if we're using everything. + if (is_null($configuration_settings->get('use_solid_file')) === TRUE || ($configuration_settings->get('use_solid_file') && $configuration_settings->get('use_regular_file') && $configuration_settings->get('use_light_file') && $configuration_settings->get('use_brands_file'))) { + // Attach the main library. + $loadedMessages = [ + t('All icons loaded'), + ]; + } + // Else we attach the libraries piecemeal. + else { + $loadedMessages = []; + if ($configuration_settings->get('use_solid_file')) { + $loadedMessages[] = t('Solid icons loaded'); + } + if ($configuration_settings->get('use_regular_file')) { + $loadedMessages[] = t('Regular icons loaded'); + } + if ($configuration_settings->get('use_light_file')) { + $loadedMessages[] = t('Light icons loaded'); + } + if ($configuration_settings->get('use_brands_file')) { + $loadedMessages[] = t('Brands icons loaded'); + } + } + + $requirements['fontawesome']['severity'] = REQUIREMENT_OK; + $requirements['fontawesome']['value'] = t('Font Awesome 5 library is installed. Using %version version. (@moreInfoLink)', [ + '%version' => $version, + '@moreInfoLink' => Link::createFromRoute(t('more information'), 'fontawesome.admin_settings')->toString(), + ]); + $requirements['fontawesome']['description'] = [ + '#theme' => 'item_list', + '#items' => $loadedMessages, + '#title' => '', + '#list_type' => 'ul', + '#attributes' => [], + ]; + } + else { + $requirements['fontawesome']['severity'] = REQUIREMENT_ERROR; + $requirements['fontawesome']['value'] = t('Not installed'); + $requirements['fontawesome']['description'] = t('The Font Awesome 5 library could not be found. Please verify Font Awesome 5 is installed correctly or that the CDN has been activated and properly configured. Please see the @adminPage and the Font Awesome module README file for more details.', [ + '@adminPage' => Link::createFromRoute(t('admin page'), 'fontawesome.admin_settings')->toString(), + ]); + } + } + + return $requirements; +} + +/** + * Implements hook_uninstall(). + */ +function fontawesome_uninstall() { + // Delete set variables. + $query = \Drupal::database()->delete('config'); + $query->condition('name', 'fontawesome.settings'); + $query->execute(); + $query = \Drupal::database()->delete('key_value'); + $query->condition('name', 'fontawesome'); + $query->execute(); + + // Icon API module : Delete fontawesome icon bundle & clear cache. + if (\Drupal::moduleHandler()->moduleExists('icon') && ($cache = \Drupal::cache()->get('icon_bundles')) && !empty($cache->data)) { + $fa_icon_bundle = isset($cache->data['fontawesome']) ? $cache->data['fontawesome'] : []; + $fa_icon_bundle['path'] = isset($fa_icon_bundle['path']) ? $fa_icon_bundle['path'] : 'fontawesome'; + icon_bundle_delete($fa_icon_bundle); + } +} diff --git a/web/modules/fontawesome/fontawesome.libraries.yml b/web/modules/fontawesome/fontawesome.libraries.yml new file mode 100644 index 0000000..04976f1 --- /dev/null +++ b/web/modules/fontawesome/fontawesome.libraries.yml @@ -0,0 +1,125 @@ +fontawesome.svg: + remote: &fontawesome_remote https://use.fontawesome.com/releases/v5.10.0/fontawesome-free-5.10.0-web.zip + license: &fontawesome_svg_license + name: CC BY 4.0 + url: https://fontawesome.com/license + gpl-compatible: true + version: &fontawesome_version "5.10.0" + header: true + js: + /libraries/fontawesome/js/all.js: { minified: true, attributes: { defer: true } } + +fontawesome.svg.shim: + version: *fontawesome_version + license: *fontawesome_svg_license + header: true + js: + /libraries/fontawesome/js/v4-shims.js: { minified: true, attributes: { defer: true } } + dependencies: + - fontawesome/fontawesome.svg + +fontawesome.svg.base: + version: *fontawesome_version + license: *fontawesome_svg_license + header: true + js: + /libraries/fontawesome/js/fontawesome.js: { minified: true, attributes: { defer: true } } + +fontawesome.svg.solid: + version: *fontawesome_version + license: *fontawesome_svg_license + header: true + js: + /libraries/fontawesome/js/solid.js: { minified: true, attributes: { defer: true } } + dependencies: + - fontawesome/fontawesome.svg.base + +fontawesome.svg.regular: + version: *fontawesome_version + license: *fontawesome_svg_license + header: true + js: + /libraries/fontawesome/js/regular.js: { minified: true, attributes: { defer: true } } + dependencies: + - fontawesome/fontawesome.svg.base + +fontawesome.svg.light: + version: *fontawesome_version + license: *fontawesome_svg_license + header: true + js: + /libraries/fontawesome/js/light.js: { minified: true, attributes: { defer: true } } + dependencies: + - fontawesome/fontawesome.svg.base + +fontawesome.svg.brands: + version: *fontawesome_version + license: *fontawesome_svg_license + header: true + js: + /libraries/fontawesome/js/brands.js: { minified: true, attributes: { defer: true } } + dependencies: + - fontawesome/fontawesome.svg.base + +fontawesome.webfonts: + remote: *fontawesome_remote + license: &fontawesome_webfonts_license + name: SIL OFL 1.1 + url: https://fontawesome.com/license + gpl-compatible: true + version: *fontawesome_version + css: + theme: + /libraries/fontawesome/css/all.css: { minified: true } + +fontawesome.webfonts.base: + version: *fontawesome_version + license: *fontawesome_webfonts_license + css: + theme: + /libraries/fontawesome/css/fontawesome.css: { minified: true } + +fontawesome.webfonts.solid: + version: *fontawesome_version + license: *fontawesome_webfonts_license + css: + theme: + /libraries/fontawesome/css/solid.css: { minified: true } + dependencies: + - fontawesome/fontawesome.webfonts.base + +fontawesome.webfonts.regular: + version: *fontawesome_version + license: *fontawesome_webfonts_license + css: + theme: + /libraries/fontawesome/css/regular.css: { minified: true } + dependencies: + - fontawesome/fontawesome.webfonts.base + +fontawesome.webfonts.light: + version: *fontawesome_version + license: *fontawesome_webfonts_license + css: + theme: + /libraries/fontawesome/css/light.css: { minified: true } + dependencies: + - fontawesome/fontawesome.webfonts.base + +fontawesome.webfonts.brands: + version: *fontawesome_version + license: *fontawesome_webfonts_license + css: + theme: + /libraries/fontawesome/css/brands.css: { minified: true } + dependencies: + - fontawesome/fontawesome.webfonts.base + +fontawesome.webfonts.shim: + version: *fontawesome_version + license: *fontawesome_webfonts_license + css: + theme: + /libraries/fontawesome/css/v4-shims.css: { minified: true } + dependencies: + - fontawesome/fontawesome.webfonts diff --git a/web/modules/fontawesome/fontawesome.links.menu.yml b/web/modules/fontawesome/fontawesome.links.menu.yml new file mode 100644 index 0000000..5d69684 --- /dev/null +++ b/web/modules/fontawesome/fontawesome.links.menu.yml @@ -0,0 +1,5 @@ +fontawesome.admin_settings: + title: 'Font Awesome Settings' + description: 'Global settings for the display of Font Awesome icons.' + route_name: fontawesome.admin_settings + parent: 'system.admin_config_content' diff --git a/web/modules/fontawesome/fontawesome.module b/web/modules/fontawesome/fontawesome.module new file mode 100644 index 0000000..56f7571 --- /dev/null +++ b/web/modules/fontawesome/fontawesome.module @@ -0,0 +1,547 @@ + ' . t('Font Awesome is an iconic font and CSS toolkit. Font Awesome gives you scalable vector icons that can instantly be customized — size, color, drop shadow, and anything that can be done with the power of CSS. For more information on how to use Font Awesome, see the Font Awesome Examples page.', [ + ':fontawesome_url' => 'https://fontawesome.com', + ':fontawesome_examples_page' => 'https://fontawesome.com/how-to-use/on-the-web/referencing-icons/basic-use', + ]) . '

'; + } +} + +/** + * Implements hook_library_info_alter(). + */ +function fontawesome_library_info_alter(&$libraries, $extension) { + // Modify the Font Awesome library to use external file if user chose. + if ($extension == 'fontawesome') { + // Load the configuration settings. + $configuration_settings = \Drupal::config('fontawesome.settings'); + + // Have to modify the library if the user is using a CDN. + if ($configuration_settings->get('use_cdn')) { + + // First check if we're using everything. + if (isset($libraries['fontawesome.' . $configuration_settings->get('method')])) { + _fontawesome_modify_library($libraries, NULL, $configuration_settings->get('method'), $configuration_settings->get('external_svg_location')); + } + + // Determine the base for the CDN. + $cdnComponents = parse_url($configuration_settings->get('external_svg_location')); + $cdnComponents['path'] = explode('/', $cdnComponents['path']); + unset($cdnComponents['path'][count($cdnComponents['path']) - 1]); + $cdnComponents['path'] = implode('/', $cdnComponents['path']) . '/'; + + if (isset($libraries['fontawesome.' . $configuration_settings->get('method') . '.base'])) { + // Modify settings for the base file. + $cdnBase = $cdnComponents; + $cdnBase['path'] .= 'fontawesome.' . ($configuration_settings->get('method') == 'webfonts' ? 'css' : 'js'); + _fontawesome_modify_library($libraries, 'base', $configuration_settings->get('method'), _fontawesome_unparse_url($cdnBase)); + } + + // Modify settings for individual included files. + foreach (['solid', 'regular', 'light', 'brands'] as $libraryType) { + if (isset($libraries['fontawesome.' . $configuration_settings->get('method') . '.' . $libraryType])) { + $cdnBase = $cdnComponents; + $cdnBase['path'] .= $libraryType . '.' . ($configuration_settings->get('method') == 'webfonts' ? 'css' : 'js'); + _fontawesome_modify_library($libraries, $libraryType, $configuration_settings->get('method'), _fontawesome_unparse_url($cdnBase)); + } + } + + // Modify the shim as well. + if (isset($libraries['fontawesome.' . $configuration_settings->get('method') . '.shim'])) { + _fontawesome_modify_library($libraries, 'shim', $configuration_settings->get('method'), $configuration_settings->get('external_shim_location')); + } + } + + // Allow pseudo-elements in JS if selected. + if ($configuration_settings->get('allow_pseudo_elements') && $configuration_settings->get('method') == 'svg') { + // Modify the libraries to add pseudo elements tag. + foreach ($libraries as $key => &$values) { + if (substr($key, 0, 15) == 'fontawesome.svg') { + $librarySettings = reset($values['js']); + $librarySource = key($values['js']); + // Font Awesome requires this script tag to enable pseudo elements. + $librarySettings['attributes'] = [ + 'data-search-pseudo-elements' => TRUE, + ]; + $values['js'][$librarySource] = $librarySettings; + } + } + } + } +} + +/** + * Modifies library inclusions to use CDN files when necessary. + * + * @param array $libraries + * The libraries inclusion array. + * @param string $librarySuffix + * The suffix of the library being modified. + * @param string $type + * The type of library we are modifying. + * @param string $cdnLocation + * The location of the CDN file being used. + */ +function _fontawesome_modify_library(array &$libraries, $librarySuffix, $type, $cdnLocation) { + // Determine the name of the library. + $libraryName = 'fontawesome.' . $type; + if (!empty($librarySuffix)) { + $libraryName .= '.' . $librarySuffix; + } + + // Handle SVG method. + if ($type == 'svg') { + $librarySettings = array_shift($libraries[$libraryName]['js']); + $libraries[$libraryName]['js'] = [ + $cdnLocation => $librarySettings, + ]; + } + // Handle WebFonts method. + elseif ($type == 'webfonts') { + $librarySettings = array_shift($libraries[$libraryName]['css']['theme']); + $libraries[$libraryName]['css']['theme'] = [ + $cdnLocation => $librarySettings, + ]; + } +} + +/** + * Unparses a CDN URL for use with individual Font Awesome file inclusions. + * + * @param array $parsed + * Array containing URL parsed data. + * + * @return string + * The unparsed URL for the CDN. + */ +function _fontawesome_unparse_url(array $parsed) { + $get = function ($key) use ($parsed) { + return isset($parsed[$key]) ? $parsed[$key] : NULL; + }; + + $pass = $get('pass'); + $user = $get('user'); + $userinfo = $pass !== NULL ? "$user:$pass" : $user; + $port = $get('port'); + $scheme = $get('scheme'); + $query = $get('query'); + $fragment = $get('fragment'); + $authority = ($userinfo !== NULL ? "$userinfo@" : '') . $get('host') . ($port ? ":$port" : ''); + + return (strlen($scheme) ? "$scheme:" : '') . (strlen($authority) ? "//$authority" : '') . $get('path') . (strlen($query) ? "?$query" : '') . (strlen($fragment) ? "#$fragment" : ''); +} + +/** + * Implements hook_ckeditor_css_alter(). + * + * This function allows for the proper functionality of the icons inside the + * CKEditor when using Webfonts with CSS as the Font Awesome display method. + * + * See fontawesome_editor_js_settings_alter() for allowing the use of the icons + * inside CKEditor when using the SVG with JS display method. + */ +function fontawesome_ckeditor_css_alter(&$css, $editor) { + // Attach the main library if we're using the CSS webfonts method.. + if (\Drupal::config('fontawesome.settings')->get('method') == 'webfonts') { + // Load the library. + $fontawesome_library = \Drupal::service('library.discovery')->getLibraryByName('fontawesome', 'fontawesome.webfonts'); + // Attach it's CSS. + $css[] = $fontawesome_library['css'][0]['data']; + + // Attach the shim CSS if needed. + if (Drupal::config('fontawesome.settings')->get('use_shim')) { + // Load the library. + $fontawesome_library_shim = \Drupal::service('library.discovery')->getLibraryByName('fontawesome', 'fontawesome.webfonts.shim'); + // Attach it's CSS. + $css[] = $fontawesome_library_shim['css'][0]['data']; + } + } +} + +/** + * Check to make sure that Font Awesome is installed. + * + * @return bool + * Flag indicating if the library is properly installed. + */ +function fontawesome_check_installed() { + // Load the configuration settings. + $configuration_settings = \Drupal::config('fontawesome.settings'); + + // Throw error if library file not found. + if ($configuration_settings->get('use_cdn')) { + return !empty($configuration_settings->get('external_svg_location')); + } + elseif ($configuration_settings->get('method') == 'webfonts') { + // Webfonts method. + $fontawesome_library = \Drupal::service('library.discovery')->getLibraryByName('fontawesome', 'fontawesome.webfonts'); + return file_exists(DRUPAL_ROOT . '/' . $fontawesome_library['css'][0]['data']); + } + else { + // SVG method. + $fontawesome_library = \Drupal::service('library.discovery')->getLibraryByName('fontawesome', 'fontawesome.svg'); + return file_exists(DRUPAL_ROOT . '/' . $fontawesome_library['js'][0]['data']); + } +} + +/** + * Implements hook_page_attachments(). + * + * Purposefully only load on page requests and not hook_init(). This is + * required so it does not increase the bootstrap time of Drupal when it isn't + * necessary. + */ +function fontawesome_page_attachments(array &$page) { + // Load the configuration settings. + $configuration_settings = \Drupal::config('fontawesome.settings'); + + // Throw error if library file not found. + if (!fontawesome_check_installed()) { + \Drupal::messenger()->addWarning(t('The Font Awesome library could not be found. Please verify Font Awesome is installed correctly or that the CDN has been activated and properly configured. Please see the @adminPage and the Font Awesome module README file for more details.', [ + '@adminPage' => Link::createFromRoute(t('admin page'), 'fontawesome.admin_settings')->toString(), + ])); + return; + } + + // First check if we're using everything. + if (is_null($configuration_settings->get('use_solid_file')) === TRUE || ($configuration_settings->get('use_solid_file') && $configuration_settings->get('use_regular_file') && $configuration_settings->get('use_light_file') && $configuration_settings->get('use_brands_file'))) { + // Attach the main library. + $page['#attached']['library'][] = 'fontawesome/fontawesome.' . $configuration_settings->get('method'); + } + // Else we attach the libraries piecemeal. + else { + if ($configuration_settings->get('use_solid_file')) { + $page['#attached']['library'][] = 'fontawesome/fontawesome.' . $configuration_settings->get('method') . '.solid'; + } + if ($configuration_settings->get('use_regular_file')) { + $page['#attached']['library'][] = 'fontawesome/fontawesome.' . $configuration_settings->get('method') . '.regular'; + } + if ($configuration_settings->get('use_light_file')) { + $page['#attached']['library'][] = 'fontawesome/fontawesome.' . $configuration_settings->get('method') . '.light'; + } + if ($configuration_settings->get('use_brands_file')) { + $page['#attached']['library'][] = 'fontawesome/fontawesome.' . $configuration_settings->get('method') . '.brands'; + } + } + + // Attach the shim file if needed. + if ($configuration_settings->get('use_shim')) { + $page['#attached']['library'][] = 'fontawesome/fontawesome.' . $configuration_settings->get('method') . '.shim'; + } +} + +/** + * Helper function returns the prefix for an icon based on icon type. + * + * @param array $styles + * An array of valid styles for the icon. + * @param string $default + * The value to assign here if it's not a brand icon. + * + * @return string + * A valid prefix for this icon. + */ +function fontawesome_determine_prefix(array $styles, $default = 'fas') { + // Determine the icon style - brands behave differently. + foreach ($styles as $style) { + if ($style == 'brands') { + return 'fab'; + } + } + return $default; +} + +/** + * Implements hook_theme(). + */ +function fontawesome_theme($existing, $type, $theme, $path) { + return [ + 'fontawesomeicons' => [ + 'variables' => [ + 'icons' => NULL, + 'layers' => FALSE, + ], + ], + 'fontawesomeicon' => [ + 'variables' => [ + 'tag' => 'i', + 'name' => NULL, + 'style' => NULL, + 'settings' => NULL, + 'transforms' => NULL, + 'mask' => NULL, + 'css' => NULL, + ], + ], + ]; +} + +/** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function fontawesome_theme_suggestions_fontawesomeicon(array $variables) { + // Suggest a template with the icon name if it exists. + if (!empty($variables['name'])) { + $suggestions[] = $variables['theme_hook_original'] . '__' . $variables['name']; + } + return $suggestions; +} + +/** + * Implements hook_theme_registry_alter(). + */ +function fontawesome_theme_registry_alter(&$theme_registry) { + /* + * By default, Drupal 8 does not include theme suggestions from inside the + * module in which they were created, so we must add them manually here. + */ + $path = drupal_get_path('module', 'fontawesome'); + $fontawesome_templates = drupal_find_theme_templates($theme_registry, '.html.twig', $path); + foreach ($fontawesome_templates as &$fontawesome_template) { + $fontawesome_template['type'] = 'module'; + } + $theme_registry += $fontawesome_templates; +} + +/** + * Implements hook_icon_providers(). + */ +function fontawesome_icon_providers() { + $providers['fontawesome'] = [ + 'title' => 'Font Awesome', + 'url' => 'http://fontawesome.io', + ]; + return $providers; +} + +/** + * Implements hook_icon_bundle_configure(). + */ +function fontawesome_icon_bundle_configure(&$settings, &$form_state, &$complete_form) { + $bundle = $form_state['bundle']; + if ($bundle['provider'] === 'fontawesome') { + $settings['tag'] = [ + '#type' => 'select', + '#title' => t('HTML Markup'), + '#description' => t('Choose the HTML markup tag that Font Awesome icons should be created with. Typically, this is a %tag tag, however it can be changed to suite the theme requirements.', [ + '%tag' => '<' . $bundle['settings']['tag'] . '>', + ]), + '#options' => array_combine( + ['i', 'span'], + ['i', 'span'] + ), + '#default_value' => $bundle['settings']['tag'], + ]; + } +} + +/** + * Implements hook_preprocess_icon_RENDER_HOOK(). + */ +function fontawesome_preprocess_icon_sprite(&$variables) { + $bundle = &$variables['bundle']; + if ($bundle['provider'] === 'fontawesome') { + // Remove the default "icon" class. + $key = array_search('icon', $variables['attributes']['class']); + if ($key !== FALSE) { + unset($variables['attributes']['class'][$key]); + } + + // TODO: need to add the correct class depending on icon type. + // Add the necessary FA identifier class. + $variables['attributes']['class'][] = 'fas'; + + // Prepend the icon with the FA prefix (which will be used as the class). + $variables['icon'] = 'fa-' . $variables['icon']; + } +} + +/** + * Implements hook_icon_bundles(). + * + * TODO: this is waiting on an 8.x release of Icon API. + */ +function fontawesome_icon_bundles() { + $bundles['fontawesome'] = [ + 'title' => 'Font Awesome', + 'provider' => 'fontawesome', + 'render' => 'sprite', + 'settings' => [ + 'tag' => 'i', + ], + 'icons' => fontawesome_extract_icons(), + ]; + return $bundles; +} + +/** + * Loads the Font Awesome metadata file. + * + * @return string + * The filepath of the metadata file. + */ +function fontawesome_get_metadata_filepath() { + // Attempt to load the icons from the local library's metadata if possible. + $metadataFile = \Drupal::service('file_system')->realpath(DRUPAL_ROOT . '/libraries/fontawesome/metadata/icons.yml'); + // If we can't load the local file, use the included module icons file. + if (!file_exists($metadataFile)) { + $metadataFile = drupal_get_path('module', 'fontawesome') . '/metadata/icons.yml'; + } + return $metadataFile; +} + +/** + * Provides a list of all available Font Awesome icons from metadata. + * + * @return array + * Array containing icons. + */ +function fontawesome_extract_icons() { + // Check for cached icons. + if (!$icons = \Drupal::cache('data')->get('fontawesome.iconlist')) { + + // Parse the metadata file and use it to generate the icon list. + $icons = []; + foreach (Yaml::parse(file_get_contents(fontawesome_get_metadata_filepath())) as $name => $icon) { + // Determine the icon type - brands behave differently. + $type = 'solid'; + foreach ($icon['styles'] as $style) { + if ($style == 'brands') { + $type = 'brands'; + break; + } + } + $icons[$name] = [ + 'name' => $name, + 'type' => $type, + 'label' => $icon['label'], + 'styles' => $icon['styles'], + ]; + } + + // Cache the icons array. + \Drupal::cache('data')->set('fontawesome.iconlist', $icons, strtotime('+1 week'), ['fontawesome', 'iconlist']); + } + else { + $icons = $icons->data; + } + + return (array) $icons; +} + +/** + * Extract metadata for a specific icon. + * + * @param string $findIcon + * The icon for which we want metadata. + * + * @return array + * Array containing icons. + */ +function fontawesome_extract_icon_metadata($findIcon) { + // Parse the metadata file and use it to generate the icon list. + foreach (Yaml::parse(file_get_contents(fontawesome_get_metadata_filepath())) as $name => $icon) { + if ($name == $findIcon) { + // Determine the icon type - brands behave differently. + $type = 'solid'; + foreach ($icon['styles'] as $style) { + if ($style == 'brands') { + $type = 'brands'; + break; + } + } + return [ + 'name' => $name, + 'type' => $type, + 'label' => $icon['label'], + 'styles' => $icon['styles'], + ]; + } + } + + return FALSE; +} + +/** + * Implements hook_entity_presave(). + */ +function fontawesome_entity_presave(EntityInterface $entity) { + if ($entity instanceof ContentEntityInterface) { + // Loop over the fields. + foreach ($entity->getFields() as $fields) { + if ($fields instanceof ItemList) { + // If this is a text field (uses an editor). + if (in_array($fields->getFieldDefinition()->getType(), [ + 'text', + 'text_long', + 'text_with_summary', + ])) { + foreach ($fields as $field) { + // Find and replace SVG strings with original icon HTML. + $fieldValue = $field->getValue(); + $fieldValue['value'] = preg_replace('%/g, '$1'); + // Set the body to the new value. + thisEditor.editor.setData(htmlBody); + }; + + // After CKEditor is ready. + CKEDITOR.on( + 'instanceReady', + (ev) => { + // On initial load, convert icons to SVGs. + Drupal.FontAwesome.tagsToSvg(drupalSettings, ev); + + // On mode change, deal with the changes on the fly. + ev.editor.on('mode', () => { + if (ev.editor.mode === 'source') { + // If we are showing source, turn SVG back to original tags. + Drupal.FontAwesome.svgToTags(ev); + } + else if (ev.editor.mode === 'wysiwyg') { + // If switching back to the display mode, have to load SVGs again. + Drupal.FontAwesome.tagsToSvg(drupalSettings, ev); + } + }); + + // Listen to the event for inserting icons from the plugin. + ev.editor.on('insertedIcon', () => { + // todo: For some reason this throws an 'Uncaught TypeError'. + // Force an update to the content. + ev.editor.setData(ev.editor.getData()); + // Then reload the SVGs. + Drupal.FontAwesome.tagsToSvg(drupalSettings, ev); + }); + }, + ); +})(jQuery, Drupal, drupalSettings, CKEDITOR); diff --git a/web/modules/fontawesome/js/plugins/drupalfontawesome/plugin.js b/web/modules/fontawesome/js/plugins/drupalfontawesome/plugin.js new file mode 100644 index 0000000..8dc78fb --- /dev/null +++ b/web/modules/fontawesome/js/plugins/drupalfontawesome/plugin.js @@ -0,0 +1,105 @@ +/** +* DO NOT EDIT THIS FILE. +* See the following change record for more information, +* https://www.drupal.org/node/2815083 +* @preserve +**/ + +(function ($, Drupal, drupalSettings, CKEDITOR) { + 'use strict'; + + CKEDITOR.plugins.add('drupalfontawesome', { + icons: 'drupalfontawesome', + hidpi: true, + + init: function init(editor) { + editor.addCommand('drupalfontawesome', { + modes: { wysiwyg: 1 }, + canUndo: true, + exec: function exec(execEditor) { + var saveCallback = function saveCallback(returnValues) { + execEditor.fire('saveSnapshot'); + + var selection = execEditor.getSelection(); + var range = selection.getRanges(1)[0]; + + var container = new CKEDITOR.dom.element('span', execEditor.document); + container.addClass('fontawesome-icon-inline'); + var icon = new CKEDITOR.dom.element(returnValues.tag, execEditor.document); + icon.setAttributes(returnValues.attributes); + container.append(icon); + container.appendHtml(' '); + + range.insertNode(container); + range.select(); + + execEditor.fire('saveSnapshot'); + + execEditor.fire('insertedIcon'); + }; + + var dialogSettings = { + title: execEditor.config.drupalFontAwesome_dialogTitleAdd, + dialogClass: 'fontawesome-icon-dialog' + }; + + Drupal.ckeditor.openDialog(execEditor, Drupal.url('fontawesome/dialog/icon/' + execEditor.config.drupal.format), {}, saveCallback, dialogSettings); + } + }); + + if (editor.ui.addButton) { + editor.ui.addButton('DrupalFontAwesome', { + label: Drupal.t('Font Awesome'), + command: 'drupalfontawesome' + }); + } + } + }); + + if ('editor' in drupalSettings && 'fontawesome' in drupalSettings.editor) { + $.each(drupalSettings.editor.fontawesome.allowedEmptyTags, function (_, tag) { + CKEDITOR.dtd.$removeEmpty[tag] = 0; + }); + } + + Drupal.FontAwesome = {}; + + Drupal.FontAwesome.tagsToSvg = function (drupalSettings, thisEditor) { + if ('editor' in drupalSettings && 'fontawesome' in drupalSettings.editor) { + $.each(drupalSettings.editor.fontawesome.fontawesomeLibraries, function (_, library) { + var $script = document.createElement('script'); + var $editorInstance = CKEDITOR.instances[thisEditor.editor.name]; + + $script.src = library; + + $editorInstance.document.getHead().$.appendChild($script); + }); + } + }; + + Drupal.FontAwesome.svgToTags = function (thisEditor) { + var htmlBody = thisEditor.editor.getData(); + + htmlBody = htmlBody.replace(/ $this->t('Title'), + ]; + } + + /** + * {@inheritdoc} + */ + public function getMetadata(MediaInterface $media, $attribute_name) { + /** @var \Drupal\fontawesome\Plugin\Field\FieldType\FontAwesomeIcon $icon */ + $icon = $media + ->get($this->configuration['source_field']) + ->first(); + + // If the source field is not required, it may be empty. + if (!$icon) { + return parent::getMetadata($media, $attribute_name); + } + switch ($attribute_name) { + case 'default_name': + return $icon + ->get('icon_name') + ->getValue(); + + case 'thumbnail_uri': + return $this->getThumbnail($icon); + + default: + return parent::getMetadata($media, $attribute_name); + } + } + + /** + * Gets the thumbnail image URI based on an icon entity. + * + * @param \Drupal\fontawesome\Plugin\Field\FieldType\FontAwesomeIcon $icon + * A Font Awesome Iocn entity. + * + * @return string + * File URI of the thumbnail image or NULL if there is no specific icon. + */ + protected function getThumbnail(FontAwesomeIcon $icon) { + + // Determine the source folder. + switch ($icon->get('style')->getCastedValue()) { + case 'fab': + $srcFolder = 'brands'; + break; + + case 'fal': + $srcFolder = 'light'; + break; + + case 'fas': + $srcFolder = 'solid'; + break; + + case 'far': + default: + $srcFolder = 'regular'; + break; + + case 'fad': + $srcFolder = 'duotone'; + break; + } + + return 'libraries/fontawesome/svgs/' . $srcFolder . '/' . $icon + ->get('icon_name') + ->getValue() . '.svg'; + } + +} diff --git a/web/modules/fontawesome/src/Commands/FontawesomeCommands.php b/web/modules/fontawesome/src/Commands/FontawesomeCommands.php new file mode 100644 index 0000000..1b279cb --- /dev/null +++ b/web/modules/fontawesome/src/Commands/FontawesomeCommands.php @@ -0,0 +1,105 @@ +mkdir($path); + } + if (is_dir($path . '/css')) { + $this->logger()->notice(dt('Font Awesome already present at @path. No download required.', ['@path' => $path])); + return; + } + + // Load the Font Awesome defined library. + if ($fontawesome_library = \Drupal::service('library.discovery')->getLibraryByName('fontawesome', 'fontawesome.svg')) { + + // Download the file. + $client = new Client(); + $destination = tempnam(sys_get_temp_dir(), 'file.') . "tar.gz"; + try { + $client->get($fontawesome_library['remote'], ['save_to' => $destination]); + } + catch (RequestException $e) { + // Remove the directory. + $fs->remove($path); + $this->logger()->error(dt('Drush was unable to download the Font Awesome library from @remote. @exception', [ + '@remote' => $fontawesome_library['remote'], + '@exception' => $e->getMessage(), + ], 'error')); + return; + } + $fs->rename($destination, $path . '/fontawesome.zip'); + if (!file_exists($path . '/fontawesome.zip')) { + // Remove the directory where we tried to install. + $fs->remove($path); + $this->logger()->error(dt('Error: unable to download Fontawesome library from @remote', [ + '@remote' => $fontawesome_library['remote'], + ], 'error')); + return; + } + + // Unzip the file. + $zip = new \ZipArchive(); + $res = $zip->open($path . '/fontawesome.zip'); + if ($res === TRUE) { + $zip->extractTo($path); + $zip->close(); + } + else { + // Remove the directory. + $fs->remove($path); + $this->logger()->error(dt('Error: unable to unzip Fontawesome file.', [], 'error')); + return; + } + + // Remove the downloaded zip file. + $fs->remove($path . '/fontawesome.zip'); + + // Move the file. + $fs->mirror($path . '/fontawesome-free-' . $fontawesome_library['version'] . '-web', $path, NULL, ['override' => TRUE]); + $fs->remove($path . '/fontawesome-free-' . $fontawesome_library['version'] . '-web'); + + // Success. + $this->logger()->notice(dt('Fontawesome library has been successfully downloaded to @path.', [ + '@path' => $path, + ], 'success')); + } + else { + $this->logger()->error(dt('Drush was unable to load the Font Awesome library')); + } + } + +} diff --git a/web/modules/fontawesome/src/Controller/AutocompleteController.php b/web/modules/fontawesome/src/Controller/AutocompleteController.php new file mode 100644 index 0000000..664d96e --- /dev/null +++ b/web/modules/fontawesome/src/Controller/AutocompleteController.php @@ -0,0 +1,80 @@ +query->get('q')) { + $typed_string = Tags::explode($input); + $typed_string = mb_strtolower(array_pop($typed_string)); + + // Load the icon data so we can check for a valid icon. + $iconData = fontawesome_extract_icons(); + + // Check each icon to see if it starts with the typed string. + foreach ($iconData as $icon => $data) { + // If the string is found. + if (strpos($icon, $typed_string) === 0) { + $iconRenders = []; + // Loop over each style. + foreach ($iconData[$icon]['styles'] as $style) { + + // Determine the prefix. + switch ($style) { + + case 'brands': + $iconPrefix = 'fab'; + break; + + case 'light': + $iconPrefix = 'fal'; + break; + + case 'regular': + $iconPrefix = 'far'; + break; + + case 'duotone': + $iconPrefix = 'fad'; + break; + + default: + case 'solid': + $iconPrefix = 'fas'; + break; + } + // Render the icon. + $iconRenders[] = new FormattableMarkup(' ', [ + ':prefix' => $iconPrefix, + ':icon' => $icon, + ]); + } + + $results[] = [ + 'value' => $icon, + 'label' => implode('', $iconRenders) . $icon, + ]; + } + } + } + + return new JsonResponse($results); + } + +} diff --git a/web/modules/fontawesome/src/Form/EditorIconDialog.php b/web/modules/fontawesome/src/Form/EditorIconDialog.php new file mode 100644 index 0000000..8f8fff6 --- /dev/null +++ b/web/modules/fontawesome/src/Form/EditorIconDialog.php @@ -0,0 +1,463 @@ +configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'fontawesome_icon_dialog'; + } + + /** + * {@inheritdoc} + * + * @param \Drupal\editor\Entity\Editor $editor + * The text editor to which this dialog corresponds. + */ + public function buildForm(array $form, FormStateInterface $form_state, Editor $editor = NULL) { + // Load the configuration settings. + $configuration_settings = $this->configFactory->get('fontawesome.settings'); + + $form['#tree'] = TRUE; + $form['#attached']['library'][] = 'editor/drupal.editor.dialog'; + + $form['#prefix'] = '
'; + $form['#suffix'] = '
'; + + $form['information'] = [ + '#type' => 'container', + '#attributes' => [], + '#children' => $this->t('For more information on icon selection, see @iconLink. If an icon below is displayed with a question mark, it is likely a Font Awesome Pro icon, unavailable with the free version of Font Awesome.', [ + '@iconLink' => Link::fromTextAndUrl($this->t('the Font Awesome icon list'), Url::fromUri('https://fontawesome.com/icons'))->toString(), + ]), + ]; + + $form['icon_name'] = [ + '#type' => 'textfield', + '#title' => $this->t('Icon Name'), + '#size' => 50, + '#field_prefix' => 'fa-', + '#default_value' => '', + '#description' => $this->t('Name of the Font Awesome Icon. See @iconsLink for valid icon names, or begin typing for an autocomplete list.', [ + '@iconsLink' => Link::fromTextAndUrl($this->t('the Font Awesome icon list'), Url::fromUri('https://fontawesome.com/icons'))->toString(), + ]), + '#autocomplete_route_name' => 'fontawesome.autocomplete', + '#element_validate' => [ + [static::class, 'validateIconName'], + ], + ]; + + // Build additional settings. + $form['settings'] = [ + '#type' => 'details', + '#open' => FALSE, + '#title' => $this->t('Additional Font Awesome Settings'), + ]; + // Allow user to determine size. + $form['settings']['style'] = [ + '#type' => 'select', + '#title' => $this->t('Style'), + '#description' => $this->t('This changes the style of the icon. Please note that this is not available for all icons, and for some of the icons this is only available in the pro version. If the icon does not render properly in the preview above, the icon does not support that style. Notably, brands do not support any styles. See @iconLink for more information.', [ + '@iconLink' => Link::fromTextAndUrl($this->t('the Font Awesome icon list'), Url::fromUri('https://fontawesome.com/icons'))->toString(), + ]), + '#options' => [ + 'fas' => $this->t('Solid'), + 'far' => $this->t('Regular'), + 'fal' => $this->t('Light'), + 'fad' => $this->t('Duotone'), + ], + '#default_value' => 'fas', + ]; + // Allow user to determine size. + $form['settings']['size'] = [ + '#type' => 'select', + '#title' => $this->t('Size'), + '#description' => $this->t('This increases icon sizes relative to their container'), + '#options' => [ + '' => $this->t('Default'), + 'fa-xs' => $this->t('Extra Small'), + 'fa-sm' => $this->t('Small'), + 'fa-lg' => $this->t('Large'), + 'fa-2x' => $this->t('2x'), + 'fa-3x' => $this->t('3x'), + 'fa-4x' => $this->t('4x'), + 'fa-5x' => $this->t('5x'), + 'fa-6x' => $this->t('6x'), + 'fa-7x' => $this->t('7x'), + 'fa-8x' => $this->t('8x'), + 'fa-9x' => $this->t('9x'), + 'fa-10x' => $this->t('10x'), + ], + '#default_value' => '', + ]; + // Set icon to fixed width. + $form['settings']['fixed-width'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Fixed Width?'), + '#description' => $this->t('Use to set icons at a fixed width. Great to use when different icon widths throw off vertical alignment. Especially useful in things like nav lists and list groups.'), + '#default_value' => FALSE, + '#return_value' => 'fa-fw', + ]; + // Add border. + $form['settings']['border'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Border?'), + '#description' => $this->t('Adds a border to the icon.'), + '#default_value' => FALSE, + '#return_value' => 'fa-border', + ]; + // Invert color. + $form['settings']['invert'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Invert color?'), + '#description' => $this->t('Inverts the color of the icon (black becomes white, etc.)'), + '#default_value' => FALSE, + '#return_value' => 'fa-inverse', + ]; + // Animated the icon. + $form['settings']['animation'] = [ + '#type' => 'select', + '#title' => $this->t('Animation'), + '#description' => $this->t('Use spin to get any icon to rotate, and pulse to have it rotate with 8 steps. Works especially well with fa-spinner & everything in the @iconLink.', [ + '@iconLink' => Link::fromTextAndUrl($this->t('spinner icons category'), Url::fromUri('https://fontawesome.com/icons?d=gallery&c=spinners'))->toString(), + ]), + '#options' => [ + '' => $this->t('None'), + 'fa-spin' => $this->t('Spin'), + 'fa-pulse' => $this->t('Pulse'), + ], + '#default_value' => '', + ]; + + // Pull the icons. + $form['settings']['pull'] = [ + '#type' => 'select', + '#title' => $this->t('Pull'), + '#description' => $this->t('This setting will pull the icon (float) to one side or the other in relation to its nearby content'), + '#options' => [ + '' => $this->t('None'), + 'fa-pull-left' => $this->t('Left'), + 'fa-pull-right' => $this->t('Right'), + ], + '#default_value' => '', + ]; + + // Build new power-transforms. + $form['settings']['power_transforms'] = [ + '#type' => 'details', + '#open' => FALSE, + '#disabled' => $configuration_settings->get('method') == 'webfonts', + '#title' => $this->t('Power Transforms'), + '#description' => $this->t('See @iconLink for additional information on Power Transforms. Note that these transforms only work with the SVG with JS version of Font Awesome and are disabled for Webfonts. See the @adminLink to set your version of Font Awesome.', [ + '@iconLink' => Link::fromTextAndUrl($this->t('the Font Awesome `Power Transforms` guide'), Url::fromUri('https://fontawesome.com/how-to-use/on-the-web/styling/power-transforms'))->toString(), + '@adminLink' => Link::createFromRoute($this->t('admin page'), 'fontawesome.admin_settings')->toString(), + ]), + ]; + // Rotate the icon. + $form['settings']['power_transforms']['rotate']['value'] = [ + '#type' => 'number', + '#title' => $this->t('Rotate'), + '#field_suffix' => '°', + '#default_value' => '', + '#description' => $this->t('Power Transform rotating effects icon angle without changing or moving the container. To rotate icons use any arbitrary value. Units are degrees with negative numbers allowed.'), + ]; + // Flip the icon. + $form['settings']['power_transforms']['flip-h']['value'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Flip Horizontal?'), + '#default_value' => FALSE, + '#description' => $this->t('Power Transform flipping effects icon reflection without changing or moving the container.'), + '#return_value' => 'h', + ]; + $form['settings']['power_transforms']['flip-v']['value'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Flip Vertical?'), + '#default_value' => FALSE, + '#description' => $this->t('Power Transform flipping effects icon reflection without changing or moving the container.'), + '#return_value' => 'v', + ]; + // Scale the icon. + $form['settings']['power_transforms']['scale'] = [ + '#type' => 'details', + '#open' => TRUE, + '#title' => $this->t('Scale'), + '#description' => $this->t('Power Transform scaling effects icon size without changing or moving the container. This field will scale icons up or down with any arbitrary value, including decimals. Units are 1/16em.'), + ]; + $form['settings']['power_transforms']['scale']['type'] = [ + '#type' => 'select', + '#title' => $this->t('Scale Type'), + '#options' => [ + '' => $this->t('None'), + 'shrink' => $this->t('Shrink'), + 'grow' => $this->t('Grow'), + ], + '#default_value' => '', + '#element_validate' => [ + [static::class, 'validatePowerTransforms'], + ], + ]; + $form['settings']['power_transforms']['scale']['value'] = [ + '#type' => 'number', + '#title' => $this->t('Scale Value'), + '#min' => 0, + '#default_value' => '', + '#states' => [ + 'disabled' => [ + ':input[name="settings[power_transforms][scale][type]"]' => ['value' => ''], + ], + ], + ]; + // Position the icon. + $form['settings']['power_transforms']['position_y'] = [ + '#type' => 'details', + '#open' => TRUE, + '#title' => $this->t('Position (Y Axis)'), + '#description' => $this->t('Power Transform positioning effects icon location without changing or moving the container. This field will move icons up or down with any arbitrary value, including decimals. Units are 1/16em.'), + ]; + $form['settings']['power_transforms']['position_y']['type'] = [ + '#type' => 'select', + '#title' => $this->t('Position Type'), + '#options' => [ + '' => $this->t('None'), + 'up' => $this->t('Up'), + 'down' => $this->t('Down'), + ], + '#default_value' => '', + '#element_validate' => [ + [static::class, 'validatePowerTransforms'], + ], + ]; + $form['settings']['power_transforms']['position_y']['value'] = [ + '#type' => 'number', + '#title' => $this->t('Position Value'), + '#min' => 0, + '#default_value' => '', + '#states' => [ + 'disabled' => [ + ':input[name="settings[power_transforms][position_y][type]"]' => ['value' => ''], + ], + ], + ]; + $form['settings']['power_transforms']['position_x'] = [ + '#type' => 'details', + '#open' => TRUE, + '#title' => $this->t('Position (X Axis)'), + '#description' => $this->t('Power Transform positioning effects icon location without changing or moving the container. This field will move icons up or down with any arbitrary value, including decimals. Units are 1/16em.'), + ]; + $form['settings']['power_transforms']['position_x']['type'] = [ + '#type' => 'select', + '#title' => $this->t('Position Type'), + '#options' => [ + '' => $this->t('None'), + 'left' => $this->t('Left'), + 'right' => $this->t('Right'), + ], + '#default_value' => '', + '#element_validate' => [ + [static::class, 'validatePowerTransforms'], + ], + ]; + $form['settings']['power_transforms']['position_x']['value'] = [ + '#type' => 'number', + '#title' => $this->t('Position Value'), + '#min' => 0, + '#default_value' => '', + '#states' => [ + 'disabled' => [ + ':input[name="settings[power_transforms][position_x][type]"]' => ['value' => ''], + ], + ], + ]; + + $form['actions'] = [ + '#type' => 'actions', + ]; + $form['actions']['save_modal'] = [ + '#type' => 'submit', + '#value' => $this->t('Insert Icon'), + // No regular submit-handler. This form only works via JavaScript. + '#submit' => [], + '#ajax' => [ + 'callback' => '::submitForm', + 'event' => 'click', + ], + ]; + + return $form; + } + + /** + * Validate the Font Awesome power transforms. + */ + public static function validatePowerTransforms($element, FormStateInterface $form_state) { + $value = $element['#value']; + if (strlen($value) == 0) { + $form_state->setValueForElement($element, ''); + return; + } + + // Check the value of the power transform. + $transformSettings = $form_state->getValues(); + foreach (array_slice($element['#parents'], 0, 3) as $key) { + $transformSettings = $transformSettings[$key]; + } + + if (!is_numeric($transformSettings['value'])) { + $form_state->setError($element, t("Invalid value for Font Awesome Power Transform %value. Please see @iconLink for information on correct values.", [ + '%value' => $value, + '@iconLink' => Link::fromTextAndUrl(t('the Font Awesome guide to Power Transforms'), Url::fromUri('https://fontawesome.com/how-to-use/on-the-web/styling/power-transforms'))->toString(), + ])); + } + } + + /** + * Validate the Font Awesome icon name. + */ + public static function validateIconName($element, FormStateInterface $form_state) { + $value = $element['#value']; + if (strlen($value) == 0) { + $form_state->setValueForElement($element, ''); + return; + } + + // Remove the prefix if the user accidentally added it. + if (substr($value, 0, 3) == 'fa-') { + $value = substr($value, 3); + } + + // Load the icon data so we can check for a valid icon. + $iconData = fontawesome_extract_icon_metadata($value); + + if (!isset($iconData['name'])) { + $form_state->setError($element, t("Invalid icon name %value. Please see @iconLink for correct icon names.", [ + '%value' => $value, + '@iconLink' => Link::fromTextAndUrl(t('the Font Awesome icon list'), Url::fromUri('https://fontawesome.com/icons'))->toString(), + ])); + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + + if ($form_state->getErrors()) { + unset($form['#prefix'], $form['#suffix']); + $form['status_messages'] = [ + '#type' => 'status_messages', + '#weight' => -10, + ]; + $response->addCommand(new HtmlCommand('#fontawesome-icon-dialog-form', $form)); + } + else { + $item = $form_state->getValues(); + + // Remove the prefix if the user accidentally added it. + if (substr($item['icon_name'], 0, 3) == 'fa-') { + $item['icon_name'] = substr($item['icon_name'], 3); + } + + // Massage rotate and flip values to make them format properly. + if (is_numeric($item['settings']['power_transforms']['rotate']['value'])) { + $item['settings']['power_transforms']['rotate']['type'] = 'rotate'; + } + else { + unset($item['settings']['power_transforms']['rotate']); + } + if (!empty($item['settings']['power_transforms']['flip-h']['value'])) { + $item['settings']['power_transforms']['flip-h']['type'] = 'flip'; + } + else { + unset($item['settings']['power_transforms']['flip-h']); + } + if (!empty($item['settings']['power_transforms']['flip-v']['value'])) { + $item['settings']['power_transforms']['flip-v']['type'] = 'flip'; + } + else { + unset($item['settings']['power_transforms']['flip-v']); + } + // Determine the icon style - brands don't allow style. + $metadata = fontawesome_extract_icon_metadata($item['icon_name']); + $item['style'] = fontawesome_determine_prefix($metadata['styles'], $item['settings']['style']); + unset($item['settings']['style']); + + // Remove blank data. + $item['settings'] = array_filter($item['settings']); + + // Get power transforms. + $item['power_transforms'] = []; + foreach ($item['settings']['power_transforms'] as $transform) { + if (!empty($transform['type'])) { + $item['power_transforms'][] = $transform['type'] . '-' . $transform['value']; + } + } + unset($item['settings']['power_transforms']); + + // Set the icon attributes. + $icon_attributes = [ + 'attributes' => [ + 'class' => [ + trim($item['style'] . ' fa-' . $item['icon_name'] . ' ' . implode(' ', $item['settings'])), + ], + ], + ]; + // If there are power transforms, add them. + if (count($item['power_transforms']) > 0) { + $icon_attributes['attributes']['data-fa-transform'] = [implode(' ', $item['power_transforms'])]; + } + + // Load the configuration settings. + $configuration_settings = $this->configFactory->get('fontawesome.settings'); + + // Set the user-selected tag type being used. + $icon_attributes['tag'] = empty($configuration_settings->get('tag')) ? 'i' : $configuration_settings->get('tag'); + + $response->addCommand(new EditorDialogSave($icon_attributes)); + $response->addCommand(new CloseModalDialogCommand()); + } + + return $response; + } + +} diff --git a/web/modules/fontawesome/src/Form/SettingsForm.php b/web/modules/fontawesome/src/Form/SettingsForm.php new file mode 100644 index 0000000..afbeda2 --- /dev/null +++ b/web/modules/fontawesome/src/Form/SettingsForm.php @@ -0,0 +1,258 @@ +libraryDiscovery = $library_discovery; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('library.discovery') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'fontawesome_admin_settings_form'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return [ + 'fontawesome.settings', + ]; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + // Get current settings. + $fontawesome_config = $this->config('fontawesome.settings'); + + // Load the fontawesome libraries so we can use its definitions here. + $fontawesome_library = $this->libraryDiscovery->getLibraryByName('fontawesome', 'fontawesome.svg'); + + $form['tag'] = [ + '#type' => 'select', + '#title' => $this->t('Font Awesome Tag'), + '#options' => [ + 'i' => $this->t('<i>'), + 'span' => $this->t('<span>'), + ], + '#default_value' => empty($fontawesome_config->get('tag')) ? 'i' : $fontawesome_config->get('tag'), + '#description' => $this->t('Font Awesome works with any consistent HTML element. By default, Font Awesome uses the <i> tag for its icons. However, in some cases you may want to use a different tag for your Font Awesome icons, such as a <span> tag. Changing the value here will change the way the tags are inserted into your site. Manually created Font Awesome tags can use any HTML element you like. Note that changing this setting will require clearing the site cache.'), + ]; + + $form['method'] = [ + '#type' => 'select', + '#title' => $this->t('Font Awesome Method'), + '#options' => [ + 'svg' => $this->t('SVG with JS'), + 'webfonts' => $this->t('Web Fonts with CSS'), + ], + '#default_value' => $fontawesome_config->get('method'), + '#description' => $this->t('This setting controls the way Font Awesome works. SVG with JS is the modern, easy, and powerful version with the most backwards compatibility. Web Fonts with CSS is the classic Font Awesome icon method that you have seen in earlier versions of Font Awesome. We recommend SVG with JS. Please note that the Webfonts with CSS version does not allow backwards compatibility with Font Awesome 4. That means you will need to check your code base to be certain that the icons are all updated to work with version 5. See @gettingStartedLink for more information.', [ + '@gettingStartedLink' => Link::fromTextAndUrl($this->t('the Font Awesome guide'), Url::fromUri('https://fontawesome.com/start'))->toString(), + ]), + ]; + + $form['allow_pseudo_elements'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Allow CSS pseudo-elements?'), + '#description' => $this->t('If you do not want to add icons directly in code, you can add them through CSS pseudo-elements. Font Awesome has leveraged the ::before pseudo-element to add icons to a page since the very beginning. For more information on how to use pseudo-elements, see the @pseudoElementsLink. Note that this feature is always available with the Webfonts version of Font Awesome. If you turn this feature on for SVG with JS, it will slow your site down noticeably.', [ + '@pseudoElementsLink' => Link::fromTextAndUrl($this->t('Font Awesome guide to pseudo-elements'), Url::fromUri('https://fontawesome.com/how-to-use/on-the-web/advanced/css-pseudo-elements'))->toString(), + ]), + '#default_value' => $fontawesome_config->get('allow_pseudo_elements'), + ]; + + $form['external'] = [ + '#type' => 'details', + '#open' => TRUE, + '#title' => $this->t('External file configuration'), + '#description' => $this->t('These settings control the method by which the Font Awesome library is loaded. You can choose to use an external (full URL) or local (relative path) library by selecting a URL / path below, or you can use a local version of the file by leaving the box unchecked and downloading the library :remoteurl and installing locally at %installpath. See the README for more information.', [ + ':remoteurl' => $fontawesome_library['remote'], + '%installpath' => '/libraries', + ]), + 'use_cdn' => [ + '#type' => 'checkbox', + '#title' => $this->t('Use external file (CDN) / local file?'), + '#description' => $this->t('Checking this box will cause the Font Awesome library to be loaded from the given source rather than from the local library file.'), + '#default_value' => $fontawesome_config->get('use_cdn'), + ], + 'external_svg_location' => [ + '#type' => 'textfield', + '#title' => $this->t('External File Location'), + '#default_value' => $fontawesome_config->get('external_svg_location'), + '#size' => 80, + '#description' => $this->t('Enter a source URL for the external Font Awesome library file you wish to use. Note that this is designed for use with the SVG with JS method. Use for the Webfonts method at your own risk. This URL should point to the Font Awesome JS svg file when using SVG with JS or it should point to the Font Awesome CSS file when using Web Fonts with CSS. Leave blank to use the default Font Awesome CDN.'), + '#states' => [ + 'disabled' => [ + ':input[name="use_cdn"]' => ['checked' => FALSE], + ], + 'visible' => [ + ':input[name="use_cdn"]' => ['checked' => TRUE], + ], + ], + ], + ]; + + $form['partial'] = [ + '#type' => 'details', + '#open' => FALSE, + '#title' => $this->t('Partial file configuration'), + '#description' => $this->t('By default, Font Awesome loads all of the icons. However, you can choose to load only some of the icon files if you only want a subset of the available icons. This method can result in reduced file size. These files will be assumed to exist in the same directory as the parent all.js/all.css file.'), + 'use_solid_file' => [ + '#type' => 'checkbox', + '#title' => $this->t('Load solid icons'), + '#description' => $this->t('Checking this box will cause the Font Awesome library to load the file containing the solid icon declarations (solid.js/solid.css)'), + '#default_value' => is_null($fontawesome_config->get('use_solid_file')) === TRUE ? TRUE : $fontawesome_config->get('use_solid_file'), + ], + 'use_regular_file' => [ + '#type' => 'checkbox', + '#title' => $this->t('Load regular icons'), + '#description' => $this->t('Checking this box will cause the Font Awesome library to load the file containing the regular icon declarations (regular.js/regular.css)'), + '#default_value' => is_null($fontawesome_config->get('use_regular_file')) === TRUE ? TRUE : $fontawesome_config->get('use_regular_file'), + ], + 'use_light_file' => [ + '#type' => 'checkbox', + '#title' => $this->t('Load light icons'), + '#description' => $this->t('Checking this box will cause the Font Awesome library to load the file containing the light icon declarations (light.js/light.css). Note that this a Pro-only feature.'), + '#default_value' => is_null($fontawesome_config->get('use_light_file')) === TRUE ? TRUE : $fontawesome_config->get('use_light_file'), + ], + 'use_brands_file' => [ + '#type' => 'checkbox', + '#title' => $this->t('Load brand icons'), + '#description' => $this->t('Checking this box will cause the Font Awesome library to load the file containing the brands icon declarations (brands.js/brands.css)'), + '#default_value' => is_null($fontawesome_config->get('use_brands_file')) === TRUE ? TRUE : $fontawesome_config->get('use_brands_file'), + ], + ]; + + $form['shim'] = [ + '#type' => 'details', + '#open' => TRUE, + '#title' => $this->t('Version 4 Backwards Compatibility'), + '#description' => $this->t('Version 5 of Font Awesome has some changes which require modifications to the way you declare many of your icons. The settings below are designed to ease that transition. See @upgradingLink for more information.', [ + '@upgradingLink' => Link::fromTextAndUrl($this->t('the Font Awesome guide to upgrading version 4 to version 5'), Url::fromUri('https://fontawesome.com/how-to-use/on-the-web/setup/upgrading-from-version-4'))->toString(), + ]), + 'use_shim' => [ + '#type' => 'checkbox', + '#title' => $this->t('Use version 4 shim file?'), + '#description' => $this->t('Rather than editing all of your Font Awesome declarations to use the new Font Awesome syntax, you can choose to include a shim file above. This file will allow you to use Font Awesome version 5 with Font Awesome version 4 syntax. This prevents you from needing to modify your existing code and syntax.'), + '#default_value' => $fontawesome_config->get('use_shim'), + ], + 'external_shim_location' => [ + '#type' => 'textfield', + '#title' => $this->t('External / local Library Location'), + '#default_value' => $fontawesome_config->get('external_shim_location'), + '#size' => 80, + '#description' => $this->t('Enter a source URL for the external / local (relative path) Font Awesome v4 shim file you wish to use. This URL should point to the Font Awesome JS shim file. Leave blank to use the default Font Awesome CDN.'), + '#states' => [ + 'disabled' => [ + ':input[name="use_cdn"]' => ['checked' => FALSE], + ':input[name="use_shim"]' => ['checked' => FALSE], + ], + 'visible' => [ + ':input[name="use_cdn"]' => ['checked' => TRUE], + ':input[name="use_shim"]' => ['checked' => TRUE], + ], + ], + ], + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + $values = $form_state->getValues(); + + // Validate URL. + if (!empty($values['fontawesome_external_location']) && !UrlHelper::isValid($values['fontawesome_external_location'])) { + $form_state->setErrorByName('fontawesome_external_location', $this->t('Invalid external library location.')); + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $values = $form_state->getValues(); + + // Load the fontawesome libraries so we can use its definitions here. + $fontawesome_library = $this->libraryDiscovery->getLibraryByName('fontawesome', 'fontawesome.svg'); + + // Clear the library cache so we use the updated information. + $this->libraryDiscovery->clearCachedDefinitions(); + + // Set external file defaults. + $default_location = 'https://use.fontawesome.com/releases/v' . $fontawesome_library['version'] . '/'; + $default_svg_location = $default_location . 'js/all.js'; + $default_webfonts_location = $default_location . 'css/all.css'; + $default_svg_shimfile_location = $default_location . 'js/v4-shims.js'; + $default_webfonts_shimfile_location = $default_location . 'css/v4-shims.css'; + + // Use default values if CDN is checked and the locations are blank. + if ($values['use_cdn']) { + if (empty($values['external_svg_location']) || $values['external_svg_location'] == $default_webfonts_location || $values['external_svg_location'] == $default_svg_location) { + // Choose the default depending on method. + $values['external_svg_location'] = ($values['method'] == 'webfonts') ? $default_webfonts_location : $default_svg_location; + } + if ($values['use_shim'] && (empty($values['external_shim_location']) || $values['external_shim_location'] == $default_webfonts_shimfile_location || $values['external_shim_location'] == $default_svg_shimfile_location)) { + // Choose the default depending on method. + $values['external_shim_location'] = ($values['method'] == 'webfonts') ? $default_webfonts_shimfile_location : $default_svg_shimfile_location; + } + } + + // Save the updated settings. + $this->config('fontawesome.settings') + ->set('tag', $values['tag']) + ->set('method', $values['method']) + ->set('use_cdn', $values['use_cdn']) + ->set('external_svg_location', (string) $values['external_svg_location']) + ->set('use_shim', $values['use_shim']) + ->set('external_shim_location', (string) $values['external_shim_location']) + ->set('allow_pseudo_elements', $values['allow_pseudo_elements']) + ->set('use_solid_file', $values['use_solid_file']) + ->set('use_regular_file', $values['use_regular_file']) + ->set('use_light_file', $values['use_light_file']) + ->set('use_brands_file', $values['use_brands_file']) + ->save(); + + parent::submitForm($form, $form_state); + } + +} diff --git a/web/modules/fontawesome/src/Plugin/CKEditorPlugin/DrupalFontAwesome.php b/web/modules/fontawesome/src/Plugin/CKEditorPlugin/DrupalFontAwesome.php new file mode 100644 index 0000000..9969a5a --- /dev/null +++ b/web/modules/fontawesome/src/Plugin/CKEditorPlugin/DrupalFontAwesome.php @@ -0,0 +1,64 @@ + $this->t('Insert Font Awesome Icon'), + ]; + } + + /** + * {@inheritdoc} + */ + public function getButtons() { + return [ + 'DrupalFontAwesome' => [ + 'label' => $this->t('Font Awesome'), + 'image' => drupal_get_path('module', 'fontawesome') . '/js/plugins/drupalfontawesome/icons/drupalfontawesome.png', + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function isEnabled(Editor $editor) { + // Assume that someone installing this module probably wants the help. + return TRUE; + } + +} diff --git a/web/modules/fontawesome/src/Plugin/Field/FieldFormatter/FontAwesomeIconFormatter.php b/web/modules/fontawesome/src/Plugin/Field/FieldFormatter/FontAwesomeIconFormatter.php new file mode 100644 index 0000000..f702d57 --- /dev/null +++ b/web/modules/fontawesome/src/Plugin/Field/FieldFormatter/FontAwesomeIconFormatter.php @@ -0,0 +1,216 @@ +configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $plugin_id, + $plugin_definition, + $configuration['field_definition'], + $configuration['settings'], + $configuration['label'], + $configuration['view_mode'], + $configuration['third_party_settings'], + $container->get('config.factory') + ); + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + // Load the configuration settings. + $configuration_settings = $this->configFactory->get('fontawesome.settings'); + + // Setting for optional download link. + $elements['layers'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Display multi-value fields as layers?'), + '#default_value' => $this->getSetting('layers'), + '#description' => $this->t('Layers are the new way to place icons and text visually on top of each other, replacing the Font Awesome classic icons stacks. With this new approach you can use more than 2 icons. Layers are awesome when you don’t want your page’s background to show through, or when you do want to use multiple colors, layer several icons, layer text, or layer counters onto an icon. Note that layers only work with the SVG version of Font Awesome. For more information, see @layersLink.', [ + '@layersLink' => Link::fromTextAndUrl($this->t('the Font Awesome guide to layers'), Url::fromUri('https://fontawesome.com/how-to-use/on-the-web/styling/layering'))->toString(), + ]), + // Disable power transforms for webfonts. + '#disabled' => $configuration_settings->get('method') == 'webfonts', + ]; + + return $elements; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $settings = $this->getSettings(); + + $summary = []; + + // Load the configuration settings. + $configuration_settings = $this->configFactory->get('fontawesome.settings'); + + // Show whether or not we are layering the icons. + $summary[] = $this->t('Display multi-value fields as layers: @layersSetting', [ + '@layersSetting' => (($settings['layers'] && $configuration_settings->get('method') != 'webfonts') ? 'Yes' : 'No'), + ]); + + return $summary; + } + + /** + * {@inheritdoc} + */ + public static function defaultSettings() { + return [ + 'layers' => FALSE, + ] + parent::defaultSettings(); + } + + /** + * {@inheritdoc} + */ + public function viewElements(FieldItemListInterface $items, $langcode) { + // Early opt-out if the field is empty. + if (count($items) <= 0) { + return []; + } + + // Load the configuration settings. + $configurationSettings = $this->configFactory->get('fontawesome.settings'); + + // Attach the libraries as needed. + $fontawesomeLibraries = []; + if ($configurationSettings->get('method') == 'webfonts') { + // Webfonts method. + $fontawesomeLibraries[] = 'fontawesome/fontawesome.webfonts'; + + // Attach the shim file if needed. + if ($configurationSettings->get('use_shim')) { + $fontawesomeLibraries[] = 'fontawesome/fontawesome.webfonts.shim'; + } + } + else { + // SVG method. + $fontawesomeLibraries[] = 'fontawesome/fontawesome.svg'; + + // Attach the shim file if needed. + if ($configurationSettings->get('use_shim')) { + $fontawesomeLibraries[] = 'fontawesome/fontawesome.svg.shim'; + } + } + + // Loop over each icon and build data. + $icons = []; + foreach ($items as $item) { + // Get the icon settings. + $iconSettings = unserialize($item->get('settings')->getValue()); + $cssStyles = []; + + // Format mask. + $iconMask = ''; + if (!empty($iconSettings['masking']['mask'])) { + $iconMask = $iconSettings['masking']['style'] . ' fa-' . $iconSettings['masking']['mask']; + } + unset($iconSettings['masking']); + + // Format power transforms. + $iconTransforms = []; + $powerTransforms = $iconSettings['power_transforms']; + foreach ($powerTransforms as $transform) { + if (!empty($transform['type'])) { + $iconTransforms[] = $transform['type'] . '-' . $transform['value']; + } + } + unset($iconSettings['power_transforms']); + + // Move duotone settings into the render. + if (isset($iconSettings['duotone'])) { + // Handle swap opacity flag. + if (!empty($iconSettings['duotone']['swap-opacity'])) { + $iconSettings['swap-opacity'] = $iconSettings['duotone']['swap-opacity']; + } + // Handle custom CSS styles. + if (!empty($iconSettings['duotone']['opacity']['primary'])) { + $cssStyles[] = '--fa-primary-opacity: ' . $iconSettings['duotone']['opacity']['primary'] . ';'; + } + if (!empty($iconSettings['duotone']['opacity']['secondary'])) { + $cssStyles[] = '--fa-secondary-opacity: ' . $iconSettings['duotone']['opacity']['secondary'] . ';'; + } + if (!empty($iconSettings['duotone']['color']['primary'])) { + $cssStyles[] = '--fa-primary-color: ' . $iconSettings['duotone']['color']['primary'] . ';'; + } + if (!empty($iconSettings['duotone']['color']['secondary'])) { + $cssStyles[] = '--fa-secondary-color: ' . $iconSettings['duotone']['color']['secondary'] . ';'; + } + + unset($iconSettings['duotone']); + } + + $icons[] = [ + '#theme' => 'fontawesomeicon', + '#tag' => $configurationSettings->get('tag'), + '#name' => 'fa-' . $item->get('icon_name')->getValue(), + '#style' => $item->get('style')->getValue(), + '#settings' => implode(' ', array_filter($iconSettings)), + '#transforms' => implode(' ', $iconTransforms), + '#mask' => $iconMask, + '#css' => implode(' ', $cssStyles), + ]; + } + + // Get the icon settings. + $settings = $this->getSettings(); + + return [ + [ + '#theme' => 'fontawesomeicons', + '#icons' => $icons, + '#layers' => $settings['layers'], + ], + '#attached' => [ + 'library' => $fontawesomeLibraries, + ], + ]; + } + +} diff --git a/web/modules/fontawesome/src/Plugin/Field/FieldType/FontAwesomeIcon.php b/web/modules/fontawesome/src/Plugin/Field/FieldType/FontAwesomeIcon.php new file mode 100644 index 0000000..0b037a5 --- /dev/null +++ b/web/modules/fontawesome/src/Plugin/Field/FieldType/FontAwesomeIcon.php @@ -0,0 +1,80 @@ + [ + 'icon_name' => [ + 'type' => 'text', + 'size' => 'normal', + 'not null' => TRUE, + ], + 'style' => [ + 'type' => 'text', + 'size' => 'tiny', + 'not null' => TRUE, + ], + 'settings' => [ + 'type' => 'text', + 'size' => 'normal', + 'not null' => FALSE, + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) { + $properties = []; + $properties['icon_name'] = DataDefinition::create('string') + ->setLabel(t('Icon Name')) + ->setDescription(t('The name of the icon')); + $properties['style'] = DataDefinition::create('string') + ->setLabel(t('Icon Style')) + ->setDescription(t('The style of the icon')); + $properties['settings'] = DataDefinition::create('string') + ->setLabel(t('Icon Settings')) + ->setDescription(t('The additional class settings for the icon')); + + return $properties; + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + $icon_name = $this->get('icon_name')->getValue(); + return $icon_name === NULL || $icon_name === ''; + } + +} diff --git a/web/modules/fontawesome/src/Plugin/Field/FieldWidget/FontAwesomeIconWidget.php b/web/modules/fontawesome/src/Plugin/Field/FieldWidget/FontAwesomeIconWidget.php new file mode 100644 index 0000000..e73d5f6 --- /dev/null +++ b/web/modules/fontawesome/src/Plugin/Field/FieldWidget/FontAwesomeIconWidget.php @@ -0,0 +1,527 @@ +configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $plugin_id, + $plugin_definition, + $configuration['field_definition'], + $configuration['settings'], + $configuration['third_party_settings'], + $container->get('config.factory') + ); + } + + /** + * {@inheritdoc} + */ + public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { + $field_name = $this->fieldDefinition->getName(); + + // Load the configuration settings. + $configuration_settings = $this->configFactory->get('fontawesome.settings'); + + $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality(); + $element['icon_name'] = [ + '#type' => 'textfield', + '#title' => $cardinality == 1 ? $this->fieldDefinition->getLabel() : $this->t('Icon Name'), + '#required' => $element['#required'], + '#size' => 50, + '#field_prefix' => 'fa-', + '#default_value' => $items[$delta]->get('icon_name')->getValue(), + '#description' => $this->t('Name of the Font Awesome Icon. See @iconsLink for valid icon names, or begin typing for an autocomplete list. Note that all four versions of the icon will be shown - Light, Regular, Solid, and Duotone respectively. If the icon shows a question mark, that icon version is not supported in your version of Fontawesome.', [ + '@iconsLink' => Link::fromTextAndUrl($this->t('the Font Awesome icon list'), Url::fromUri('https://fontawesome.com/icons'))->toString(), + ]), + '#autocomplete_route_name' => 'fontawesome.autocomplete', + '#element_validate' => [ + [static::class, 'validateIconName'], + ], + ]; + + // Get current settings. + $iconSettings = unserialize($items[$delta]->get('settings')->getValue()); + // Build additional settings. + $element['settings'] = [ + '#type' => 'details', + '#open' => FALSE, + '#title' => $this->t('Additional Font Awesome Settings'), + ]; + + // Allow user to determine style. + $element['settings']['style'] = [ + '#type' => 'select', + '#title' => $this->t('Style'), + '#description' => $this->t('This changes the style of the icon. Please note that this is not available for all icons, and for some of the icons this is only available in the pro version. If the icon does not render properly in the , the icon does not support that style. Notably, brands do not support any styles. See @iconLink for more information.', [ + '@iconLink' => Link::fromTextAndUrl($this->t('the Font Awesome icon list'), Url::fromUri('https://fontawesome.com/icons'))->toString(), + ]), + '#options' => [ + 'fas' => $this->t('Solid'), + 'far' => $this->t('Regular'), + 'fal' => $this->t('Light'), + 'fad' => $this->t('Duotone'), + ], + '#default_value' => $items[$delta]->get('style')->getValue(), + ]; + // Remove style options if they aren't being loaded. + if (!$configuration_settings->get('use_solid_file')) { + unset($element['settings']['style']['#options']['fas']); + } + if (!$configuration_settings->get('use_regular_file')) { + unset($element['settings']['style']['#options']['far']); + } + if (!$configuration_settings->get('use_light_file')) { + unset($element['settings']['style']['#options']['fal']); + } + + // Allow user to determine size. + $element['settings']['size'] = [ + '#type' => 'select', + '#title' => $this->t('Size'), + '#description' => $this->t('This increases icon sizes relative to their container'), + '#options' => [ + '' => $this->t('Default'), + 'fa-xs' => $this->t('Extra Small'), + 'fa-sm' => $this->t('Small'), + 'fa-lg' => $this->t('Large'), + 'fa-2x' => $this->t('2x'), + 'fa-3x' => $this->t('3x'), + 'fa-4x' => $this->t('4x'), + 'fa-5x' => $this->t('5x'), + 'fa-6x' => $this->t('6x'), + 'fa-7x' => $this->t('7x'), + 'fa-8x' => $this->t('8x'), + 'fa-9x' => $this->t('9x'), + 'fa-10x' => $this->t('10x'), + ], + '#default_value' => isset($iconSettings['size']) ? $iconSettings['size'] : '', + ]; + // Set icon to fixed width. + $element['settings']['fixed-width'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Fixed Width?'), + '#description' => $this->t('Use to set icons at a fixed width. Great to use when different icon widths throw off vertical alignment. Especially useful in things like nav lists and list groups.'), + '#default_value' => isset($iconSettings['fixed-width']) ? $iconSettings['fixed-width'] : FALSE, + '#return_value' => 'fa-fw', + ]; + // Add border. + $element['settings']['border'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Border?'), + '#description' => $this->t('Adds a border to the icon.'), + '#default_value' => isset($iconSettings['border']) ? $iconSettings['border'] : FALSE, + '#return_value' => 'fa-border', + ]; + // Invert color. + $element['settings']['invert'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Invert color?'), + '#description' => $this->t('Inverts the color of the icon (black becomes white, etc.)'), + '#default_value' => isset($iconSettings['invert']) ? $iconSettings['invert'] : FALSE, + '#return_value' => 'fa-inverse', + ]; + // Animated the icon. + $element['settings']['animation'] = [ + '#type' => 'select', + '#title' => $this->t('Animation'), + '#description' => $this->t('Use spin to get any icon to rotate, and pulse to have it rotate with 8 steps. Works especially well with fa-spinner & everything in the @iconLink.', [ + '@iconLink' => Link::fromTextAndUrl($this->t('spinner icons category'), Url::fromUri('https://fontawesome.com/icons?c=spinner-icons'))->toString(), + ]), + '#options' => [ + '' => $this->t('None'), + 'fa-spin' => $this->t('Spin'), + 'fa-pulse' => $this->t('Pulse'), + ], + '#default_value' => isset($iconSettings['animation']) ? $iconSettings['animation'] : '', + ]; + + // Pull the icons. + $element['settings']['pull'] = [ + '#type' => 'select', + '#title' => $this->t('Pull'), + '#description' => $this->t('This setting will pull the icon (float) to one side or the other in relation to its nearby content'), + '#options' => [ + '' => $this->t('None'), + 'fa-pull-left' => $this->t('Left'), + 'fa-pull-right' => $this->t('Right'), + ], + '#default_value' => isset($iconSettings['pull']) ? $iconSettings['pull'] : '', + ]; + + // Allow user to edit duotone. + $element['settings']['duotone'] = [ + '#type' => 'details', + '#open' => FALSE, + // Disable power transforms for webfonts. + '#title' => $this->t('Duotone Settings'), + '#description' => $this->t('Duotone provides a version of every icon in Font Awesome that has two distinct shades of color. They’re great for adding more of your brand or an illustrative quality to the icons in your project. See @duotoneLink for more information. Note that duotone only works with the Pro version of Font Awesome.', [ + '@duotoneLink' => Link::fromTextAndUrl($this->t('the Font Awesome guide to duotone'), Url::fromUri('https://fontawesome.com/how-to-use/on-the-web/styling/duotone-icons'))->toString(), + ]), + ]; + $element['settings']['duotone']['swap-opacity'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Swap Opacity?'), + '#description' => $this->t('Use to swap the default opacity of each duotone icon’s layers. This will make an icon’s primary layer have the default opacity of 40% rather than its secondary layer.'), + '#default_value' => isset($iconSettings['duotone']['swap-opacity']) ? $iconSettings['duotone']['swap-opacity'] : '', + '#return_value' => 'fa-swap-opacity', + ]; + // Manual opacity. + $element['settings']['duotone']['opacity'] = [ + '#type' => 'details', + '#open' => TRUE, + // Disable power transforms for webfonts. + '#title' => $this->t('Layer Opacity'), + '#description' => $this->t('By default the secondary layer in a duotone icon is set to 40% opacity (via an opacity 0.4; rule in Font Awesome’s support CSS). You can explicitly set the opacity of a duotone icon’s layer by using CSS custom properties either in your style sheets or by setting them manually below. New to custom properties? Here are some @cssLink.', [ + '@cssLink' => Link::fromTextAndUrl($this->t('places to set them'), Url::fromUri('https://fontawesome.com/how-to-use/on-the-web/styling/duotone-icons#using-in-a-project'))->toString(), + ]), + ]; + $element['settings']['duotone']['opacity']['primary'] = [ + '#type' => 'number', + '#title' => $this->t('Primary Layer Opacity'), + '#step' => 0.01, + '#default_value' => isset($iconSettings['duotone']['opacity']['primary']) ? $iconSettings['duotone']['opacity']['primary'] : '', + '#description' => $this->t('Opacity of the primary duotone layer.'), + ]; + $element['settings']['duotone']['opacity']['secondary'] = [ + '#type' => 'number', + '#title' => $this->t('Secondary Layer Opacity'), + '#step' => 0.01, + '#default_value' => isset($iconSettings['duotone']['opacity']['secondary']) ? $iconSettings['duotone']['opacity']['secondary'] : '', + '#description' => $this->t('Opacity of the secondary duotone layer.'), + ]; + // Manual opacity. + $element['settings']['duotone']['color'] = [ + '#type' => 'details', + '#open' => TRUE, + // Disable power transforms for webfonts. + '#title' => $this->t('Layer Color'), + '#description' => $this->t('Like all other Font Awesome icons, duotone icons automatically inherit CSS size and color. A duotone icon consists of a primary and secondary layer. By default, The secondary layer is given an opacity of 40% so that it appears as a lighter shade of the icon’s inherited or directly set color. Using CSS custom properties, we’ve also added some color hooks to a duotone icon’s primary and secondary layers. New to custom properties? Here are some @cssLink.', [ + '@cssLink' => Link::fromTextAndUrl($this->t('places to set them'), Url::fromUri('https://fontawesome.com/how-to-use/on-the-web/styling/duotone-icons#using-in-a-project'))->toString(), + ]), + ]; + $element['settings']['duotone']['color']['primary'] = [ + '#type' => 'color', + '#title' => $this->t('Primary Layer Color'), + '#step' => 0.01, + '#default_value' => isset($iconSettings['duotone']['color']['primary']) ? $iconSettings['duotone']['color']['primary'] : '', + '#description' => $this->t('Opacity of the primary duotone layer.'), + ]; + $element['settings']['duotone']['color']['secondary'] = [ + '#type' => 'color', + '#title' => $this->t('Secondary Layer Color'), + '#step' => 0.01, + '#default_value' => isset($iconSettings['duotone']['color']['secondary']) ? $iconSettings['duotone']['color']['secondary'] : '', + '#description' => $this->t('Opacity of the secondary duotone layer.'), + ]; + + // Allow user to add masking. + $element['settings']['masking'] = [ + '#type' => 'details', + '#open' => FALSE, + // Disable power transforms for webfonts. + '#disabled' => $configuration_settings->get('method') == 'webfonts', + '#title' => $this->t('Icon Mask'), + '#description' => $this->t('Masking is used to combine two icons to create one single-color shape. Use it with Power Transforms for some really awesome effects. Masks are great when you do want your background color to show through. See @maskingLink for more information. Note that masking only works with the SVG version of Font Awesome.', [ + '@maskingLink' => Link::fromTextAndUrl($this->t('the Font Awesome guide to masking'), Url::fromUri('https://fontawesome.com/how-to-use/svg-with-js#masking'))->toString(), + ]), + ]; + $element['settings']['masking']['mask'] = [ + '#type' => 'textfield', + '#title' => $this->t('Icon Name'), + '#size' => 50, + '#field_prefix' => 'fa-', + '#default_value' => isset($iconSettings['masking']['mask']) ? $iconSettings['masking']['mask'] : '', + '#description' => $this->t('Name of the Font Awesome Icon. See @iconsLink for valid icon names, or begin typing for an autocomplete list.', [ + '@iconsLink' => Link::fromTextAndUrl($this->t('the Font Awesome icon list'), Url::fromUri('https://fontawesome.com/icons'))->toString(), + ]), + '#autocomplete_route_name' => 'fontawesome.autocomplete', + '#element_validate' => [ + [static::class, 'validateIconName'], + ], + ]; + $element['settings']['masking']['style'] = [ + '#type' => 'select', + '#title' => $this->t('Style'), + '#description' => $this->t('This changes the style of the masking icon. Please note that this is not available for all icons, and for some of the icons this is only available in the pro version. If the icon does not render properly in the preview above, the icon does not support that style. Notably, brands do not support any styles. See @iconLink for more information.', [ + '@iconLink' => Link::fromTextAndUrl($this->t('the Font Awesome icon list'), Url::fromUri('https://fontawesome.com/icons'))->toString(), + ]), + '#options' => [ + 'fas' => $this->t('Solid'), + 'far' => $this->t('Regular'), + 'fal' => $this->t('Light'), + 'fad' => $this->t('Duotone'), + ], + '#default_value' => isset($iconSettings['masking']['style']) ? $iconSettings['masking']['style'] : '', + ]; + + // Build new power-transforms. + $element['settings']['power_transforms'] = [ + '#type' => 'details', + '#open' => FALSE, + // Disable power transforms for webfonts. + '#disabled' => $configuration_settings->get('method') == 'webfonts', + '#title' => $this->t('Power Transforms'), + '#description' => $this->t('See @iconLink for additional information on Power Transforms. Note that these transforms only work with the SVG with JS version of Font Awesome and are disabled for Webfonts. See the @adminLink to set your version of Font Awesome.', [ + '@iconLink' => Link::fromTextAndUrl($this->t('the Font Awesome `How to use` guide'), Url::fromUri('https://fontawesome.com/how-to-use/svg-with-js'))->toString(), + '@adminLink' => Link::createFromRoute($this->t('admin page'), 'fontawesome.admin_settings')->toString(), + ]), + ]; + // Rotate the icon. + $element['settings']['power_transforms']['rotate']['value'] = [ + '#type' => 'number', + '#title' => $this->t('Rotate'), + '#step' => 0.01, + '#field_suffix' => '°', + '#default_value' => isset($iconSettings['power_transforms']['rotate']['value']) ? $iconSettings['power_transforms']['rotate']['value'] : '', + '#description' => $this->t('Power Transform rotating effects icon angle without changing or moving the container. To rotate icons use any arbitrary value. Units are degrees with negative numbers allowed.'), + ]; + // Flip the icon. + $element['settings']['power_transforms']['flip-h']['value'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Flip Horizontal?'), + '#default_value' => isset($iconSettings['power_transforms']['flip-h']['value']) ? $iconSettings['power_transforms']['flip-h']['value'] : FALSE, + '#description' => $this->t('Power Transform flipping effects icon reflection without changing or moving the container.'), + '#return_value' => 'h', + ]; + $element['settings']['power_transforms']['flip-v']['value'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Flip Vertical?'), + '#default_value' => isset($iconSettings['power_transforms']['flip-v']['value']) ? $iconSettings['power_transforms']['flip-v']['value'] : FALSE, + '#description' => $this->t('Power Transform flipping effects icon reflection without changing or moving the container.'), + '#return_value' => 'v', + ]; + // Scale the icon. + $element['settings']['power_transforms']['scale'] = [ + '#type' => 'details', + '#open' => TRUE, + '#title' => $this->t('Scale'), + '#description' => $this->t('Power Transform scaling effects icon size without changing or moving the container. This field will scale icons up or down with any arbitrary value, including decimals. Units are 1/16em.'), + '#element_validate' => [ + [static::class, 'validatePowerTransforms'], + ], + ]; + $element['settings']['power_transforms']['scale']['type'] = [ + '#type' => 'select', + '#title' => $this->t('Scale Type'), + '#options' => [ + '' => $this->t('None'), + 'shrink' => $this->t('Shrink'), + 'grow' => $this->t('Grow'), + ], + '#default_value' => isset($iconSettings['power_transforms']['scale']['type']) ? $iconSettings['power_transforms']['scale']['type'] : '', + ]; + $element['settings']['power_transforms']['scale']['value'] = [ + '#type' => 'number', + '#title' => $this->t('Scale Value'), + '#min' => 0, + '#step' => 0.01, + '#default_value' => isset($iconSettings['power_transforms']['scale']['value']) ? $iconSettings['power_transforms']['scale']['value'] : '', + '#states' => [ + 'disabled' => [ + ':input[name="' . $field_name . '[' . $delta . '][settings][power_transforms][scale][type]"]' => ['value' => ''], + ], + ], + ]; + // Position the icon. + $element['settings']['power_transforms']['position_y'] = [ + '#type' => 'details', + '#open' => TRUE, + '#title' => $this->t('Position (Y Axis)'), + '#description' => $this->t('Power Transform positioning effects icon location without changing or moving the container. This field will move icons up or down with any arbitrary value, including decimals. Units are 1/16em.'), + '#element_validate' => [ + [static::class, 'validatePowerTransforms'], + ], + ]; + $element['settings']['power_transforms']['position_y']['type'] = [ + '#type' => 'select', + '#title' => $this->t('Position Type'), + '#options' => [ + '' => $this->t('None'), + 'up' => $this->t('Up'), + 'down' => $this->t('Down'), + ], + '#default_value' => isset($iconSettings['power_transforms']['position_y']['type']) ? $iconSettings['power_transforms']['position_y']['type'] : '', + ]; + $element['settings']['power_transforms']['position_y']['value'] = [ + '#type' => 'number', + '#title' => $this->t('Position Value'), + '#min' => 0, + '#step' => 0.01, + '#default_value' => isset($iconSettings['power_transforms']['position_y']['value']) ? $iconSettings['power_transforms']['position_y']['value'] : '', + '#states' => [ + 'disabled' => [ + ':input[name="' . $field_name . '[' . $delta . '][settings][power_transforms][position_y][type]"]' => ['value' => ''], + ], + ], + ]; + $element['settings']['power_transforms']['position_x'] = [ + '#type' => 'details', + '#open' => TRUE, + '#title' => $this->t('Position (X Axis)'), + '#description' => $this->t('Power Transform positioning effects icon location without changing or moving the container. This field will move icons up or down with any arbitrary value, including decimals. Units are 1/16em.'), + '#element_validate' => [ + [static::class, 'validatePowerTransforms'], + ], + ]; + $element['settings']['power_transforms']['position_x']['type'] = [ + '#type' => 'select', + '#title' => $this->t('Position Type'), + '#options' => [ + '' => $this->t('None'), + 'left' => $this->t('Left'), + 'right' => $this->t('Right'), + ], + '#default_value' => isset($iconSettings['power_transforms']['position_x']['type']) ? $iconSettings['power_transforms']['position_x']['type'] : '', + ]; + $element['settings']['power_transforms']['position_x']['value'] = [ + '#type' => 'number', + '#title' => $this->t('Position Value'), + '#min' => 0, + '#step' => 0.01, + '#default_value' => isset($iconSettings['power_transforms']['position_x']['value']) ? $iconSettings['power_transforms']['position_x']['value'] : '', + '#states' => [ + 'disabled' => [ + ':input[name="' . $field_name . '[' . $delta . '][settings][power_transforms][position_x][type]"]' => ['value' => ''], + ], + ], + ]; + + return $element; + } + + /** + * Validate the Font Awesome power transforms. + */ + public static function validatePowerTransforms($element, FormStateInterface $form_state) { + $values = $form_state->getValue($element['#parents']); + + if (!empty($values['type']) && empty($values['value'])) { + $form_state->setError($element, t('Missing value for Font Awesome Power Transform %value. Please see @iconLink for information on correct values.', [ + '%value' => $values['type'], + '@iconLink' => Link::fromTextAndUrl(t('the Font Awesome icon list'), Url::fromUri('https://fontawesome.com/how-to-use/svg-with-js'))->toString(), + ])); + } + elseif (empty($values['type']) && !empty($values['value'])) { + $form_state->setError($element, t('Missing type value for Font Awesome Power Transform. Please see @iconLink for information on correct values.', [ + '@iconLink' => Link::fromTextAndUrl(t('the Font Awesome icon list'), Url::fromUri('https://fontawesome.com/how-to-use/svg-with-js'))->toString(), + ])); + } + if (!empty($values['value']) && !is_numeric($values['value'])) { + $form_state->setError($element, t("Invalid value for Font Awesome Power Transform %value. Please see @iconLink for information on correct values.", [ + '%value' => $values['type'], + '@iconLink' => Link::fromTextAndUrl(t('the Font Awesome icon list'), Url::fromUri('https://fontawesome.com/how-to-use/svg-with-js'))->toString(), + ])); + } + } + + /** + * Validate the Font Awesome icon name. + */ + public static function validateIconName($element, FormStateInterface $form_state) { + $value = $element['#value']; + if (strlen($value) == 0) { + $form_state->setValueForElement($element, ''); + return; + } + + // Load the icon data so we can check for a valid icon. + $iconData = fontawesome_extract_icon_metadata($value); + + if (!isset($iconData['name'])) { + $form_state->setError($element, t("Invalid icon name %value. Please see @iconLink for correct icon names.", [ + '%value' => $value, + '@iconLink' => Link::fromTextAndUrl(t('the Font Awesome icon list'), Url::fromUri('https://fontawesome.com/icons'))->toString(), + ])); + } + } + + /** + * {@inheritdoc} + */ + public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { + // Load the icon data so we can determine the icon type. + $metadata = fontawesome_extract_icons(); + + // Loop over each item and set the data properly. + foreach ($values as &$item) { + // Remove the prefix if the user accidentally added it. + if (substr($item['icon_name'], 0, 3) == 'fa-') { + $item['icon_name'] = substr($item['icon_name'], 3); + } + + if (!empty($item['settings']['masking']['style'])) { + $item['settings']['masking']['style'] = isset($metadata[$item['icon_name']]['styles']) ? fontawesome_determine_prefix($metadata[$item['icon_name']]['styles'], $item['settings']['masking']['style']) : 'fas'; + } + + // Massage rotate and flip values to make them format properly. + if (is_numeric($item['settings']['power_transforms']['rotate']['value'])) { + $item['settings']['power_transforms']['rotate']['type'] = 'rotate'; + } + else { + unset($item['settings']['power_transforms']['rotate']); + } + if (!empty($item['settings']['power_transforms']['flip-h']['value'])) { + $item['settings']['power_transforms']['flip-h']['type'] = 'flip'; + } + else { + unset($item['settings']['power_transforms']['flip-h']); + } + if (!empty($item['settings']['power_transforms']['flip-v']['value'])) { + $item['settings']['power_transforms']['flip-v']['type'] = 'flip'; + } + else { + unset($item['settings']['power_transforms']['flip-v']); + } + // Determine the icon style - brands don't allow style. + $item['style'] = isset($metadata[$item['icon_name']]['styles']) ? fontawesome_determine_prefix($metadata[$item['icon_name']]['styles'], $item['settings']['style']) : 'fas'; + unset($item['settings']['style']); + + $item['settings'] = serialize(array_filter($item['settings'])); + } + + return $values; + } + +} diff --git a/web/modules/fontawesome/templates/fontawesomeicon.html.twig b/web/modules/fontawesome/templates/fontawesomeicon.html.twig new file mode 100644 index 0000000..8dd612b --- /dev/null +++ b/web/modules/fontawesome/templates/fontawesomeicon.html.twig @@ -0,0 +1,20 @@ +{# +/** + * @file + * Default implementation for Font Awesome Icon field. + * + * Available variables: + * - tag: the HTML tag being used to create the icon. + * - icon: the name of the icon being used for templating. + * - style: the Font Awesome style for the icon. + * - settings: the additional Font Awesome style settings. + * - transforms: Font Awesome power transforms. + * - mask: Font Awesome mask. + * - css: Additional inline CSS styles (for duotone, etc). + * + * @ingroup themeable + */ +#} +
+ <{{tag }} class="{{ style }} {{ name }} {{ settings }}" data-fa-transform="{{ transforms }}" data-fa-mask="{{ mask }}" style="{{ css }}"> +
diff --git a/web/modules/fontawesome/templates/fontawesomeicons.html.twig b/web/modules/fontawesome/templates/fontawesomeicons.html.twig new file mode 100644 index 0000000..079bb2b --- /dev/null +++ b/web/modules/fontawesome/templates/fontawesomeicons.html.twig @@ -0,0 +1,19 @@ +{# +/** + * @file + * Default implementation for Font Awesome icons. + * + * Available variables: + * - icons: a list of Font Awesome icons to be rendered. + * - layers: flag indicating if icons are printing as layers. + * + * @ingroup themeable + */ +#} +
+ {% if layers == '1' %} + {{ icons }} + {% else %} + {{ icons }} + {% endif %} +
diff --git a/web/sites/default/local.services.yml b/web/sites/default/local.services.yml new file mode 100644 index 0000000..6404bb7 --- /dev/null +++ b/web/sites/default/local.services.yml @@ -0,0 +1,175 @@ +parameters: + session.storage.options: + # Default ini options for sessions. + # + # Some distributions of Linux (most notably Debian) ship their PHP + # installations with garbage collection (gc) disabled. Since Drupal depends + # on PHP's garbage collection for clearing sessions, ensure that garbage + # collection occurs by using the most common settings. + # @default 1 + gc_probability: 1 + # @default 100 + gc_divisor: 100 + # + # Set session lifetime (in seconds), i.e. the time from the user's last + # visit to the active session may be deleted by the session garbage + # collector. When a session is deleted, authenticated users are logged out, + # and the contents of the user's $_SESSION variable is discarded. + # @default 200000 + gc_maxlifetime: 200000 + # + # Set session cookie lifetime (in seconds), i.e. the time from the session + # is created to the cookie expires, i.e. when the browser is expected to + # discard the cookie. The value 0 means "until the browser is closed". + # @default 2000000 + cookie_lifetime: 2000000 + # + # Drupal automatically generates a unique session cookie name based on the + # full domain name used to access the site. This mechanism is sufficient + # for most use-cases, including multi-site deployments. However, if it is + # desired that a session can be reused across different subdomains, the + # cookie domain needs to be set to the shared base domain. Doing so assures + # that users remain logged in as they cross between various subdomains. + # To maximize compatibility and normalize the behavior across user agents, + # the cookie domain should start with a dot. + # + # @default none + # cookie_domain: '.example.com' + # + twig.config: + # Twig debugging: + # + # When debugging is enabled: + # - The markup of each Twig template is surrounded by HTML comments that + # contain theming information, such as template file name suggestions. + # - Note that this debugging markup will cause automated tests that directly + # check rendered HTML to fail. When running automated tests, 'debug' + # should be set to FALSE. + # - The dump() function can be used in Twig templates to output information + # about template variables. + # - Twig templates are automatically recompiled whenever the source code + # changes (see auto_reload below). + # + # For more information about debugging Twig templates, see + # https://www.drupal.org/node/1906392. + # + # Not recommended in production environments + # @default false + debug: true + # Twig auto-reload: + # + # Automatically recompile Twig templates whenever the source code changes. + # If you don't provide a value for auto_reload, it will be determined + # based on the value of debug. + # + # Not recommended in production environments + # @default null + auto_reload: null + # Twig cache: + # + # By default, Twig templates will be compiled and stored in the filesystem + # to increase performance. Disabling the Twig cache will recompile the + # templates from source each time they are used. In most cases the + # auto_reload setting above should be enabled rather than disabling the + # Twig cache. + # + # Not recommended in production environments + # @default true + cache: true + renderer.config: + # Renderer required cache contexts: + # + # The Renderer will automatically associate these cache contexts with every + # render array, hence varying every render array by these cache contexts. + # + # @default ['languages:language_interface', 'theme', 'user.permissions'] + required_cache_contexts: + ["languages:language_interface", "theme", "user.permissions"] + # Renderer automatic placeholdering conditions: + # + # Drupal allows portions of the page to be automatically deferred when + # rendering to improve cache performance. That is especially helpful for + # cache contexts that vary widely, such as the active user. On some sites + # those may be different, however, such as sites with only a handful of + # users. If you know what the high-cardinality cache contexts are for your + # site, specify those here. If you're not sure, the defaults are fairly safe + # in general. + # + # For more information about rendering optimizations see + # https://www.drupal.org/developing/api/8/render/arrays/cacheability#optimizing + auto_placeholder_conditions: + # Max-age at or below which caching is not considered worthwhile. + # + # Disable by setting to -1. + # + # @default 0 + max-age: 0 + # Cache contexts with a high cardinality. + # + # Disable by setting to []. + # + # @default ['session', 'user'] + contexts: ["session", "user"] + # Tags with a high invalidation frequency. + # + # Disable by setting to []. + # + # @default [] + tags: [] + # Cacheability debugging: + # + # Responses with cacheability metadata (CacheableResponseInterface instances) + # get X-Drupal-Cache-Tags and X-Drupal-Cache-Contexts headers. + # + # For more information about debugging cacheable responses, see + # https://www.drupal.org/developing/api/8/response/cacheable-response-interface + # + # Not recommended in production environments + # @default false + http.response.debug_cacheability_headers: false + factory.keyvalue: + {} + # Default key/value storage service to use. + # @default keyvalue.database + # default: keyvalue.database + # Collection-specific overrides. + # state: keyvalue.database + factory.keyvalue.expirable: + {} + # Default key/value expirable storage service to use. + # @default keyvalue.database.expirable + # default: keyvalue.database.expirable + # Allowed protocols for URL generation. + filter_protocols: + - http + - https + - ftp + - news + - nntp + - tel + - telnet + - mailto + - irc + - ssh + - sftp + - webcal + - rtsp + + # Configure Cross-Site HTTP requests (CORS). + # Read https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS + # for more information about the topic in general. + # Note: By default the configuration is disabled. + cors.config: + enabled: false + # Specify allowed headers, like 'x-allowed-header'. + allowedHeaders: [] + # Specify allowed request methods, specify ['*'] to allow all possible ones. + allowedMethods: [] + # Configure requests allowed from specific origins. + allowedOrigins: ["*"] + # Sets the Access-Control-Expose-Headers header. + exposedHeaders: false + # Sets the Access-Control-Max-Age header. + maxAge: false + # Sets the Access-Control-Allow-Credentials header. + supportsCredentials: false diff --git a/web/themes/custom/barbell/barbell.libraries.yml b/web/themes/custom/barbell/barbell.libraries.yml index b890d07..09d68e3 100644 --- a/web/themes/custom/barbell/barbell.libraries.yml +++ b/web/themes/custom/barbell/barbell.libraries.yml @@ -18,3 +18,7 @@ global-styling: css/blog.css: {} css/tim.css: {} css/programi.css: {} + css/raspored.css: {} + css/galerija.css: {} + css/pojedine_strane.css: {} + css/kontakt.css: {} diff --git a/web/themes/custom/barbell/barbell.svg b/web/themes/custom/barbell/barbell.svg new file mode 100644 index 0000000..f3619e5 --- /dev/null +++ b/web/themes/custom/barbell/barbell.svg @@ -0,0 +1,4574 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/web/themes/custom/barbell/css/blog.css b/web/themes/custom/barbell/css/blog.css index 03185f4..1a5222f 100644 --- a/web/themes/custom/barbell/css/blog.css +++ b/web/themes/custom/barbell/css/blog.css @@ -9,7 +9,6 @@ margin-bottom: 120px; } - /* NO-SPAN TAGGED TITLES and YELLOW STRIPES FOR NO-SPAN TAGGED TITLES */ .path-blog h1.page-title, .path-vesti h1.page-title, @@ -27,25 +26,32 @@ content: url("../img/yellow-stripes-78.png"); position: absolute; z-index: -1; + top: 6px; + left: -31px; +} + +.path-tim .page-title:before { bottom: -16px; - left: -30px; - color: red; + left: -34px; } .path-blog #block-barbell-content, .path-vesti #block-barbell-content { - margin: 100px; + margin: 100px 0 150px; } + @media only screen and (min-width: 320px) and (max-width: 700px) { .path-blog #block-barbell-content, .path-vesti #block-barbell-content { - margin: 20px; + margin: 7px; } } + .path-blog div .layout-container main .layout-content, .path-vesti div .layout-container main .layout-content { margin: 0 7%; } + @media only screen and (min-width: 320px) and (max-width: 700px) { .path-blog div .layout-container main .layout-content, .path-vesti div .layout-container main .layout-content { @@ -53,36 +59,97 @@ } } -.path-blog .image-style-medium, -.path-vesti .image-style-medium { - padding-top: 100%!important; -} - -.path-blog div .layout-container main .layout-content div #block-barbell-content div div .view-content, -.path-vesti div .layout-container main .layout-content div #block-barbell-content div div .view-content { +.path-blog + div + .layout-container + main + .layout-content + div + #block-barbell-content + div + div + .view-content, +.path-vesti + div + .layout-container + main + .layout-content + div + #block-barbell-content + div + div + .view-content { display: grid; grid-template-columns: repeat(4, 1fr); grid-gap: 20px; justify-content: space-between; } -@media only screen and (min-width: 320px) and (max-width: 1100px) { - .path-blog div .layout-container main .layout-content div #block-barbell-content div div .view-content, - .path-vesti div .layout-container main .layout-content div #block-barbell-content div div .view-content { + +@media only screen and (max-width: 1100px) { + .path-blog + div + .layout-container + main + .layout-content + div + #block-barbell-content + div + div + .view-content, + .path-vesti + div + .layout-container + main + .layout-content + div + #block-barbell-content + div + div + .view-content { grid-template-columns: repeat(3, 1fr); + margin: 0 2vw 7em; + } + + .path-blog #block-barbell-content, + .path-vesti #block-barbell-content { + margin: 0; + } + + .path-blog .article-thumb .article-info-wrapper .article-title span, + .path-vesti .article-thumb .article-info-wrapper .article-title span { + font-size: 1.7rem; + line-height: 24px; } } -@media only screen and (min-width: 320px) and (max-width: 900px) { - .path-blog div .layout-container main .layout-content div #block-barbell-content div div .view-content, - .path-vesti div .layout-container main .layout-content div #block-barbell-content div div .view-content { +@media only screen and (max-width: 576px) { + .path-blog + div + .layout-container + main + .layout-content + div + #block-barbell-content + div + div + .view-content, + .path-vesti + div + .layout-container + main + .layout-content + div + #block-barbell-content + div + div + .view-content { grid-template-columns: repeat(2, 1fr); + margin: 0 2px; } -} -@media only screen and (min-width: 320px) and (max-width: 700px) { - .path-blog div .layout-container main .layout-content div #block-barbell-content div div .view-content, - .path-vesti div .layout-container main .layout-content div #block-barbell-content div div .view-content { - grid-template-columns: 1fr; + .path-blog #block-barbell-content, + .path-vesti #block-barbell-content { + margin-bottom: 100px !important; } } @@ -94,51 +161,59 @@ .path-blog .views-row, .path-vesti .views-row { border: 2px solid var(--yellow); + position: relative; + padding-bottom: 40px; } - - .article-thumb a { text-decoration: none; color: black; font-family: "Brandon Grotesque Medium"; } -.path-blog .article-thumb a:hover .article-info-wrapper .article-title, -.path-vesti .article-thumb a:hover .article-info-wrapper .article-title { +.path-blog .views-row:hover .article-title span, +.path-vesti .views-row:hover .article-title span { color: whitesmoke; } /* Article title */ .path-blog .article-thumb .article-info-wrapper .article-title span, .path-vesti .article-thumb .article-info-wrapper .article-title span { - font-size: 4.65vmin; + font-size: 30px; + line-height: 30px; + display: inline-block; + width: 100%; } -@media only screen and (min-width: 320px) and (max-width: 1500px) { + +@media only screen and (min-width: 1000px) and (max-width: 1500px) { .path-blog .article-thumb .article-info-wrapper .article-title span, .path-vesti .article-thumb .article-info-wrapper .article-title span { - font-size: 4vmin; + font-size: 28px; } -} -@media only screen and (min-width: 320px) and (max-width: 1000px) { +} + +@media only screen and (min-width: 768px) and (max-width: 1000px) { .path-blog .article-thumb .article-info-wrapper .article-title span, .path-vesti .article-thumb .article-info-wrapper .article-title span { - font-size: 3vmin; + font-size: 26px; } -} +} -@media only screen and (min-width: 320px) and (max-width: 700px) { +@media only screen and (min-width: 320px) and (max-width: 768px) { .path-blog .article-thumb .article-info-wrapper .article-title span, .path-vesti .article-thumb .article-info-wrapper .article-title span { - font-size: 6vmin; + font-size: 19px; + line-height: 24px; } -} -.article-image { - background: dimgray; + .article-info-wrapper .article-date div time { + font-size: 16px; + } } .article-image div img { width: 100%; + display: inline-block; + height: auto; } .article-info-wrapper { @@ -147,5 +222,115 @@ .article-info-wrapper .article-date div time { color: var(--gray); - font-size: 30px; + font-size: 24px; + position: absolute; + bottom: 8px; + left: 50%; + transform: translate(-50%); +} + +@media only screen and (max-width: 1200px) { + .article-info-wrapper .article-date div time { + font-size: 23px; + } +} + +@media only screen and (max-width: 992px) { + .article-info-wrapper .article-date div time { + font-size: 21px; + } +} + +.pager__items { + padding-left: 0; + position: relative; + margin: 35px 0; +} + +.pager__items .pager__item--next, +.pager__items .pager__item--previous { + text-transform: uppercase; +} + +.pager__items .pager__item--previous { + position: absolute; + top: -30px; + left: 0; +} + +.pager__items .pager__item--next { + position: absolute; + top: -23px; + right: 0; +} +.pager__items .is-active { + position: absolute; + left: 50%; + transform: translate(-50%); + font-size: 18px; +} + +.pager__items a { + text-decoration: none; + color: black; +} + +.next_arrow, +.previous_arrow { + font-size: 45px; + position: relative; + top: 5px; +} + +.next_txt, +.previous_txt { + font-size: 16px; +} + +@media only screen and (max-width: 1100px) { + .next_txt, + .previous_txt { + display: none; + } +} + +@media only screen and (min-width: 577px) and (max-width: 1100px) { + .pager__items { + top: -80px; + } +} + +/* ///////////////// */ + +@media only screen and (min-width: 768px) and (max-width: 992px) { + .path-vesti .page-title, + .path-blog .page-title { + /* margin: 0; */ + font-size: 45px; + } +} + +@media only screen and (max-width: 768px) { + .path-vesti .page-title, + .path-blog .page-title { + /* margin: 0; */ + font-size: 32px; + } + + .path-tim .page-title:before, + .path-vesti .page-title:before, + .path-blog .page-title:before { + content: url(../img/page-title-bcg-sm.png) !important; + top: 2px !important; + left: -18px !important; + } + + .path-tim .page-title:before { + top: -1px !important; + left: -24px !important; + } + + .path-tim .page-title { + font-size: 31px; + } } diff --git a/web/themes/custom/barbell/css/brandon-grotesque-bold-587bd6400afd0.woff b/web/themes/custom/barbell/css/brandon-grotesque-bold-587bd6400afd0.woff new file mode 100644 index 0000000..566d28f Binary files /dev/null and b/web/themes/custom/barbell/css/brandon-grotesque-bold-587bd6400afd0.woff differ diff --git a/web/themes/custom/barbell/css/brandon-grotesque-medium-cufonfonts-webfont.zip b/web/themes/custom/barbell/css/brandon-grotesque-medium-cufonfonts-webfont.zip new file mode 100644 index 0000000..a0bd0dd Binary files /dev/null and b/web/themes/custom/barbell/css/brandon-grotesque-medium-cufonfonts-webfont.zip differ diff --git a/web/themes/custom/barbell/css/frontPage.css b/web/themes/custom/barbell/css/frontPage.css index a9d2b1c..1a37828 100644 --- a/web/themes/custom/barbell/css/frontPage.css +++ b/web/themes/custom/barbell/css/frontPage.css @@ -1,18 +1,20 @@ /* POČETNA */ -/* slider */ -#hero-section { - background: black; /* where the image should be just for visibility sake */ +.front-slider, +.slider-image { + height: calc(100vh - 75px); } -/* .slider-link div div a { - text-transform: uppercase; - text-decoration: none; -} */ +@media only screen and (max-width: 768px) { + .front-slider, + .slider-image { + height: calc(80vh - 75px); + } +} -/* DEVELOPMENTAL */ -.slide--1 { - display: none; +.slider-content { + display: flex; + flex-direction: column; } .slide @@ -25,10 +27,18 @@ border: none; } +.slider-image { + background-repeat: no-repeat; + background-size: cover; + background-position: center; + height: 100%; +} + #slick-views-front-slider-block-1-1-slider { - padding-top: 1px; - margin-top: -1px; - height: calc(100vh - 75px); + /* padding-top: 1px; */ + /* margin-top: -1px; */ + /*height: calc(100vh - 73px);*/ + position: relative; } h2 span:before { @@ -36,13 +46,13 @@ h2 span:before { position: absolute; left: -29px; z-index: -1; - bottom: -16px; + top: 6px; } @media only screen and (max-width: 768px) { h2 span:before { content: url("../img/page-title-bcg.png"); - bottom: -15px; + top: 0; left: -25px; } } @@ -50,6 +60,8 @@ h2 span:before { h2 { text-align: center; margin: 32vh auto 20px; + display: inline-block; + min-width: 250px; } h2 span { @@ -58,10 +70,10 @@ h2 span { color: whitesmoke; margin: 120px; font-size: 64px; - left: 20px; position: relative; font-family: "Brandon Grotesque Bold", sans-serif; } + @media only screen and (max-width: 768px) { h2 span { margin: 0; @@ -76,6 +88,7 @@ h2 span { margin: 25px auto; font-size: 30px; } + @media only screen and (max-width: 768px) { .slider-desc div p { width: 90vw; @@ -90,11 +103,17 @@ h2 span { .slide .front-slider .slider-image .slider-content .slider-link div { background: transparent !important; - border: 3px solid var(--yellow); + /* border: 3px solid var(--yellow); */ text-align: center; padding: 10px 53px; } +.slide .front-slider .slider-image .slider-content .slider-link div a { + border: 3px solid var(--yellow); + border-radius: 45px; + padding: 0.4em 2em; +} + @media only screen and (max-width: 768px) { .slide .front-slider .slider-image .slider-content .slider-link div { padding: 10px 20px; @@ -135,14 +154,31 @@ h2 span { } #after_content div div h2:before { - bottom: -15px; - left: -22px; + top: 4px; + left: -25px; } -@media only screen and (max-width: 768px) { +@media only screen and (min-width: 551px) and (max-width: 768px) { .path-frontpage #after_content div div h2 { /* margin: 0; */ - font-size: 45px; + font-size: 35px; + } + + #after_content div div h2:before { + top: -1px; + left: 1px; + } +} + +@media only screen and (max-width: 551px) { + .path-frontpage #after_content div div h2 { + /* margin: 0; */ + font-size: 32px; + } + + #after_content div div h2:before { + top: 3px; + left: 24px; } } @@ -167,9 +203,11 @@ h2 span { grid-column: 1/2; } -.view-content .views-row:hover { - border: 2px solid black; - background: black; +.path-frontpage .view-content .views-row:hover, +.path-blog .view-content .views-row:hover, +.path-vesti .view-content .views-row:hover { + border: 2px solid var(--black); + background: var(--black); } .view-content .views-row:hover .program-link { @@ -205,6 +243,9 @@ h2 span { display: block; border-bottom: 3px solid var(--yellow); width: 70px; +} + +.path-frontpage .program-description div:before { margin: 20px auto; } @@ -212,24 +253,30 @@ h2 span { text-align: center; padding-bottom: 10px; } - -.slick-prev, -.slick-next { - content: " " !important; - background: var(--yellow); - visibility: hidden; +.slick-dots { + width: 100vw; + text-align: center; + position: absolute; + bottom: 0; + padding-left: 0; +} +.slick-dots li { + display: inline-block; + text-align: center; } -.slick-prev:before, -.slick-next:before { - content: " "; - background: var(--yellow); +.slick-dots button { + text-indent: -9999px; width: 20px; height: 20px; - border-radius: 50%; - z-index: 10; - visibility: visible; - display: inline-block; + border-radius: 50px; + margin: 0 7px; + background: var(--black); + border: var(--black); +} +.slick-active button { + background: var(--yellow); + border: var(--yellow); } /* PROGRAM BOXES */ @@ -343,5 +390,255 @@ h2 span { margin: 10px auto; } } - /* // PROGRAM BOXES ENDS */ + +.hamburgerHolder { + display: none !important; +} + +/* NAV RESPO */ +@media only screen and (max-width: 1500px) { + #block-barbell-main-menu ul li { + margin-right: 10px; + } +} + +@media only screen and (min-width: 320px) and (max-width: 1365px) { + header div { + grid-template-columns: 415px auto; + width: 100vw; + justify-content: space-between; + } + + #block-barbell-branding a img { + margin-right: -15px; + } + + .language-switcher-language-url { + display: flex; + justify-content: flex-end; + width: 100%; + height: 98%; + } + + #block-languageswitcher ul { + padding-left: 0px; + } + + .en:after { + left: 48px; + } + + .hamburgerHolder { + display: inline-block !important; + width: 60px; + padding: 9px 0; + position: relative; + top: -1px; + margin-right: 15px; + } + + .hamburgerHolder:hover { + cursor: pointer; + } + + #block-languageswitcher ul { + right: 15px; + } + + .hamburgerHolder img { + width: 100%; + height: auto; + } + + /* NAV */ + header div nav { + position: fixed; + top: -100vh; + display: flex; + justify-content: center; + align-items: center; + width: 100vw; + height: 100vh; + background: #171717; + z-index: 9999; + transition: all 0.5s ease-out; + } + + header div nav ul { + display: block; + height: 80%; + } + + #block-barbell-main-menu ul li { + display: block; + margin: 15px 0; + text-align: center; + } + + #block-barbell-main-menu ul li a { + color: whitesmoke; + font-size: 27px; + font-family: "roboto", sans-serif; + font-weight: 100; + } + + /* X-CLOSE */ + + .xHolder { + display: block; + width: 39px; + height: 40px; + position: absolute; + background: #171717; + top: 40px; + right: 45px; + border: none; + transition: all 0.4s ease-in; + } + + .xPara:hover { + cursor: pointer; + } + + .xPara { + position: relative; + top: -1px; + width: 100%; + height: 100%; + text-align: center; + color: whitesmoke; + margin: 0; + font-size: 1px; + opacity: 0; + transition: all 0.4s ease-in; + } + + /* X-CLOSE ENDS*/ +} + +@media only screen and (max-width: 600px) { + header div { + grid-template-columns: 65% auto; + /* height: 65px; */ + } + + .hamburgerHolder { + width: 46px; + padding: 11px 0; + margin-right: 15px; + } + + .language-switcher-language-url { + height: 98%; + } + + #block-languageswitcher ul li { + margin: 0 5px; + font-size: 16px; + } + + .en:after { + left: 40px; + top: 0; + } + + #block-barbell-branding { + text-align: center; + } + + #block-barbell-branding a img { + width: 270px; + margin-left: 10px; + } + + .xHolder { + top: 20px; + right: 25px; + } +} + +@media only screen and (max-width: 400px) { + .hamburgerHolder { + width: 46px; + margin-right: 17px; + } + + #block-languageswitcher ul { + right: 5px; + } + + #block-languageswitcher ul li { + margin: 0 6px 0 0; + font-size: 14px; + } + + .en:after { + left: 30px; + top: 0; + font-size: 17px; + } + + #block-barbell-branding a img { + width: 217px; + margin-left: 7px; + } +} + +@media only screen and (max-width: 480px) and (orientation: landscape) { + header div nav ul { + margin-top: -40px; + } + + main-menu ul li { + margin: 10px 0; + } + + #block-barbell-main-menu ul li a { + font-size: 20px; + } + + #slick-views-front-slider-block-1-1-slider { + height: auto; + position: relative; + } + + .slider-content h2 { + margin: 7vh auto 20px; + font-size: 40px; + } + + .slider-content h2 span { + font-size: 40px; + } + + .path-frontpage .field--name-body p { + margin: 0 20px; + font-size: 19px; + } + + .slide .front-slider .slider-image .slider-content .slider-link div { + padding: 3px 9px; + } + + .path-frontpage .field--name-field-link-button, + #block-zakazitetrening div p { + margin: 0 auto 50px; + } + + #slick-views-front-slider-block-1-1-slider { + height: calc(100vh - 74px); + } + + .slick-dots button { + width: 16px; + height: 16px; + } +} + +/* NAV RESPO ENDS */ + +/* TEMPORARY */ +.xHolder { + display: none; +} diff --git a/web/themes/custom/barbell/css/funkcTreninzi.css b/web/themes/custom/barbell/css/funkcTreninzi.css index c61a3a7..36f0f68 100644 --- a/web/themes/custom/barbell/css/funkcTreninzi.css +++ b/web/themes/custom/barbell/css/funkcTreninzi.css @@ -1,11 +1,9 @@ /* INDIVIDUALNI TRENINZI IMG right */ .field--name-body p .align-right { margin-left: 70px; - background: lightblue; } /* INDIVIDUALNI TRENINZI IMG left */ .field--name-body p .align-left { margin-right: 18px; - background: lightblue; } diff --git a/web/themes/custom/barbell/css/galerija.css b/web/themes/custom/barbell/css/galerija.css new file mode 100644 index 0000000..35449ec --- /dev/null +++ b/web/themes/custom/barbell/css/galerija.css @@ -0,0 +1,35 @@ +.page-node-type-galerija #block-barbell-content { + width: 100%; + margin-bottom: 150px; +} + +[class*="block-column-"], +.item-list > [class*="block-column-"] { + column-gap: 0; +} + +[class*="block-column-"] > .grid, +.item-list > [class*="block-column-"] > .grid { + margin-bottom: -8px !important; +} + +.blazy img { + width: 100%; + height: auto; + background: lightblue; +} + +.page-node-type-galerija .blazy li, +.page-node-type-galerija .blazy div, +.page-node-type-galerija .blazy a { + display: inline-block !important; + width: 100% !important; +} + +.blazy a:hover { + cursor: pointer; +} + +/* .slick-lightbox-close:before { + font-size: 50px; +} diff --git a/web/themes/custom/barbell/css/kontakt.css b/web/themes/custom/barbell/css/kontakt.css new file mode 100644 index 0000000..5274d17 --- /dev/null +++ b/web/themes/custom/barbell/css/kontakt.css @@ -0,0 +1,83 @@ +form { + display: flex; + flex-direction: column; + margin-left: 20%; + margin-right: 20%; + margin-bottom: 200px; +} + +.field__label { + visibility: hidden; +} + +label, +input { + border: 1px solid black; + height: 80px; +} + +label { + display: block; + font-family: "Brandon Grotesque Medium"; + font-size: 25px; + border-bottom: none; + padding: 21px 30px; +} + +input { + width: 100%; + font-size: 19px; + padding: 29px 30px; +} + +textarea { + height: 250px; + padding: 30px; + border: 1px solid black; +} + +input, +textarea { + font-size: 19px; +} + +.form-actions { + text-align: center; +} + +.button { + width: 300px; + height: 60px; + margin: 0 auto; + padding: 16px; + font-size: 25px; + border-radius: 7px; + background: var(--yellow); + transition: all 0.2s; +} + +.button:hover { + background: darkorange; +} + +@media only screen and (max-width: 992px) { + form { + margin-left: 7%; + margin-right: 7%; + } +} + +/* @media only screen and (max-width: 992px) { + form { + margin-left: 7%; + margin-right: 7%; + } +} */ + +@media only screen and (max-width: 415px) { + form div:nth-child(1) label, + form div:nth-child(2) label { + height: 100px; + line-height: 29px; + } +} diff --git a/web/themes/custom/barbell/css/pojedine_strane.css b/web/themes/custom/barbell/css/pojedine_strane.css new file mode 100644 index 0000000..fa17e7c --- /dev/null +++ b/web/themes/custom/barbell/css/pojedine_strane.css @@ -0,0 +1,180 @@ +.page-node-type-article article img { + width: 100%; + height: auto; + margin: 1em 0; +} + +@media only screen and (min-width: 1200px) { + .page-node-type-article article img { + margin: 1em 0 4em; + } +} + +.page-node-type-article #block-zakazitetrening { + padding-top: 5em; +} + +.align-left { + width: 100%; + height: auto; + padding-bottom: 1.5em; +} + +blockquote p { + text-transform: uppercase; + font-family: "Henry", sans-serif !important; + text-align: center !important; + margin: 80px 0 0 !important; +} + +blockquote p:before, +blockquote p:after { + content: '"'; +} + +@media only screen and (max-width: 360px) { + .page-node-type-article #block-zakazitetrening p { + margin: 2em 0; + } + + .page-node-type-program .field--name-field-link-button div, + .page-node-type-tim .field--name-field-link-button div, + .page-node-type-article #block-zakazitetrening p { + font-size: 16px; + text-align: center; + padding: 1.2em 1em; + } + + .field--name-field-link-button { + padding: 0; + } + + /* .page-node-type-tim .field--name-field-link-button div { + padding: 0; + margin: 1em 2em; + } */ +} + +@media only screen and (max-width: 480px) { + blockquote p { + font-size: 26px !important; + } +} + +@media only screen and (min-width: 480px) { + blockquote p { + font-size: 28px !important; + } +} + +@media only screen and (min-width: 576px) { + blockquote p { + font-size: 36px !important; + } +} +/* .page-node-type-tim #block-barbell-content, +.page-node-type-program #block-barbell-content, +.page-node-type-article #block-barbell-content, +.page-node-type-tim p { + padding: 0 1em; +} */ + +.page-node-type-article p { + margin: 0; +} + +.page-node-type-article .node__content .field__item, +.page-node-type-tim .field__item, +.page-node-type-program .field__item { + padding: 0 2em; +} + +@media only screen and (max-width: 768px) { + .page-node-type-article .node__content .field__item, + .page-node-type-tim .field__item, + .page-node-type-program .field__item { + padding: 0; + } +} + +@media only screen and (min-width: 1200px) { + .page-node-type-tim #block-barbell-content, + .page-node-type-program #block-barbell-content, + .page-node-type-article #block-barbell-content, + .page-node-type-tim p { + margin: 0; + } + + .align-rigth { + width: 50%; + float: right; + position: relative; + bottom: -10px; + padding: 0; + } + + .align-left { + width: 50%; + float: left; + position: relative; + bottom: -10px; + padding: 0; + } +} + +@media only screen and (max-width: 992px) { + .page-node-type-tim #block-barbell-content, + .page-node-type-program #block-barbell-content, + .page-node-type-article #block-barbell-content { + padding: 1em; + } + + .page-node-type-tim #block-barbell-content p, + .page-node-type-program #block-barbell-content p, + .page-node-type-article #block-barbell-content div { + margin: 0; + /* padding: 1em; */ + } + .page-node-type-article #block-barbell-content p { + padding: 0; + margin: 0; + } +} + +@media only screen and (min-width: 1200px) { + .page-node-type-tim #block-barbell-content, + .page-node-type-program #block-barbell-content, + .page-node-type-article #block-barbell-content { + padding: 0 10em; + } + + .page-node-type-tim #block-barbell-content p { + font-size: 38px; + /* margin: 1.5em 220px 2em; */ + line-height: 48px; + } + + .page-node-type-tim #block-barbell-content blockquote p { + font-size: 48px !important; + line-height: 70px; + } +} + +@media only screen and (min-width: 1600px) { + .page-node-type-tim #block-barbell-content, + .page-node-type-program #block-barbell-content, + .page-node-type-article #block-barbell-content { + padding: 0 15em; + } + + .page-node-type-tim #block-barbell-content blockquote p { + font-size: 74px !important; + margin: 80px 0 0 !important; + line-height: 70px !important; + } + + .page-node-type-article #block-barbell-content p { + /* width: 1500px; */ + margin: -1em auto 2em; + } +} diff --git a/web/themes/custom/barbell/css/programi.css b/web/themes/custom/barbell/css/programi.css index 216d944..0a65c7f 100644 --- a/web/themes/custom/barbell/css/programi.css +++ b/web/themes/custom/barbell/css/programi.css @@ -1,92 +1,239 @@ -:root { - --each: 400px; /* širina celog jednog program boxa */ - --oneHalf: 200px; /* širina slike / texta unutar jednog boxa */ - --text: 350px; +/* Basic layout of one box */ + +.path-programi .program-link { + display: grid; + grid-template-columns: repeat(2, 50%); + height: 100%; + width: 95vw; + margin: 3em auto; + color: black; + text-decoration: none; +} + +/* OKRETANJE PARNIH PROGRAMA NA DRUGU STRANU */ +.path-programi + .view-content + .views-row:nth-child(even) + .program-thumb + .program-link + .program-image { + grid-column: 2/3; + grid-row: 1/2; + +} + +.path-programi + .view-content + .views-row + .program-thumb + .program-link + .program-image { +background-size: cover; } - .path-programi .program-title span { - font-family: "Henry"; - font-size: 26px; - text-transform: uppercase; +/* OUTER INFO WRAPPER */ +.path-programi .views-row .program-info-wrapper { + width: 160%; + z-index: 5; + background: var(--yellow); + padding: 3px; +} + +.path-programi .views-row:nth-child(odd) .program-info-wrapper { + position: relative; + left: -60%; + clip-path: polygon(37% 0, 100% 0, 100% 100%, 0 100%); + text-align: right; +} + +.path-programi .views-row:nth-child(even) .program-info-wrapper { + clip-path: polygon(0 0, 63% 0, 100% 102%, 0 100%); +} + +.path-programi .program-link:hover .program-info-wrapper, +.path-programi .program-link:hover .program-info-wrapper-mini { + background: var(--black); +} + +.path-programi .views-row:hover .program-description, +.path-programi .views-row:hover .program-title { + color: black; +} +.path-programi .program-link:hover .program-description, +.path-programi .program-link:hover .program-title { + color: whitesmoke; +} + +/* INNER INFO WRAPPER */ +.path-programi .views-row .program-info-wrapper-mini { + background: whitesmoke; + padding: 0.3em; + + font-family: "Brandon Grotesque Medium"; +} + +.path-programi .views-row .program-info-wrapper-mini { + height: 100%; +} + +@media only screen and (min-width: 624px) and (max-width: 848px) { + .path-programi .view-content .program-thumb { + height: 275px; + margin-bottom: 7em !important; + } +} + +@media only screen and (min-width: 430px) and (max-width: 624px) { + .path-programi .view-content .program-thumb { + height: 230px; + margin-bottom: 7em !important; } +} + +.path-programi .views-row:nth-child(odd) .program-info-wrapper-mini { + clip-path: polygon(37.5% 0, 100% 0, 100% 100%, 1.3% 100%); + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.path-programi .views-row:nth-child(even) .program-info-wrapper-mini { + clip-path: polygon(0 0, 62.5% 0, 98.7% 101%, 0 100%); +} + +.path-programi .program-title { + width: 65%; +} + +.path-programi .program-title span { + font-family: "Henry"; + font-size: 4vw; + text-transform: uppercase; + display: inline-block; +} + +.path-programi .program-description { + font-size: 4vw; +} + +.path-programi .program-description div { + align-self: end; +} +.path-programi .views-row:nth-child(odd) .program-description div:before { + margin: 1em 1em 1em auto; +} + +.path-programi .views-row:nth-child(even) .program-description div:before { + margin: 1em auto 1em 1em; +} + +.path-programi .program-description { + font-size: 1.2rem; + width: 80%; +} + +.path-programi .views-row:nth-child(odd) .program-description { + display: flex; + flex-direction: column; + align-self: flex-end; +} + +/* MEDIA QUERIES */ +@media only screen and (min-width: 27em) { .path-programi .program-link { - color: black; - text-decoration: none; + width: 25em; } - /* spoljni grid koji drži po dva programa u jednom redu */ - .path-programi .view-content { - display: grid; - grid-template-columns: var(--each) var(--each); - justify-content: space-around; + .path-programi .program-title { + width: 69%; } - .path-programi .view-content .views-row { - height: 233px; - overflow: hidden; + .path-programi .program-title span { + font-size: 1.5em; } - /* div pojedinog programa */ - .path-programi .view-content .views-row .program-thumb { - margin: 0; - height: 100%; + .path-programi .program-description { + font-size: 1.2rem; } - .path-programi .view-content .views-row .program-thumb .program-link { - display: grid; - grid-template-columns: var(--oneHalf) var(--oneHalf); - height: 100%; + .path-programi .views-row .program-info-wrapper-mini { + padding: 1em 0.7em; } - /* .path-programi .view-content .views-row:nth-child(odd) .program-thumb .program-link .program-info-wrapper { - position: absolute; - border: 3px solid var(--yellow); - height: 100%; - } */ - /* LEVA SLIKA */ - .path-programi .view-content .views-row:nth-child(odd) .program-thumb .program-link .program-image { - clip-path: polygon(0 0, 100% 0%, 15% 100%, 0% 100%); - background: var(--yellow); - + .path-programi .views-row:nth-child(odd) .program-info-wrapper-mini { + clip-path: polygon(37.5% 0, 100% 0, 100% 100%, 1% 100%); } - .path-programi .view-content .views-row:nth-child(odd) .program-thumb .program-link .program-image img { - overflow: hidden; +} + +@media only screen and (min-width: 39em) { + /*608*/ + .path-programi .program-link { + width: 37.5em; } -/* LEVI TEXT */ -.path-programi .view-content .views-row:nth-child(odd) .program-thumb .program-link .program-title { - position: relative; - right: 10px; - padding: 10px; + .path-programi .program-title span { + font-size: 2.375em; + } + + .path-programi .program-description { + font-size: 1.6rem; + } } -/* DESNI TEXT */ -.path-programi .view-content .views-row:nth-child(even) .program-thumb .program-link .program-title { - /* position: relative; - right: 10px; */ - padding: 10px; +@media only screen and (min-width: 53em) { + /*832*/ + .path-programi .program-link { + width: 25em; + } + + .path-programi .view-content { + display: grid; + grid-template-columns: 1fr 1fr; + justify-content: space-around; + margin-bottom: 190px; + } + + .path-programi .views-row { + height: 13em; + margin-bottom: 3em; + } + + .path-programi .program-thumb, + .path-programi .program-thumb .program-info-wrapper-mini { + height: 100%; + } + + .path-programi .program-title span, + .path-programi .program-description { + font-size: 1.2rem; + } } - /* OKRETANJE PARNIH PROGRAMA NA DRUGU STRANU */ - .path-programi .view-content .views-row:nth-child(even) .program-thumb .program-link .program-image { - background: var(--yellow); - grid-column: 2/3; - clip-path: polygon(0 0, 100% 0%, 100% 100%, 85% 100%); +@media only screen and (min-width: 76em) { + /*1216*/ + .path-programi .program-link { + width: 37.5em; + } + + .path-programi .program-title span { + font-size: 2.375em; } - .path-programi .view-content .views-row:nth-child(even) .program-thumb .program-link .program-info-wrapper { - grid-column: 1/2; - grid-row: 1/2; + .path-programi .program-description { + font-size: 1.6rem; } - .path-programi .view-content .views-row { - border: 3px solid transparent; - color: black; + .path-programi .views-row { + height: 20em; + margin-bottom: 5em; } - .path-programi .view-content .views-row:hover { - backgrond: black; - border: 3px solid transparent!important; - /*background: transparent;*/ - } \ No newline at end of file + .path-programi .views-row:nth-child(odd) .program-info-wrapper-mini { + clip-path: polygon(37.3% 0, 100% 0, 100% 100%, 0.5% 100%); + } + + .path-programi .views-row:nth-child(even) .program-info-wrapper-mini { + clip-path: polygon(0 0, 62.7% 0, 99% 101%, 0 100%); + } +} diff --git a/web/themes/custom/barbell/css/raspored.css b/web/themes/custom/barbell/css/raspored.css new file mode 100644 index 0000000..16bbc83 --- /dev/null +++ b/web/themes/custom/barbell/css/raspored.css @@ -0,0 +1,325 @@ +.node__content > div:nth-child(1) > table:nth-child(1) { + width: 85% !important; + margin: 0 auto 50px; + border-spacing: 11px; + border-collapse: separate; + border-color: transparent; +} + +/* tbody tr { + height: 170px !important; +} */ + +td, +th { + border-color: transparent; + padding: 0; + text-align: center; +} + +td div { + text-align: center; + padding: 7px; +} + +th { + background-color: var(--yellow); +} + +tr td { + position: relative; + height: 100%; + width: 14.3%; + /* background: #e3e3e3; */ + background-image: url("../img/empty-raspored.png"); + background-size: cover; + background-repeat: round; + border: none; +} + +thead > tr { + height: 40px; + overflow-y: hidden; +} + +thead > tr > th:nth-child(1n + 2) { + width: 200px; + min-width: 190px !important; + padding: 10px; + display: table-cell; + height: 20px !important; + font-family: "Brandon Grotesque Medium"; +} + +thead tr th:nth-child(1) { + width: 77px !important; + height: 58px !important; +} + +tr > th, +thead > tr > th:first-child { + width: 77px; + height: 100%; + margin: 0; + font-size: 1.6rem; + display: inline-block; +} + +thead tr th:first-child { + width: 77px; + padding: 0 45%; +} + +tr > th { + padding: 90% 5px; +} + +.rasporedText { + font-family: "Brandon Grotesque Bold" !important; + text-align: center !important; + font-size: 21px !important; + margin: 0 !important; + vertical-align: middle; + line-height: 22px; +} + +.rasporedImgWrapper { + height: 66.6%; + display: flex; + justify-content: center; + align-items: center; +} + +.rasporedImgWrapper img { + height: auto; + width: 100px; +} + +/* .raspored-as img { + width: 70px; +} */ + +@media only screen and (max-width: 1580px) { + thead > tr > th:nth-child(1n + 2) { + min-width: 150px !important; + } + + .rasporedText { + font-size: 15px !important; + margin: 0 !important; + padding: 0 10px !important; + } + + tr > th, + thead > tr > th:first-child { + width: 77px; + margin: 0; + font-size: 1.1rem; + } + + /* .raspored-as img { + width: 60px; + } */ +} + +@media only screen and (max-width: 1300px) { + thead > tr > th:nth-child(1n + 2) { + min-width: 100px !important; + } + + tr > th, + thead > tr > th:first-child { + width: 48px; + margin: 0; + font-size: 0.7rem; + } + + thead tr th:nth-child(1) { + width: 48px !important; + height: 48px !important; + } + + .rasporedText { + font-size: 10px !important; + margin: 0 !important; + padding: 0 0px !important; + line-height: 13px; + } + + .rasporedImgWrapper img { + height: auto; + width: 50px; + } + + /* .raspored-as img { + width: 35px; + } */ +} + +@media only screen and (max-width: 880px) { + thead > tr > th:nth-child(1n + 2) { + min-width: 85px !important; + } + + tr > th, + thead > tr > th:first-child { + width: 44px; + margin: 0; + font-size: 0.6rem; + } + + thead tr th:nth-child(1) { + width: 44px !important; + height: 44px !important; + } + + .rasporedText { + font-size: 10px !important; + margin: 0 !important; + padding: 0 0px !important; + line-height: 13px; + } + + .rasporedImgWrapper img { + height: auto; + width: 50px; + } + + /* .raspored-as img { + width: 35px; + } */ +} + +@media only screen and (max-width: 768px) { + .node__content > div:nth-child(1) > table:nth-child(1) { + width: 97% !important; + } + + .rasporedAllWrapper { + display: flex; + flex-direction: column; + } + + .rasporedAllWrapper table { + order: 2; + } + + .legendaWrapper ul { + list-style: none; + padding-left: 10px; + } + + .legendaWrapper ul li { + display: flex; + flex-direction: row; + align-items: center; + } + + .legendaWrapper img { + width: 55px; + margin: 13px; + } + + .legendaWrapper span { + font-size: 15px; + } + + .legendaWrapper ul li:last-of-type img { + width: 44px; + } + + .legendaWrapper ul li:last-of-type span { + position: relative; + left: 12px; + } + + tbody tr { + height: 0 !important; + } + + .rasporedTitleHolder { + display: none; + } + + .rasporedImgWrapper { + height: 100%; + display: flex; + justify-content: center; + align-items: center; + } + + thead > tr > th:nth-child(1n + 2) { + min-width: 30px !important; + } + + td div { + text-align: center; + padding: 2px; + } + + tr > th { + padding: 40% 5px; + } + + .rasporedImgWrapper img { + width: 35px; + } + + /* .raspored-as img { + width: 24px; + } */ +} + +@media only screen and (max-width: 430px) { + .legendaWrapper ul { + padding-left: 10px; + } + + .legendaWrapper img { + width: 47px; + margin: 7px; + } + + .legendaWrapper ul li:last-of-type img { + width: 38px; + } + + .legendaWrapper span { + font-size: 14px; + } + + .node__content > div:nth-child(1) > table:nth-child(1) { + width: 95% !important; + } + + thead > tr > th:nth-child(1n + 2) { + padding: 0px; + min-width: 30px !important; + } + + tr > th, + thead > tr > th:first-child { + width: 40px; + margin: 0; + font-size: 0.7rem; + } + + thead tr th:nth-child(1) { + width: 40px !important; + height: 40px !important; + } + + .rasporedImgWrapper img { + width: 28px; + } + + /* .raspored-as img { + width: 19px; + } */ +} + +@media only screen and (max-width: 390px) { + .node__content > div:nth-child(1) > table:nth-child(1) { + border-spacing: 5px; + } +} diff --git a/web/themes/custom/barbell/css/social.css b/web/themes/custom/barbell/css/social.css index e45fc0e..82e8ee4 100644 --- a/web/themes/custom/barbell/css/social.css +++ b/web/themes/custom/barbell/css/social.css @@ -18,7 +18,7 @@ display: inline; } -@media only screen and (max-width: 768px) { +@media only screen and (max-width: 992px) { #block-mainnavigation ul li { display: block; padding-left: 35px; @@ -26,6 +26,10 @@ #block-mainnavigation ul { margin: 20px; } + + .region-footer { + height: unset; + } } @media only screen and (max-width: 992px) { @@ -44,6 +48,12 @@ } #block-sociallinks div a { - margin: 0 4px; + margin: 4px 4px; + } +} + +@media only screen and (max-width: 576px) { + #block-sociallinks div { + flex-direction: column; } } diff --git a/web/themes/custom/barbell/css/style.css b/web/themes/custom/barbell/css/style.css index 31dcad7..c87c7cd 100644 --- a/web/themes/custom/barbell/css/style.css +++ b/web/themes/custom/barbell/css/style.css @@ -1,6 +1,63 @@ :root { --yellow: #f6b018; --gray: #919191; + --black: #231f20; +} + +* { + box-sizing: border-box; + font-variant-ligatures: none; +} + +@font-face { + font-family: "Brandon Grotesque Medium"; + font-style: normal; + font-weight: normal; + src: local("Brandon Grotesque Medium"), + url("brandon-grotesque-medium-587bd623e472a.woff") format("woff"); +} + +@font-face { + font-family: "Brandon Grotesque Bold"; + font-style: normal; + font-weight: normal; + src: local("Brandon Grotesque Bold"), + url("brandon-grotesque-bold-587bd6400afd0.woff") format("woff"); +} + +@font-face { + font-family: "Henry"; + font-style: normal; + font-weight: normal; + src: local("Henry"), url("Henrik-Regular.otf") format("woff"); +} + +/* NAVIGATION */ +header div, +#block-barbell-branding, +#block-barbell-branding a img, +#block-barbell-branding div, +header div nav, +header div nav h2, +header div nav div, +header div nav ul, +header div nav ul li, +.language-switcher-language-url, +.language-switcher-language-url div, +header div div div, +ul.links, +li.en, +li.sr { + display: inline; +} + +header div { + display: grid; + grid-template-columns: 30% auto 10%; + height: 60px; + background: var(--yellow); + width: 100%; + align-items: center; } * { @@ -14,9 +71,13 @@ body, overflow-x: hidden; } -/* FOOTER EXTRA SPACE SOLUTION */ -#block-mainnavigation-menu { - display: none !important; +.layout-container { + position: relative; +} + +.path-frontpage .text-formatted p a { + text-decoration: none; + color: #e52828; } @font-face { @@ -71,6 +132,18 @@ header div { /* NAV LOGO */ #block-barbell-branding { + clip-path: polygon(0 0, 100% 0%, 86% 100%, 0% 100%); + width: 100%; + background: black; + height: 100%; + text-align: right; + padding-right: 20px; + vertical-align: middle; +} + +#block-barbell-branding a img { + display: inline-block; + margin-right: 80px; clip-path: polygon(0 0, 100% 0%, 84% 100%, 0% 100%); width: 100%; background: black; @@ -85,9 +158,9 @@ header div { #block-barbell-branding a img { display: inline-block; - margin-right: 62px; - width: 290px; - padding: 19px 0; + margin-right: 0px; + width: 370px; + padding: 0px 0; } /* NAV MAIN */ @@ -98,6 +171,14 @@ header div { margin-right: 20px; } +#block-barbell-main-menu ul li a { + text-decoration: none; + color: black; + font-family: "Brandon Grotesque Medium"; + text-transform: uppercase; + margin-right: 20px; +} + #block-barbell-main-menu ul li a { text-decoration: none; color: black; @@ -110,6 +191,41 @@ header div div .links { align-items: center; } +#block-languageswitcher ul { + display: inline-grid; + grid-template-columns: 1fr 1fr; + align-content: center; + justify-content: center; +} +.language-link { + font-family: "Brandon Grotesque Bold"; + text-transform: uppercase; + color: black; + text-decoration: none; + display: inline-grid; +} + +.page-title:before { + content: url("../img/page-title-bcg.png"); + position: relative; + left: 100px; + z-index: -1; + bottom: -12px; +} + +.path-node .page-title::before { + content: none; +} + +.page-title { + text-align: center; + text-transform: uppercase; + margin: 120px; + font-size: 60px; + position: relative; + font-family: "Henry"; +} + .language-switcher-language-url { display: flex; align-items: center; @@ -157,9 +273,9 @@ header div div .links { text-align: center; text-transform: uppercase; margin: 145px auto; - font-size: 64px; + font-size: 60px; font-family: "Henry", sans-serif; - letter-spacing: 2px; + letter-spacing: 0px; } @media only screen and (max-width: 768px) { @@ -167,7 +283,7 @@ header div div .links { .kontakt-block-content h3, #after_content div div h2 { /* margin: 0; */ - font-size: 45px; + font-size: 36px; } } @@ -180,11 +296,26 @@ header div div .links { .page-title .field--name-title:before, .kontakt-block-content h3:before, #after_content div div h2:before { - content: url("../img/yellow-stripes-78.png"); + content: url(../img/page-title-bcg-big.png); position: absolute; z-index: -1; } +.page-title .field--name-title:before { + left: -37px; + top: 1px; +} + +@media only screen and (max-width: 992px) { + .page-title .field--name-title:before, + .kontakt-block-content h3:before, + #after_content div div h2:before { + content: url("../img/page-title-bcg-med.png"); + top: 1px; + left: -25px; + } +} + @media only screen and (max-width: 768px) { .page-title .field--name-title:before, .kontakt-block-content h3:before, @@ -192,45 +323,110 @@ header div div .links { content: url("../img/page-title-bcg.png"); } } -.page-title .field--name-title:before { - left: -32px; - bottom: -15px; -} .kontakt-block-content h3 { display: inline-block; } .kontakt-block-content h3:before { - left: -39px; - bottom: -21px; + left: -25px; + bottom: -24px; } @media only screen and (max-width: 768px) { .page-title .field--name-title:before, .kontakt-block-content h3:before { - left: -25px; - bottom: -15px; + left: -11px; + top: -2px; + } +} + +@media only screen and (max-width: 551px) { + .page-title, + .kontakt-block-content h3, + #after_content div div h2 { + margin-left: 15px; + margin-right: 15px; + font-size: 26px; + letter-spacing: 0; + } + + .page-title .field--name-title:before, + .kontakt-block-content h3:before, + #after_content div div h2:before { + content: url("../img/page-title-bcg-sm.png"); } } /* MAIN TXT (all, except home page) */ -.field--name-body p { +.path-frontpage .field--name-body p { margin: 0 11%; + text-align: justify; +} + +.path-frontpage .slider-content p { + margin: 0 auto; +} + +.field--name-body p { font-family: "Brandon Grotesque Medium"; font-size: 29px; - text-align: justify; +} + +@media only screen and (max-width: 992px) { + .field--name-body p { + font-size: 24px; + } +} + +@media only screen and (max-width: 768px) { + .field--name-body p { + font-size: 22px; + } } /* bold prices txt */ -.field--name-body p strong { +.page-node-type-program .field--name-body p strong { font-family: "Brandon Grotesque Bold"; font-size: 30px; letter-spacing: 1px; + display: inline-block; + margin: 2em 0; +} + +@media only screen and (min-width: 576px) { + .page-node-type-program .field--name-body p strong { + font-family: "Brandon Grotesque Bold"; + font-size: 20px; + /* font-weight: 1; */ + } +} + +@media only screen and (min-width: 768px) { + .page-node-type-program .field--name-body p strong { + font-size: 25px; + /* font-weight: 1; */ + } +} + +@media only screen and (min-width: 1200px) { + .page-node-type-program .field--name-body p strong { + font-size: 40px; + /* font-weight: 1; */ + } +} + +@media only screen and (max-width: 576px) { + .page-node-type-program .field--name-body p strong { + font-size: 19px; + + font-family: "Brandon Grotesque Medium"; + /*font-weight: 1;*/ + } } /* BUTTON (all, except home page)*/ -.node__content, +/*.node__content,*/ #block-zakazitetrening div { display: grid; position: relative; @@ -238,14 +434,21 @@ header div div .links { .field--name-field-link-button, #block-zakazitetrening div p { - background: var(--yellow); + /* background: var(--yellow); */ color: white; display: inline-block; text-transform: uppercase; - padding: 25px 50px; font-size: 27px; - margin: 90px auto 90px; + width: 100%; +} + +.field--name-field-link-button div.field__item { + background: var(--yellow); border-radius: 45px; + margin: 3em auto 5em; + padding: 1em; + max-width: 500px; + text-align: center; } @media only screen and (max-width: 768px) { @@ -271,9 +474,63 @@ header div div .links { font-family: "Brandon Grotesque Medium"; } +/* FOOTER EXTRA SPACE SOLUTION */ +#block-mainnavigation-menu { + display: none !important; +} + +/* BACK TO TOP BUTTON */ +.scroll-top-button { + background: white; + display: none; + position: fixed; + bottom: 90px; + right: 20px; + box-shadow: 0 0 2px grey; + z-index: 9999; +} + +.scroll-top-button p { + text-align: center; + margin: 0; +} + +.scroll-top-button p:nth-child(1) { + font-size: 30px; + padding: 0 15px; +} + +.scroll-top-button p:nth-child(2) { + font-size: 16px; + padding: 0 0; +} + +.fa-angle-up { + color: black !important; +} + +@media only screen and (max-width: 768px) { + .scroll-top-button { + bottom: 5px; + right: 10px; + } + + .scroll-top-button p:nth-child(1) { + font-size: 20px; + padding: 0 15px; + } + + .scroll-top-button p:nth-child(2) { + font-size: 10px; + padding: 0 0; + } +} + /* FOOTER */ footer { height: 100px; + width: 100%; + bottom: 0; } .region-footer { @@ -292,11 +549,15 @@ footer { } } -#block-yearname { +#block-yearname div p { color: var(--yellow); + text-align: left; + padding-left: 0; + padding-right: 0; + margin: 0 11%; } -@media only screen and (max-width: 768px) { +@media only screen and (max-width: 992px) { #block-yearname { align-self: center; display: grid; @@ -332,13 +593,8 @@ footer div nav ul.menu a.is-active { align-items: center; } -/* BLOCKQUOTE */ -blockquote p { - text-transform: uppercase; - font-family: "Henry", sans-serif !important; - text-align: center !important; - font-size: 74px !important; - margin: 80px 0 0 !important; +#block-sociallinks a { + margin: 0 12px; } /* +++++++++++ KONTAKT +++++++++++ */ @@ -355,9 +611,11 @@ blockquote p { .front-kontakt-holder { height: 100%; + background-size: cover; + background-position: center; + background-repeat: no-repeat; } - -@media only screen and (max-width: 768px) { +@media only screen and (max-width: 992px) { #pre_footer { grid-template-columns: 1fr; height: unset; @@ -365,7 +623,7 @@ blockquote p { #pre_footer .pre_footer_right, .pre_footer_right div { - height: 175px; + height: 200px; } } @@ -373,7 +631,7 @@ blockquote p { font-family: "Brandon Grotesque Bold", sans-serif; color: whitesmoke; text-align: center; - background: gray; + /*background: gray;*/ } .kontakt-block-content h3 { @@ -426,22 +684,28 @@ blockquote p { } } -.kontakt-block-content .socials { - font-size: 42px; - margin-bottom: 60px; +.socials { + padding-bottom: 60px; } -.kontakt-block-content .socials span { - margin: 0 5px; +.socials a { + font-size: 42px; + color: whitesmoke; + padding-bottom: 60px; + margin: 0 10px; } -/* CONTACT - MAP */ +/* .kontakt-block-content .socials span { + margin: 0 5px 60px; +} */ + +/* CONTACT - MAP, SOME TITLES */ @media only screen and (min-width: 768px) and (max-width: 992px) { .page-title, .kontakt-block-content h3, #after_content div div h2 { /* margin: 0; */ - font-size: 53px; + font-size: 49px; } .kontakt-block-content .tel a { @@ -451,4 +715,67 @@ blockquote p { .kontakt-block-content .address { font-size: 28px; } + + /* vesti naslov */ + .path-vesti .page-title:before, + .path-tim .page-title:before, + .path-programi .page-title:before { + content: url(../img/page-title-bcg.png) !important; + top: 4px !important; + left: -26px !important; + } + + /* blog naslov */ + .path-blog .page-title:before, + .path-tim .page-title:before, + .path-programi .page-title:before { + content: url(../img/page-title-bcg.png) !important; + top: 5px !important; + left: -21px !important; + } + + /* tim naslov */ + /* .path-tim .page-title:before { + content: url(../img/page-title-bcg.png) !important; + bottom: -10px !important; + left: -21px !important; + } */ +} + +@media only screen and (max-width: 768px) { + /* vesti naslov */ + .path-vesti .page-title:before, + .path-programi .page-title:before { + content: url(../img/page-title-bcg.png) !important; + top: 0px !important; + left: -26px !important; + } + + /* programi naslov */ + .path-programi .page-title:before { + content: url(../img/page-title-bcg-sm.png) !important; + top: -1px !important; + left: -18px !important; + } + + /* tim naslov */ + .path-tim .page-title:before { + content: url(../img/page-title-bcg-sm.png) !important; + bottom: -14px !important; + left: -28px !important; + } +} + +@media only screen and (min-width: 551px) and (max-width: 768px) { + /* programi naslov */ + .path-programi .page-title:before { + content: url(../img/page-title-bcg.png) !important; + top: -1px !important; + left: -18px !important; + } +} + +.page-title { + margin-left: 15px; + margin-right: 15px; } diff --git a/web/themes/custom/barbell/css/tim.css b/web/themes/custom/barbell/css/tim.css index 5915bb2..665699f 100644 --- a/web/themes/custom/barbell/css/tim.css +++ b/web/themes/custom/barbell/css/tim.css @@ -1,18 +1,21 @@ -.path-tim +/* .path-tim div div main div div #block-barbell-content - div - .view-tim - .view-content { + div */ +.view-tim .view-content { display: grid; grid-template-columns: 1fr 1fr; grid-gap: 55px; - /* justify-content: none; */ - /* margin: 0 auto; */ + width: 70vw; + margin: 0 auto; +} + +.view-tim { + margin-bottom: 130px; } /* HR style */ @@ -25,15 +28,12 @@ .view-tim .view-content div div a:hover div { color: whitesmoke; + background: var(--black); } .view-tim .view-content div div a .tim-image div img { width: 100%; -} - -.view-tim { - width: 1000px; - margin: 0 auto; + height: auto; } .tim-thumb { @@ -58,6 +58,10 @@ margin-bottom: 50px; } +.path-tim #block-barbell-page-title { + margin: 150px auto 120px; +} + .tim-description::before { content: " "; display: block; @@ -92,3 +96,98 @@ .tim-image { margin: 0 0 -6px 0; } + + +@media only screen and (min-width: 1325px) { + .tim-description { + height: 95px; + } +} + +@media only screen and (min-width: 992px) and (max-width: 1325px) { + .view-tim .view-content { + width: 80vw; + } + + .tim-title { + font-size: 35px; + } + + .tim-description { + height: 95px; + } +} + +@media only screen and (min-width: 768px) and (max-width: 992px) { + .view-tim .view-content { + width: 90vw; + grid-gap: 30px; + } + + .tim-title { + font-size: 30px; + } + + .tim-description { + height: 95px; + } +} + +@media only screen and (min-width: 576px) and (max-width: 768px) { + .view-tim .view-content { + width: 90vw; + grid-gap: 30px; + } + + .tim-title { + font-size: 22px; + } + + .tim-description { + font-size: 22px; + margin-bottom: 20px; + height: 130px; + } +} + +@media only screen and (min-width: 480px) and (max-width: 576px) { + .view-tim .view-content { + width: 90vw; + grid-gap: 30px; + } + + .tim-title { + font-size: 18px; + } + + .tim-description { + font-size: 20px; + margin-bottom: 15px; + height: 130px; + } +} + +@media only screen and (min-width: 320px) and (max-width: 480px) { + .view-tim .view-content { + width: 98vw; + grid-gap: 5px; + } + + .tim-title { + font-size: 14px; + } + + .tim-description { + font-size: 17px; + margin-bottom: 5px; + } + + .tim-info-wrapper { + line-height: 18px; + height: 150px; + } + + .tim-description::before { + margin: 10px auto; + } +} diff --git a/web/themes/custom/barbell/img/asics.webp b/web/themes/custom/barbell/img/asics.webp new file mode 100644 index 0000000..ad31178 Binary files /dev/null and b/web/themes/custom/barbell/img/asics.webp differ diff --git a/web/themes/custom/barbell/img/dummy.png b/web/themes/custom/barbell/img/dummy.png deleted file mode 100644 index 90dd040..0000000 Binary files a/web/themes/custom/barbell/img/dummy.png and /dev/null differ diff --git a/web/themes/custom/barbell/img/empty-raspored.png b/web/themes/custom/barbell/img/empty-raspored.png new file mode 100644 index 0000000..01168dc Binary files /dev/null and b/web/themes/custom/barbell/img/empty-raspored.png differ diff --git a/web/themes/custom/barbell/img/fe.webp b/web/themes/custom/barbell/img/fe.webp new file mode 100644 index 0000000..e13016f Binary files /dev/null and b/web/themes/custom/barbell/img/fe.webp differ diff --git a/web/themes/custom/barbell/img/hamburger.png b/web/themes/custom/barbell/img/hamburger.png new file mode 100644 index 0000000..05c0f9e Binary files /dev/null and b/web/themes/custom/barbell/img/hamburger.png differ diff --git a/web/themes/custom/barbell/img/hh.webp b/web/themes/custom/barbell/img/hh.webp new file mode 100644 index 0000000..261143d Binary files /dev/null and b/web/themes/custom/barbell/img/hh.webp differ diff --git a/web/themes/custom/barbell/img/hh1.webp b/web/themes/custom/barbell/img/hh1.webp new file mode 100644 index 0000000..2477926 Binary files /dev/null and b/web/themes/custom/barbell/img/hh1.webp differ diff --git a/web/themes/custom/barbell/img/hhInit.webp b/web/themes/custom/barbell/img/hhInit.webp new file mode 100644 index 0000000..46a49a5 Binary files /dev/null and b/web/themes/custom/barbell/img/hhInit.webp differ diff --git a/web/themes/custom/barbell/img/kontakt.png b/web/themes/custom/barbell/img/kontakt.png new file mode 100644 index 0000000..8f8ea42 Binary files /dev/null and b/web/themes/custom/barbell/img/kontakt.png differ diff --git a/web/themes/custom/barbell/img/logo.png b/web/themes/custom/barbell/img/logo.png deleted file mode 100644 index 4391b87..0000000 Binary files a/web/themes/custom/barbell/img/logo.png and /dev/null differ diff --git a/web/themes/custom/barbell/img/page-title-bcg-sm.png b/web/themes/custom/barbell/img/page-title-bcg-sm.png new file mode 100644 index 0000000..752ec6b Binary files /dev/null and b/web/themes/custom/barbell/img/page-title-bcg-sm.png differ diff --git a/web/themes/custom/barbell/img/se.webp b/web/themes/custom/barbell/img/se.webp new file mode 100644 index 0000000..c189d29 Binary files /dev/null and b/web/themes/custom/barbell/img/se.webp differ diff --git a/web/themes/custom/barbell/js/main.js b/web/themes/custom/barbell/js/main.js index e69de29..d96acfe 100644 --- a/web/themes/custom/barbell/js/main.js +++ b/web/themes/custom/barbell/js/main.js @@ -0,0 +1,358 @@ +Drupal.behaviors.exampleModule = { + attach: function(context, settings) { + //RESPO NAVIGATION + let navigacija = (function() { + var langSwitch = document.querySelector("#block-languageswitcher"); + var hamburgerHolder = document.createElement("div"); + var hamburgerImg = document.createElement("img"); + hamburgerImg.src = "/barbell/web/sites/default/files/img/hamburger.png"; + hamburgerHolder.classList.add("hamburgerHolder"); + hamburgerHolder.appendChild(hamburgerImg); + langSwitch.appendChild(hamburgerHolder); + + var nav = document.querySelector("header div nav"); + var xHolder = document.createElement("div"); + var xPara = document.createElement("p"); + xPara.innerHTML = "X"; + xHolder.appendChild(xPara); + nav.appendChild(xHolder); + xHolder.classList.add("xHolder"); + xPara.classList.add("xPara"); + + hamburgerHolder.addEventListener("click", function() { + nav.style.top = "0"; + + setTimeout(() => { + xHolder.style.display = "block"; + }, 200); + + setTimeout(() => { + xPara.style.fontSize = "35px"; + xPara.style.opacity = "1"; + }, 230); + + setTimeout(() => { + xHolder.style.border = "1px solid whitesmoke"; + }, 550); + + function closeNav() { + nav.style.top = "-100vh"; + xHolder.style.display = "none"; + xPara.style.fontSize = "1px"; + xPara.style.opacity = "0"; + xHolder.style.border = "none"; + } + + var navLinks = document.querySelectorAll( + "#block-barbell-main-menu ul li" + ); + + navLinks.forEach(link => + link.addEventListener("click", function() { + if (nav.style.top == "0") { + closeNav(); + } + }) + ); + + xHolder.addEventListener("click", closeNav); + }); + })(); + //RESPO NAVIGATION ENDS + + //REMOVING INLINE STYLES + const removeInlineStyles = (() => { + //align-right + //if (document.querySelector(".path-tim")) { + // const regionOfRelevantImages = document.querySelector("main"); + // let imgs = regionOfRelevantImages.querySelectorAll("img"); + // imgs = [...imgs]; + // imgs.forEach(x => { + // if (x.attributes.length - 1) { + // x.attributes.removeNamedItem("width"); + // x.attributes.removeNamedItem("height"); + // if (x.classList.contains("align-right")) + // x.classList.remove("align-right"); + // } + // }); + //} + })(); + //REMOVING INLINE STYLES ENDS + + //NOVI RASPORED + if (document.querySelector("table")) { + let noviRaspored = (function() { + //novi redovi + let tbody = document.querySelector("tbody"); + for (let i = 1; i < 3; i++) { + let tr = tbody.lastElementChild; + let trClone = tr.cloneNode(true); + tbody.appendChild(trClone); + } + + //th-nedelja i kolona nedelja + let theadTr = document.querySelector("thead tr"); + let nedelja = document.createElement("th"); + nedelja.setAttribute("scope", "col"); + nedelja.innerHTML = "NEDELJA"; + theadTr.appendChild(nedelja); + + //td-ovi u koloni nedelja i novi sati + let sati = [ + "09:00", + "10:00", + "11:00", + "12:00", + "16:00", + "17:00", + "18:00", + "19:00", + "20:00", + "21:00" + ]; + + let dani = document.querySelectorAll("thead th"); + dani = [...dani]; + dani.shift(); + + //td content wrapper-i + function okviriZaTermineUtd(x) { + const imgContainer = document.createElement("div"); + const img = document.createElement("img"); + const titleContainer = document.createElement("div"); + const title = document.createElement("p"); + + x.appendChild(imgContainer); + x.appendChild(titleContainer); + titleContainer.appendChild(title); + imgContainer.appendChild(img); + + titleContainer.className = "rasporedTitleHolder"; + title.className = "rasporedText"; + imgContainer.className = "rasporedImgWrapper"; + } + + let tbodyTr = document.querySelectorAll("tbody tr"); + tbodyTr = [...tbodyTr]; + tbodyTr.forEach((it, index) => { + let newTd = document.createElement("td"); + it.appendChild(newTd); + it.firstElementChild.innerHTML = sati[index]; + let tdInThisRow = it.children; + tdInThisRow = [...tdInThisRow]; + tdInThisRow.shift(); + + //id-jevi za dane po satu (npr za celiju ponedeljak 09:00 - id = PON09) + tdInThisRow.forEach((it, i) => { + it.innerHTML = ""; + okviriZaTermineUtd(it); + if (i == 3) { + it.id = "CET" + sati[index].substring(0, 2); + } else { + it.id = + dani[i].innerHTML.substring(0, 3) + sati[index].substring(0, 2); + } + }); + }); + })(); + } + + let vrstaTreninga = [ + "FUNKCIONALNI TRENING", + "SNAGA I ESTETIKA", + "HIP HOP ČASOVI" + ]; + + let ikonaTreninga = [ + "/barbell/web/sites/default/files/img/fe.webp", + "/barbell/web/sites/default/files/img/se.webp", + "/barbell/web/sites/default/files/img/hh.webp" + ]; + + function raspored([...ftTermini], [...seTermini], [...hhTermini]) { + ftTermini = [...ftTermini]; + seTermini = [...seTermini]; + hhTermini = [...hhTermini]; + + ftTermini.forEach((it, i) => { + let termin = document.getElementById(it); + if (termin) { + termin.style.background = "#E3E3E3"; + termin.firstElementChild.firstElementChild.src = ikonaTreninga[0]; + termin.lastElementChild.firstElementChild.innerHTML = + vrstaTreninga[0]; + } + }); + + seTermini.forEach((it, i) => { + let termin = document.getElementById(it); + if (termin) { + termin.style.background = "#E3E3E3"; + termin.firstElementChild.firstElementChild.src = ikonaTreninga[1]; + termin.lastElementChild.firstElementChild.innerHTML = + vrstaTreninga[1]; + } + }); + + hhTermini.forEach((it, i) => { + let termin = document.getElementById(it); + if (termin) { + termin.style.background = "#E3E3E3"; + termin.firstElementChild.firstElementChild.src = ikonaTreninga[2]; + termin.lastElementChild.firstElementChild.innerHTML = + vrstaTreninga[2]; + } + }); + } + + //SETOVANJE TERMINA, redom: + //Funkcionalni trening, + //Snaga i estetika, + //Hiphop casovi + //UNOSI SE ID TD-a u niz + raspored( + [ + "PON09", + "PON18", + "PON19", + "PON20", + "UTO18", + "UTO19", + "SRE09", + "SRE18", + "SRE19", + "SRE20", + "CET18", + "CET19", + "PET09", + "PET18", + "PET19", + "PET20", + "SUB10", + "SUB11" + ], + ["UTO20", "CET20", "SUB12"], + ["SUB16", "NED16"] + ); + + function rasporedLegendaIskraceniDani() { + // skraceni dani + let days = document.querySelectorAll("thead th"); + days = [...days]; + days.shift(); + days.forEach(it => (it.innerHTML = it.innerHTML.substring(0, 3))); + + // raspored legenda + if (document.querySelector("table")) { + let rasporedTable = document.querySelector("table"); + var rasporedTableParent = rasporedTable.parentElement; + let legendaWrapper = document.createElement("div"); + legendaWrapper.className = "legendaWrapper"; + let legendaUl = document.createElement("ul"); + for (let i = 0; i < 3; i++) { + let legendaLi = document.createElement("li"); + let legendaImg = document.createElement("img"); + let legendaSpan = document.createElement("span"); + legendaImg.src = ikonaTreninga[i]; + legendaSpan.innerHTML = vrstaTreninga[i]; + legendaLi.appendChild(legendaImg); + legendaLi.appendChild(legendaSpan); + legendaUl.appendChild(legendaLi); + legendaWrapper.appendChild(legendaUl); + } + rasporedTableParent.appendChild(legendaWrapper); + rasporedTableParent.classList.add("rasporedAllWrapper"); + } + } + + if (window.innerWidth <= 768) { + rasporedLegendaIskraceniDani(); + } + //RASPORED ENDS + + //SCROLL TO TOP + // scroll back to top button + let scrollTopButton = document.querySelector(".scroll-top-button"); + + // show / hide button + showHideButton = () => { + window.pageYOffset < 150 + ? (scrollTopButton.style.display = "none") + : (scrollTopButton.style.display = "block"); + }; + document.addEventListener("scroll", showHideButton); + + // scroll to top + window.addEventListener("load", function() { + document + .querySelector(".scroll-top-button") + .addEventListener("click", function(e) { + e.preventDefault(); + document + .querySelector("header") + .scrollIntoView({ behavior: "smooth" }); + }); + }); + // /SCROLL TO TOP ENDS / + + //FIXED FOOTER + let fixedFooter = (function() { + let layoutContainer = document.querySelector(".layout-container"); + let footer = document.querySelector("footer"); + + if (layoutContainer.offsetHeight < window.innerHeight) { + layoutContainer.style.height = window.innerHeight + "px"; + footer.style.position = "fixed"; + footer.style.bottom = "0"; + } + })(); + //FIXED FOOTER ENDS / + + // fixContactImg = (() => { + // if (document.querySelector(".front-kontakt-holder")) { + // const contact = document.querySelector(".front-kontakt-holder"); + + // const oldPath = contact.style.backgroundImage.split('"'); + // const newPath = oldPath[0] + '".' + oldPath[1] + '"' + oldPath[2]; + // contact.style.backgroundImage = newPath; + // } + // })(); + + //KONTAKT SOC IKONE + let fbLink = document.querySelector(".social-fb"); + let inLink = document.querySelector(".social-in"); + let ytLink = document.querySelector(".social-yt"); + + fbLink.innerHTML = ''; + inLink.innerHTML = ''; + ytLink.innerHTML = ''; + + if (document.querySelector(".fm")) { + let faLink = document.querySelector(".fm"); + let vbLink = document.querySelector(".vb"); + let waLink = document.querySelector(".wa"); + + faLink.innerHTML = ''; + vbLink.innerHTML = ''; + waLink.innerHTML = ''; + } + //KONTAKT SOC IKONE ENDS + + //SLEDECA I PRETHODNA STRANA LINKOVI BLOG (VEROVATNO I VESTI) + if (document.querySelector(".pager__item--next a span:nth-child(2)")) { + const next = document.querySelector( + ".pager__item--next a span:nth-child(2)" + ); + next.innerHTML = + 'sledeća stranica'; + } + if (document.querySelector(".pager__item--previous a span:nth-child(2)")) { + const previous = document.querySelector( + ".pager__item--previous a span:nth-child(2)" + ); + previous.innerHTML = + 'prethodna stranica'; + } + //SLEDECA I PRETHODNA STRANA LINKOVI BLOG (VEROVATNO I VESTI) ENDS + } +}; diff --git a/web/themes/custom/barbell/logo.svg b/web/themes/custom/barbell/logo.svg new file mode 100644 index 0000000..0425619 --- /dev/null +++ b/web/themes/custom/barbell/logo.svg @@ -0,0 +1,4574 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/web/themes/custom/barbell/templates/node--program--teaser.html.twig b/web/themes/custom/barbell/templates/node--program--teaser.html.twig index e50796a..f2374ba 100644 --- a/web/themes/custom/barbell/templates/node--program--teaser.html.twig +++ b/web/themes/custom/barbell/templates/node--program--teaser.html.twig @@ -76,8 +76,10 @@
-
{{ label }}
-
{{ content.body }}
+
+
{{ label }}
+
{{ content.body }}
+
diff --git a/web/themes/custom/barbell/templates/page.html.twig b/web/themes/custom/barbell/templates/page.html.twig index b684f0a..59e6c8a 100644 --- a/web/themes/custom/barbell/templates/page.html.twig +++ b/web/themes/custom/barbell/templates/page.html.twig @@ -44,38 +44,52 @@ #}
-
- {{ page.header }} -
- {% if page.hero %} -
- {{ page.hero }} -
- {% endif %} +
+ {{ page.header }} +
+ {% if page.hero %} +
+ {{ page.hero }} +
+ {% endif %} -
- {# link is in html.html.twig #} +
+ + {# link is in html.html.twig #} -
- {{ page.content }} -
{# /.layout-content #} -
- {% if page.after_content %} -
- {{ page.after_content }} -
- {% endif %} +
+ {{ page.content }} +
+ {# /.layout-content #} +
+ {% if page.after_content %} +
+ {{ page.after_content }} +
+ {% endif %} - {% if page.pre_footer_left or page.pre_footer_right %} - - {% endif %} - {% if page.footer %} -
- {{ page.footer }} -
- {% endif %} + {% if page.pre_footer_left or page.pre_footer_right %} + + {% endif %} + {% if page.footer %} + + {% endif %} -
{# /.layout-container #} + +{# /.layout-container #}