diff --git a/.idea/cms-niceties.iml b/.idea/cms-niceties.iml new file mode 100644 index 0000000..c956989 --- /dev/null +++ b/.idea/cms-niceties.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..4c91e9e --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/_config.php b/_config.php new file mode 100755 index 0000000..f9f25bd --- /dev/null +++ b/_config.php @@ -0,0 +1,39 @@ +enablePlugins([ + 'template', + 'fullscreen', + 'hr', + 'contextmenu', + 'charmap', + 'visualblocks', + 'lists', + 'charcount' => ModuleResourceLoader::resourceURL( + 'drmartingonzo/ss-tinymce-charcount:client/dist/js/bundle.js' + ), +]); +$config->addButtonsToLine(2, 'hr'); +$config->setOption('block_formats', 'Paragraph=p;Heading 2=h2;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6;Address=address;Pre=pre'); +$config->setOption('invalid_elements', 'h1'); +$config->setOption( + 'table_class_list', + [ + ['title' => 'Transparent Table', 'value' => 'table-none'], + ['title' => 'Shaded rows', 'value' => 'table table-striped table-bordered'], + ] +); + +FulltextSearchable::enable(); + +// replace embed parser +/*$parser = ShortcodeParser::get('default'); +$parser->unregister('embed'); +$parser->register('embed', [EmbedShortcodeProvider::class, 'handle_shortcode']);*/ diff --git a/_config/base-config.yml b/_config/base-config.yml new file mode 100755 index 0000000..e99f8d8 --- /dev/null +++ b/_config/base-config.yml @@ -0,0 +1,28 @@ +--- +Name: webapp-base-config +--- +SilverStripe\Core\Manifest\ModuleManifest: + project: app + +Page: + default_container_class: 'container' + +#SilverStripe\Admin\LeftAndMain: +# extra_requirements_javascript: +# - 'colymba/gridfield-bulk-editing-tools:client/dist/js/main.js' +# - 'colymba/gridfield-bulk-editing-tools:client/lang/en.js' +# extra_requirements_css: +# - 'colymba/gridfield-bulk-editing-tools:client/dist/styles/main.css' + +SilverStripe\Admin\LeftAndMain: + extra_requirements_javascript: + - 'app/client/dist/js/app_cms.js' + extra_requirements_css: + - 'app/client/dist/css/app_cms.css' + +SilverStripe\Forms\HTMLEditor\TinyMCEConfig: + editor_css: + - 'app/client/dist/css/app_editor.css' + +SilverStripe\Control\Email\Email: + send_all_emails_from: noreply@twma.pro diff --git a/_config/base-extensions.yml b/_config/base-extensions.yml new file mode 100755 index 0000000..5ddc89c --- /dev/null +++ b/_config/base-extensions.yml @@ -0,0 +1,47 @@ +--- +Name: webapp-base-extensions +--- +# Basic extensions +SilverStripe\Admin\LeftAndMain: + extensions: + - A2nt\CMSNiceties\Extensions\LeftAndMainExtension + +SilverStripe\SiteConfig\SiteConfig: + extensions: + - A2nt\CMSNiceties\Extensions\SocialExtension + - A2nt\CMSNiceties\Extensions\SiteConfigExtension + - A2nt\CMSNiceties\Extensions\NotificationsExtension + +SilverStripe\CMS\Model\SiteTree: + extensions: + - A2nt\CMSNiceties\Extensions\SiteTreeExtension + +Sheadawson\Linkable\Models\EmbeddedObject: + extensions: + - A2nt\CMSNiceties\Extensions\EmbeddedObjectExtension + +SilverStripe\Assets: + extensions: + - A2nt\CMSNiceties\Extensions\ImageExtension + +Dynamic\FlexSlider\Model\SlideImage: + extensions: + - A2nt\CMSNiceties\Extensions\SlideImageExtension + +SilverStripe\Core\Injector\Injector: + #SilverStripe\UserForms\Model\UserDefinedForm: + # class: A2nt\CMSNiceties\Extensions\UserDefinedForm_HiddenClass + Sheadawson\Linkable\Forms\EmbeddedObjectField: + class: A2nt\CMSNiceties\Extensions\EmbedObjectField + SilverStripe\Forms\CompositeField: + class: A2nt\CMSNiceties\Extensions\CompositeFieldExtension + +SilverStripe\UserForms\Form\UserForm: + extensions: + - A2nt\CMSNiceties\Extensions\PlaceholderFormExtension + +Page: + searchable_objects: + - A2nt\CMSNiceties\Models\TeamMember + extensions: + - DNADesign\Elemental\Extensions\ElementalPageExtension diff --git a/_config/base-files.yml b/_config/base-files.yml new file mode 100755 index 0000000..881603a --- /dev/null +++ b/_config/base-files.yml @@ -0,0 +1,81 @@ +--- +Name: webapp-base-files +--- +SilverStripe\Blog\Model\BlogPost: + featured_images_directory: 'blog-posts' + +SilverStripe\Assets\Upload_Validator: + allowedExtensions: + - 'stl' + +SilverStripe\Assets\File: + allowed_extensions: + - 'ace' + - 'arc' + - 'arj' + - 'asf' + - 'au' + - 'avi' + - 'bmp' + - 'bz2' + - 'cab' + - 'cda' + - 'csv' + - 'dmg' + - 'doc' + - 'docx' + - 'dotx' + - 'flv' + - 'gif' + - 'gpx' + - 'gz' + - 'hqx' + - 'ico' + - 'jpeg' + - 'jpg' + - 'kml' + - 'm4a' + - 'm4v' + - 'mid' + - 'midi' + - 'mkv' + - 'mov' + - 'mp3' + - 'mp4' + - 'mpa' + - 'mpeg' + - 'mpg' + - 'ogg' + - 'ogv' + - 'pages' + - 'pcx' + - 'pdf' + - 'png' + - 'pps' + - 'ppt' + - 'pptx' + - 'potx' + - 'ra' + - 'ram' + - 'rm' + - 'rtf' + - 'sit' + - 'sitx' + - 'tar' + - 'tgz' + - 'tif' + - 'tiff' + - 'txt' + - 'wav' + - 'webm' + - 'wma' + - 'wmv' + - 'xls' + - 'xlsx' + - 'xltx' + - 'zip' + - 'zipx' + - 'stl' + app_categories: + document: + - 'stl' diff --git a/_config/base-graphql.yml b/_config/base-graphql.yml new file mode 100644 index 0000000..9f98f7e --- /dev/null +++ b/_config/base-graphql.yml @@ -0,0 +1,38 @@ +--- +Name: webapp-base-graphql +After: graphqlconfig +--- +SilverStripe\Control\Director: + rules: + 'graphql': '%$SilverStripe\GraphQL\Controller.default' + +SilverStripe\GraphQL\Controller: + cors: + Enabled: true + Allow-Origin: '*' + Allow-Headers: 'Authorization, Content-Type, Content-Language, apikey' + Allow-Methods: 'GET, PUT, DELETE, OPTIONS, POST' + #Allow-Credentials: 'true' + Max-Age: 600 # 600 seconds = 10 minutes. + +SilverStripe\GraphQL\Auth\Handler: + authenticators: + - class: A2nt\CMSNiceties\GraphQL\APIKeyAuthenticator + priority: 30 + +SilverStripe\GraphQL\Manager.default: + properties: + Middlewares: + APIKeyMiddleware: A2nt\CMSNiceties\GraphQL\APIKeyMiddleware + +SilverStripe\GraphQL\Manager: + schemas: + default: + types: + member: 'A2nt\CMSNiceties\GraphQL\MemberTypeCreator' + page: 'A2nt\CMSNiceties\GraphQL\PageTypeCreator' + element: 'A2nt\CMSNiceties\GraphQL\ElementTypeCreator' + queries: + readPages: 'A2nt\CMSNiceties\GraphQL\PaginatedReadPagesQueryCreator' + readMembers: 'A2nt\CMSNiceties\GraphQL\ReadMembersQueryCreator' + paginatedReadMembers: 'A2nt\CMSNiceties\GraphQL\PaginatedReadMembersQueryCreator' diff --git a/_config/base-logs.yml_ b/_config/base-logs.yml_ new file mode 100755 index 0000000..3da07ff --- /dev/null +++ b/_config/base-logs.yml_ @@ -0,0 +1,60 @@ +--- +Name: webapp-base-logs-dev +Only: + environment: dev +--- +SilverStripe\Core\Injector\Injector: + Psr\Log\LoggerInterface.errorhandler: + calls: + pushMyDisplayErrorHandler: [pushHandler, ['%$DisplayErrorHandler']] + DisplayErrorHandler: + class: SilverStripe\Logging\HTTPOutputHandler + constructor: + - 'notice' + properties: + Formatter: '%$SilverStripe\Logging\DetailedErrorFormatter' + CLIFormatter: '%$SilverStripe\Logging\DetailedErrorFormatter' +--- +Name: webapp-base-logs-live +Except: + environment: dev +--- +SilverStripe\Core\Injector\Injector: + # Default logger implementation for general purpose use + Psr\Log\LoggerInterface: + calls: + # Save system logs to file + pushFileLogHandler: [pushHandler, ['%$LogFileHandler']] + + # Core error handler for system use + Psr\Log\LoggerInterface.errorhandler: + calls: + # Save errors to file + pushFileLogHandler: [pushHandler, ['%$LogFileHandler']] + # Format and display errors in the browser/CLI + pushMyDisplayErrorHandler: [pushHandler, ['%$DisplayErrorHandler']] + + # Custom handler to log to a file + LogFileHandler: + class: Monolog\Handler\StreamHandler + constructor: + - '../silverstripe.log' + - 'notice' + properties: + Formatter: '%$Monolog\Formatter\HtmlFormatter' + ContentType: text/html + + # Handler for displaying errors in the browser or CLI + DisplayErrorHandler: + class: SilverStripe\Logging\HTTPOutputHandler + constructor: + - 'error' + properties: + Formatter: '%$SilverStripe\Logging\DebugViewFriendlyErrorFormatter' + + # Configuration for the "friendly" error formatter + SilverStripe\Logging\DebugViewFriendlyErrorFormatter: + class: SilverStripe\Logging\DebugViewFriendlyErrorFormatter + properties: + Title: 'There has been an error' + Body: 'The website server has not been able to respond to your request' diff --git a/_config/base-mimevalidator.yml b/_config/base-mimevalidator.yml new file mode 100644 index 0000000..d1261af --- /dev/null +++ b/_config/base-mimevalidator.yml @@ -0,0 +1,9 @@ +--- +Name: webapp-base-mimeuploadvalidator +After: '#mimeuploadvalidator' +Only: + moduleexists: 'silverstripe/mimevalidator' +--- +SilverStripe\Core\Injector\Injector: + SilverStripe\Assets\Upload_Validator: + class: SilverStripe\MimeValidator\MimeUploadValidator diff --git a/_config/base-security.yml b/_config/base-security.yml new file mode 100755 index 0000000..e7b4c8a --- /dev/null +++ b/_config/base-security.yml @@ -0,0 +1,105 @@ +--- +Name: 'webapp-base-security' +After: 'framework/*, cms/*, security_baseline' +--- +SilverStripe\Core\Injector\Injector: + SilverStripe\Security\MemberAuthenticator\LostPasswordHandler: + class: A2nt\CMSNiceties\Extensions\LostPasswordHandlerExtension + SilverStripe\Security\MemberAuthenticator\MemberLoginForm: + class: A2nt\CMSNiceties\Extensions\SiteMemberLoginForm +--- +Except: + environment: dev +--- +# Secure cookies +SilverStripe\Control\Session: + cookie_secure: true + strict_user_agent_check: false + timeout: 604800 + +SilverStripe\Forms\PasswordField: + autocompleate: false + +SilverStripe\Security\Member: + lock_out_after_incorrect_logins: 5 + lock_out_delay_mins: 5 + # Password expiry should only happen when the password is leaked (optionally expire automatically if PCI/NIST compliance is required) + # password_expiry_days: 90 + # instead of password change, we send out a notice on change of password OR Email (notify_account_security_change) + notify_password_change: false +####################### +# Security Headers +####################### +#Controller: +# security_headers: +# # # Values may contain :security_reporting_base_url: placeholders, will be replaced with the URL to SecurityBaselineController endpoint +# # Header-Directive: "value; another value;" +# # X-Version-Alias-Of-Same-Header: "x:Header-Directive" # 'x-alias' headers may be aliased to the standard by a value starting with "x:Standard" +# # X-Another-Alias-Version-Of-Same: "different; value syntax as well;" + +# +# A useful base from guttmann/silverstripe-security-headers - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers#Security: +# + +# # Content-Security-Policy - https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP +# # Specifies approved sources of content that the browser may load from your website +# # Useful: upgrade-insecure-requests; (Instructs browser to treat a site's insecure URLs as if they are HTTPS (eg for legacy sites) +# # Example: Allow everything but only from the same origin: +# Content-Security-Policy: "default-src 'self';" +# # Example: Allow Google Analytics, Google AJAX CDN and Same Origin +# Content-Security-Policy: "script-src 'self' www.google-analytics.com ajax.googleapis.com;" +# # Example: Starter Policy - allows images, scripts, AJAX, form actions, and CSS from the same origin, and does not allow any +# # other resources to load (eg object, frame, media, etc). It is a good starting point for many sites. +# Content-Security-Policy: "default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self';base-uri 'self';form-action 'self'" + +# # Content-Security-Policy-Report-Only - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only +# # Allows web developers to experiment with policies by monitoring (but not enforcing) their effects +# # Browsers capable of enforcing CSP will send a violation report as a POST request to report-uri +# Content-Security-Policy-Report-Only: default-src https:; report-uri /security-reporting-endpoint/csp/ +# Content-Security-Policy-Report-Only: "default-src https:; script-src 'self' https: 'unsafe-inline' 'unsafe-eval'; connect-src 'self'; img-src 'self' https: data:; style-src 'self' 'unsafe-inline'; base-uri 'self'; form-action 'self'; report-uri /security-reporting-endpoint/csp/;" + +# # Strict-Transport-Security - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security +# # Tells the browser to ONLY interact with the site using HTTPS and never HTTP +# Strict-Transport-Security: "max-age=31536000" # time in seconds (one year=31536000) to remember that the site is only accessible over HTTPS + +# # Frame-Options - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options +# # Disallowes pages to render within a frame - protects against clickjacking attacks +# Frame-Options: "deny" + +# # XSS-Protection - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection +# # protect against Cross-site Scripting attacks (value 1=sanitize (default in most browsers), set to "1; mode=block" to prevent rendering if attack is detected) +# # Deprecated: if you do not need to support legacy browsers, it is recommended that you use Content-Security-Policy without allowing unsafe-inline scripts instead +# X-XSS-Protection: "1; mode=block" + +# # X-Content-Type-Options - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options +# # Indicate that the MIME types advertised in the Content-Type headers should not be changed and be followed +# # NOTE: Opting out of MIME sniffing can cause HTML web pages to be downloaded instead of rendered when they are +# # served with a MIME type other than text/html. Make sure to set both headers correctly. +# # Site security testers usually expect this header to be set. +# X-Content-Type-Options: "nosniff" + +# +# Some more from https://help.dreamhost.com/hc/en-us/articles/360036486952-Security-headers +# + +# # Referrer-Policy - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy +# # controls how much referrer information should be sent to another server +# Referrer-Policy: no-referrer + +# # Feature-Policy - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy +# # (Experimental 2020) controls which browser features are allowed on your website, eg for sites allowing third-party content + +# # CORS - Allow resource sharing with another domain (eg webfonts & ajax requests) +# # Access-Control-Allow-Origin - developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin + +# +# A further selection from https://github.com/bepsvpt/secure-headers/blob/master/config/secure-headers.php +# + +# Clear-Site-Data - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data +# Clears browsing data (cookies, storage, cache) associated with the requesting website + +# Expect-CT - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect-CT +# Lets sites opt in to reporting and/or enforcement of Certificate Transparency requirements, +# to prevent the use of misissued certificates for that site from going unnoticed. +# (will likely become obsolete in June 2021) diff --git a/_config/base-version-truncator.yml b/_config/base-version-truncator.yml new file mode 100755 index 0000000..66bca45 --- /dev/null +++ b/_config/base-version-truncator.yml @@ -0,0 +1,8 @@ +--- +Name: 'webapp-base-version-truncator' +--- +Axllent\VersionTruncator\VersionTruncator: + keep_versions: 4 # how many (published) versions of each page to keep + keep_drafts: 2 # how many drafts of each page to keep + keep_redirects: true # keep page versions that have a different URLSegment (for redirects) + keep_old_page_types: false # keep page versions where page type (ClassName) has changed diff --git a/_config/debugbar.yml b/_config/debugbar.yml new file mode 100755 index 0000000..2154cb2 --- /dev/null +++ b/_config/debugbar.yml @@ -0,0 +1,12 @@ +--- +Name: 'webapp-debugbar' +After: + - 'framework' + - 'debugbar' +Only: + environment: 'dev' +--- +LeKoala\DebugBar\DebugBar: + enabled_in_admin: false + query_limit: 500 + max_header_length: 2048 diff --git a/_config/env-check.yml b/_config/env-check.yml new file mode 100755 index 0000000..8d3b8fc --- /dev/null +++ b/_config/env-check.yml @@ -0,0 +1,28 @@ +--- +Name: webapp-env-check +--- + +SilverStripe\EnvironmentCheck\EnvironmentCheckSuite: + registered_checks: + curl: + definition: 'HasFunctionCheck("curl_init")' + title: 'is curl available?' + gd: + definition: 'HasFunctionCheck("imagecreatetruecolor")' + title: 'Does PHP have GD2 support?' + db: + definition: 'DatabaseCheck("Page")' + title: 'Is the database accessible?' + url: + definition: 'URLCheck()' + title: 'Is the homepage accessible?' + registered_suites: + check: + - curl + - gd + - db + - url + health: + - db + - url + diff --git a/_config/locale-fluent.yml_ b/_config/locale-fluent.yml_ new file mode 100755 index 0000000..83bbe2b --- /dev/null +++ b/_config/locale-fluent.yml_ @@ -0,0 +1,42 @@ +--- +Name: webapp-locale-fluent +After: + - webapp-extensions + - webapp-elemental + - webapp-locale +--- +# Define Fluent locales +TractorCow\Fluent\Model\Locale: + default_records: + en: + Title: 'EN' + Locale: en_US + URLSegment: en + IsGlobalDefault: 1 + us: + Locale: en_US + Title: 'EN' + URLSegment: en + ru: + Locale: ru_RU + Title: 'RU' + URLSegment: ru + Fallbacks: + - =>TractorCow\Fluent\Model\Locale.us + +# Enable Fluent extensions +Page: + extensions: + - DNADesign\Elemental\TopPage\SiteTreeExtension + - DNADesign\Elemental\Extensions\ElementalPageExtension + - A2nt\CMSNiceties\Extensions\PageFluentExtension + +DNADesign\Elemental\Models\ElementalArea: + extensions: + - DNADesign\Elemental\TopPage\FluentExtension + - A2nt\CMSNiceties\Extensions\ElementalArea + +DNADesign\Elemental\Models\BaseElement: + extensions: + - DNADesign\Elemental\TopPage\FluentExtension + - A2nt\CMSNiceties\Extensions\ElementRows diff --git a/_config/locale.yml b/_config/locale.yml new file mode 100755 index 0000000..d29471d --- /dev/null +++ b/_config/locale.yml @@ -0,0 +1,59 @@ +--- +Name: webapp-locale +--- + +Symbiote\Addressable\Addressable: + allowed_countries: + 'us': 'United States' + allowed_states: + 'AL': 'Alabama' + 'AK': 'Alaska' + 'AZ': 'Arizona' + 'AR': 'Arkansas' + 'CA': 'California' + 'CO': 'Colorado' + 'CT': 'Connecticut' + 'DE': 'Delaware' + 'DC': 'District Of Columbia' + 'FL': 'Florida' + 'GA': 'Georgia' + 'HI': 'Hawaii' + 'ID': 'Idaho' + 'IL': 'Illinois' + 'IN': 'Indiana' + 'IA': 'Iowa' + 'KS': 'Kansas' + 'KY': 'Kentucky' + 'LA': 'Louisiana' + 'ME': 'Maine' + 'MD': 'Maryland' + 'MA': 'Massachusetts' + 'MI': 'Michigan' + 'MN': 'Minnesota' + 'MS': 'Mississippi' + 'MO': 'Missouri' + 'MT': 'Montana' + 'NE': 'Nebraska' + 'NV': 'Nevada' + 'NH': 'New Hampshire' + 'NJ': 'New Jersey' + 'NM': 'New Mexico' + 'NY': 'New York' + 'NC': 'North Carolina' + 'ND': 'North Dakota' + 'OH': 'Ohio' + 'OK': 'Oklahoma' + 'OR': 'Oregon' + 'PA': 'Pennsylvania' + 'RI': 'Rhode Island' + 'SC': 'South Carolina' + 'SD': 'South Dakota' + 'TN': 'Tennessee' + 'TX': 'Texas' + 'UT': 'Utah' + 'VT': 'Vermont' + 'VA': 'Virginia' + 'WA': 'Washington' + 'WV': 'West Virginia' + 'WI': 'Wisconsin' + 'WY': 'Wyoming' diff --git a/_config/options-elements.yml b/_config/options-elements.yml new file mode 100755 index 0000000..d7b2da6 --- /dev/null +++ b/_config/options-elements.yml @@ -0,0 +1,52 @@ +--- +Name: webapp-options-elements +After: + - elemental + - elemental-list + - elementalvirtual + - webapp-base-extensions +--- +Page: + searchable_elements: + - DNADesign\Elemental\Models\ElementContent + extensions: + - DNADesign\Elemental\Extensions\ElementalPageExtension + +SilverStripe\CMS\Model\SiteTree: + allowed_elements: + - DNADesign\ElementalList\Model\ElementList + - DNADesign\Elemental\Models\ElementContent + - DNADesign\ElementalUserForms\Model\ElementForm + - Dynamic\Elements\Image\Elements\ElementImage + - Dynamic\Elements\Blog\Elements\ElementBlogPosts + - Dynamic\Elements\Oembed\Elements\ElementOembed + - Dynamic\Elements\Elements\ElementTestimonials + #- A2nt\ElementalBasics\Elements\TeamMembersElement + - A2nt\ElementalBasics\Elements\SliderElement + - A2nt\ElementalBasics\Elements\MapElement + #- A2nt\ElementalBasics\Elements\AccordionElement + - DNADesign\ElementalVirtual\Model\ElementVirtual + - A2nt\ElementalBasics\Elements\AccordionElement + - A2nt\ElementalBasics\Elements\CustomSnippetElement + - A2nt\ElementalBasics\Elements\InstagramElement + +DNADesign\ElementalList\Model\ElementList: + inline_editable: false + default_global_elements: false + allowed_elements: + - DNADesign\ElementalList\Model\ElementList + - DNADesign\Elemental\Models\ElementContent + - DNADesign\ElementalUserForms\Model\ElementForm + - Dynamic\Elements\Image\Elements\ElementImage + - Dynamic\Elements\Blog\Elements\ElementBlogPosts + - Dynamic\Elements\Oembed\Elements\ElementOembed + - Dynamic\Elements\Elements\ElementTestimonials + #- A2nt\ElementalBasics\Elements\TeamMembersElement + - A2nt\ElementalBasics\Elements\SliderElement + - A2nt\ElementalBasics\Elements\MapElement + - A2nt\ElementalBasics\Elements\AccordionElement + - A2nt\ElementalBasics\Elements\CustomSnippetElement + - A2nt\ElementalBasics\Elements\InstagramElement + styles: + whiteframe: 'White Frame' + noframe: 'No Frame' diff --git a/_config/options-widgets.yml b/_config/options-widgets.yml new file mode 100755 index 0000000..a0f1ab1 --- /dev/null +++ b/_config/options-widgets.yml @@ -0,0 +1,33 @@ +--- +Name: webapp-options-widgets +--- +# Blog + Widgets module extensions +Page: + extensions: + - A2nt\CMSNiceties\Widgets\WidgetPageExtension + +SilverStripe\Blog\Model\Blog: + extensions: + - A2nt\CMSNiceties\Extensions\BlogExtension +SilverStripe\Blog\Model\BlogPost: + extensions: + - A2nt\CMSNiceties\Extensions\BlogPostExtension + +SilverStripe\Widgets\Model\Widget: + icon: '' + extensions: + - A2nt\CMSNiceties\Widgets\WidgetExtension +SilverStripe\Blog\Widgets\BlogArchiveWidget: + icon: '' +SilverStripe\Blog\Widgets\BlogCategoriesWidget: + icon: '' +SilverStripe\Blog\Widgets\BlogFeaturedPostsWidget: + icon: '' +SilverStripe\Blog\Widgets\BlogRecentPostsWidget: + icon: '' +SilverStripe\Blog\Widgets\BlogTagsCloudWidget: + icon: '' +SilverStripe\Blog\Widgets\BlogTagsWidget: + icon: '' + only_available_in: + - CMSMain_HiddenClass diff --git a/_config/shop.yml_ b/_config/shop.yml_ new file mode 100755 index 0000000..41ac173 --- /dev/null +++ b/_config/shop.yml_ @@ -0,0 +1,23 @@ +--- +Name: webapp-shop +--- +SilverStripe\Core\Injector\Injector: + SilverShop\Checkout\SinglePageCheckoutComponentConfig: + class: A2nt\CMSNiceties\Models\CheckoutNoDeliveryConfig + +SilverShop\Extension\ShopConfigExtension: + base_currency: USD + +SilverShop\Model\Address: + extensions: + - A2nt\CMSNiceties\Extensions\AddressExtension + +SilverShop\Cart\ShoppingCartController: + extensions: + - A2nt\CMSNiceties\Extensions\ShoppingCartControllerExtension + +A2nt\CMSNiceties\Templates\DeferedRequirements: + custom_requirements: + SilverShop\Page\AccountPageController: + - SilverShop.Page.CheckoutPageController.js + - SilverShop.Page.CheckoutPageController.css diff --git a/_config/templates-requirements.yml b/_config/templates-requirements.yml new file mode 100755 index 0000000..0691bce --- /dev/null +++ b/_config/templates-requirements.yml @@ -0,0 +1,41 @@ +--- +Name: webapp-templates-requirements +--- +App\Templates\DeferredRequirements: + nofontawesome: false + version: false + static_domain: false + deferred: true + noreact: false + nojquery: true + jquery_version: '3.4.1' + +SilverStripe\FontAwesome\FontAwesomeField: + version: '5.12.0' + +SilverStripe\View\Requirements: + disable_flush_combined: true +SilverStripe\View\Requirements_Backend: + combine_in_dev: true + combine_hash_querystring: true + default_combined_files_folder: 'combined' +SilverStripe\Core\Injector\Injector: + # Create adapter that points to the custom directory root + SilverStripe\Assets\Flysystem\PublicAdapter.custom-adapter: + class: SilverStripe\Assets\Flysystem\PublicAssetAdapter + constructor: + Root: ./app/javascript + # Set flysystem filesystem that uses this adapter + League\Flysystem\Filesystem.custom-filesystem: + class: 'League\Flysystem\Filesystem' + constructor: + Adapter: '%$SilverStripe\Assets\Flysystem\PublicAdapter.custom-adapter' + # Create handler to generate assets using this filesystem + SilverStripe\Assets\Storage\GeneratedAssetHandler.custom-generated-assets: + class: SilverStripe\Assets\Flysystem\GeneratedAssets + properties: + Filesystem: '%$League\Flysystem\Filesystem.custom-filesystem' + # Assign this generator to the requirements builder + SilverStripe\View\Requirements_Backend: + properties: + AssetHandler: '%$SilverStripe\Assets\Storage\GeneratedAssetHandler.custom-generated-assets' diff --git a/_config/templates-themes.yml b/_config/templates-themes.yml new file mode 100755 index 0000000..c26bf12 --- /dev/null +++ b/_config/templates-themes.yml @@ -0,0 +1,19 @@ +--- +Name: webapp-templates-themes +After: + - webapp-options-elements +--- +SilverStripe\View\SSViewer: + source_file_comments: true + themes: + - '$public' + - '$default' + +App\Elements\SliderElement: + slide_width: 2140 + slide_height: 700 + +# 2x container width to automatically resize images for 2K display +App\Elements\Extensions\ElementRows: + container_max_width: 2280 + column_class: 'col-block col-md' diff --git a/_config/webpack.yml b/_config/webpack.yml new file mode 100755 index 0000000..a358a90 --- /dev/null +++ b/_config/webpack.yml @@ -0,0 +1,21 @@ +# Name: webapp-webpack +# that's important to place this file into /app/_config/webpack.yml +# with all configuration variables presented +# Cuz WebPack compiling script use it to set configuration + +A2nt\CMSNiceties\Templates\WebpackTemplateProvider: + APPDIR: './app' + THEMESDIR: './themes' + HOSTNAME: 127.0.0.1 + PORT: 3000 + SRC: client/src + DIST: client/dist + TYPESJS: client/src/js/types + TYPESSCSS: client/src/scss/types + webp: false + NODE_ENV: production #production,development + HTTPS: true + injectClient: true + GRAPHQL_URL: '/graphql' + GRAPHQL_API_KEY: 'LgPaRkVPYa8IY7x3AjbLC8wx6oPPSlO01yPflFXecvQ' + #STATIC_URL: 'http://127.0.0.1' diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..e515f4d --- /dev/null +++ b/composer.json @@ -0,0 +1,27 @@ +{ + "name": "a2nt/cms-niceties", + "description": "Some useful CMS updates", + "type": "silverstripe-vendormodule", + "keywords": [ + "silverstripe", + "elemental" + ], + "license": "BSD-3-Clause", + "authors": [{ + "name": "Tony Air", + "email": "tony@twma.pro" + }], + "minimum-stability": "dev", + "require": { + "silverstripe/cms": "^4", + "a2nt/silverstripe-elemental-basics": "*" + }, + "require-dev": { + "phpunit/phpunit": "^5.7" + }, + "autoload": { + "psr-4": { + "A2nt\\ElementalBasics\\": "src/" + } + } +} diff --git a/lang/en.yml b/lang/en.yml new file mode 100755 index 0000000..5e70948 --- /dev/null +++ b/lang/en.yml @@ -0,0 +1,31 @@ +en: + SilverStripe\Security\Member: + SURNAME: 'Last Name' + db_Surname: 'Last Name' + SUBJECTPASSWORDRESET: 'Your password reset link' + ENTEREMAIL: 'Please enter an email address to get a password reset link.' + PASSWORDRESETSENTHEADER: 'Password reset link sent' + PASSWORDRESETSENTTEXT: 'Thank you. A reset link has been sent, provided an account exists for this email address.' + Page: + LOADINGTEXT: 'LOADING ..' + JAVASCRIPTREQUIRED: 'Please, enable javascript!' + UPGRADEBROWSER: 'Upgrade your browser' + OUTDATEDBROWSER: 'You are using an outdated browser. For a faster, safer browsing experience, upgrade for free today.' + SilverShop\Model\Address: + db_Surname: 'Last Name' + SilverShop\Model\Order: + db_Surname: 'Last Name' + SilverShop\Page\CheckoutPage: + ProceedToPayment: 'Send Order to Store' + UndefinedOffset\NoCaptcha\Forms\NocaptchaField: + EMPTY: 'Please prove you are human - check the Captcha box.' + NOSCRIPT: 'You must enable JavaScript to submit this form' + VALIDATE_ERROR: 'Captcha could not be validated' + Dynamic\FlexSlider\Model\SlideImage: + SINGULARNAME: 'Slide' + PLURALNAME: 'Slides' + Addressable: + SUBURB: 'City' + Symbiote\AddressableAddressable: + SUBURB: 'City' + diff --git a/lang/ru.yml b/lang/ru.yml new file mode 100755 index 0000000..8804957 --- /dev/null +++ b/lang/ru.yml @@ -0,0 +1,19 @@ +ru: + Page: + LOADINGTEXT: "ЗАГРУЗКА .." + JAVASCRIPTREQUIRED: "Для корректной работы страницы требуется включить Javascript!" + UPGRADEBROWSER: "Обновите браузер!" + OUTDATEDBROWSER: "Вы используете устаревшую версию браузера. Обновите ваш браузер сейчас для повышения уровня безопасности вашей системы." + PaginationLabel: "Переключение страниц" + PaginationPrevious: "Назад" + PaginationNext: "Вперед" + SilverStripe\Blog\Model\Blog: + PostedIn: "Категории" + Tagged: "Отметки" + Comments: "Коментарии" + Posted: "Опубликовано" + By: " - " + AND: "и" + LessThanAMinuteToRead: "Меньше минуты на чтение" + MinutesToRead: "Минут на чтение" + READMORE: "Подробнее" diff --git a/src/Extensions/AddressExtension.php b/src/Extensions/AddressExtension.php new file mode 100755 index 0000000..a6e439c --- /dev/null +++ b/src/Extensions/AddressExtension.php @@ -0,0 +1,29 @@ +push($field); + $fields->remove($field); + } + + $holder->addExtraClass('col-sm-6'); + $fields->push($holder); + } +} diff --git a/src/Extensions/BlogExtension.php b/src/Extensions/BlogExtension.php new file mode 100755 index 0000000..53c1c4f --- /dev/null +++ b/src/Extensions/BlogExtension.php @@ -0,0 +1,19 @@ +dataFieldByName('ChildPages'); + if ($f) { + $f->setConfig(GridFieldConfigBlogPost::create(75)); + } + } +} diff --git a/src/Extensions/BlogPostExtension.php b/src/Extensions/BlogPostExtension.php new file mode 100755 index 0000000..7a2d7cf --- /dev/null +++ b/src/Extensions/BlogPostExtension.php @@ -0,0 +1,27 @@ + 'Boolean(0)', + ]; + + public function updateCMSFields(FieldList $fields) + { + $mainTab = $fields->findOrMakeTab('Root.Main'); + $mainTab->push(CheckboxField::create('Featured')); + } +} diff --git a/src/Extensions/CMSMain_HiddenClass.php b/src/Extensions/CMSMain_HiddenClass.php new file mode 100755 index 0000000..87162bc --- /dev/null +++ b/src/Extensions/CMSMain_HiddenClass.php @@ -0,0 +1,17 @@ +getName(); + + $fields = [ + CheckboxField::create( + $name . '[autoplay]', + _t(self::CLASS.'AUTOPLAY', 'Autoplay video?') + )->setValue($this->object->getField('Autoplay')), + + CheckboxField::create( + $name . '[loop]', + _t(self::CLASS.'LOOP', 'Loop video?') + )->setValue($this->object->getField('Loop')), + + CheckboxField::create( + $name.'[controls]', + _t(self::CLASS.'CONTROLS', 'Show player controls?') + )->setValue($this->object->getField('Controls')) + ]; + + return CompositeField::create(array_merge([ + LiteralField::create( + $name.'Options', + parent::FieldHolder($properties) + ) + ], $fields)); + } +} diff --git a/src/Extensions/EmbedShortcodeProvider.php b/src/Extensions/EmbedShortcodeProvider.php new file mode 100755 index 0000000..4ee503f --- /dev/null +++ b/src/Extensions/EmbedShortcodeProvider.php @@ -0,0 +1,52 @@ + + if (!empty($arguments['caption'])) { + $xmlCaption = Convert::raw2xml($arguments['caption']); + $content .= "\n

{$xmlCaption}

"; + } + + // Convert arguments to data-*argument_name* + foreach ($arguments as $k => $v) { + if($k === 'class' || $k === 'style') { + continue; + } + + unset($arguments[$k]); + $arguments['data-'.$k] = $v; + } + + $arguments['class'] .= ' embed-youtube embed-responsive embed-responsive-16by9'; + + $iframe = strpos($content, 'iframe'); + if($iframe >= 0) { + $content = substr($content, 0, $iframe+6).' class="embed-responsive-item" '.substr($content, $iframe +7); + } + + return HTML::createTag('div', $arguments, $content); + } +} diff --git a/src/Extensions/EmbeddedObjectExtension.php b/src/Extensions/EmbeddedObjectExtension.php new file mode 100755 index 0000000..93477c8 --- /dev/null +++ b/src/Extensions/EmbeddedObjectExtension.php @@ -0,0 +1,136 @@ + 'Boolean(0)', + 'Loop' => 'Boolean(0)', + 'Controls' => 'Boolean(1)', + ]; + + public function Embed() + { + $this->owner->Embed(); + $this->setEmbedParams(); + + return $this->owner; + } + + public function setEmbedParams($params = []) + { + $iframe_params = []; + if ($this->owner->getField('Autoplay')) { + $iframe_params[] = 'allow="autoplay"'; + } + + // YouTube params + if (stripos($this->owner->EmbedHTML, 'https://www.youtube.com/embed/') > 0) { + $url = $this->owner->getField('SourceURL'); + preg_match( + '/^(?:http(?:s)?:\/\/)?(?:www\.)?(?:m\.)?(?:youtu\.be\/|youtube\.com\/(?:(?:watch)?\?(?:.*&)?v(?:i)?=|(?:embed|v|vi|user)\/))([^\?&"\'>]+)/', + $url, + $matches + ); + if (isset($matches[1])) { + $videoID = $matches[1]; + + $params = array_merge($params, [ + 'feature=oembed', + 'wmode=transparent', + 'enablejsapi=1', + 'disablekb=1', + 'iv_load_policy=3', + 'modestbranding=1', + 'rel=0', + 'showinfo=0', + //'controls='.($this->owner->getField('Controls') ? '1': '0') + ]); + + if ($this->owner->getField('Autoplay')) { + $params[] = 'autoplay=1'; + $params[] = 'mute=1'; + } + + if ($this->owner->getField('Loop')) { + $params[] = 'loop=1'; + $params[] = 'playlist=' . $videoID; + } + + $this->owner->EmbedHTML = preg_replace( + '/src="([A-z0-9:\/\.]+)\??(.*?)"/', + 'src="https://www.youtube.com/embed/' . $videoID . '?' . implode('&', $params) . '" ' + . implode(' ', $iframe_params), + $this->owner->EmbedHTML + ); + } + } + + if (stripos($this->owner->EmbedHTML, 'https://player.vimeo.com/video/') > 0) { + $url = $this->owner->getField('SourceURL'); + preg_match( + '/^https:\/\/vimeo\.com\/([A-z0-9]+)/', + $url, + $matches + ); + $videoID = $matches[1]; + + $params = array_merge($params, [ + 'controls='.($this->owner->getField('Controls') ? '1': '0'), + 'background=1', + ]); + + if ($this->owner->getField('Autoplay')) { + $params[] = 'autoplay=1'; + } + + if ($this->owner->getField('Loop')) { + $params[] = 'loop=1'; + } + $this->owner->EmbedHTML = preg_replace( + '/src="([A-z0-9:\/\.]+)\??(.*?)"/', + 'src="https://player.vimeo.com/video/'.$videoID.'?' . implode('&', $params) . '" ' + .implode(' ', $iframe_params), + $this->owner->EmbedHTML + ); + } + } + + public function updateCMSFields(FieldList $fields) + { + parent::updateCMSFields($fields); + + $fields->removeByName([ + 'Width', 'Height', 'EmbedHTML', 'ThumbURL', + 'Autoplay', 'Loop', 'Controls', + 'ExtraClass', 'Type', + ]); + + $fields->addFieldsToTab('Root.Extra', [ + CheckboxField::create('Autoplay'), + CheckboxField::create('Loop'), + CheckboxField::create('Controls'), + NumericField::create('Width'), + NumericField::create('Height'), + TextareaField::create('EmbedHTML'), + TextField::create('ThumbURL'), + TextField::create('ExtraClass'), + TextField::create('Type'), + ]); + } + + public function onBeforeWrite() + { + parent::onBeforeWrite(); + $this->setEmbedParams(); + } +} diff --git a/src/Extensions/HtmlEditorFieldExtension.php b/src/Extensions/HtmlEditorFieldExtension.php new file mode 100755 index 0000000..c436d05 --- /dev/null +++ b/src/Extensions/HtmlEditorFieldExtension.php @@ -0,0 +1,19 @@ +byID($page_id)->URLSegment; + + $computerUploadField = $form->Fields()->dataFieldByName('AssetUploadField'); + $computerUploadField->setFolderName(sprintf("%s/images/%s", 'Uploads', $page_urlsegment)); + } +} diff --git a/src/Extensions/ImageExtension.php b/src/Extensions/ImageExtension.php new file mode 100755 index 0000000..a3aef40 --- /dev/null +++ b/src/Extensions/ImageExtension.php @@ -0,0 +1,23 @@ +removeByName([ + 'Filename', + ]);*/ + } +} diff --git a/src/Extensions/LeftAndMainExtension.php b/src/Extensions/LeftAndMainExtension.php new file mode 100755 index 0000000..943f15c --- /dev/null +++ b/src/Extensions/LeftAndMainExtension.php @@ -0,0 +1,26 @@ +get(DeferredRequirements::class); + // App libs + if (!$config['nofontawesome']) { + $v = !isset($config['fontawesome_version']) || !$config['fontawesome_version'] + ? Config::inst()->get(FontAwesomeField::class, 'version') + : $config['fontawesome_version']; + + Requirements::css('//use.fontawesome.com/releases/v'.$v.'/css/all.css'); + } + } +} diff --git a/src/Extensions/LostPasswordHandlerExtension.php b/src/Extensions/LostPasswordHandlerExtension.php new file mode 100755 index 0000000..35407cc --- /dev/null +++ b/src/Extensions/LostPasswordHandlerExtension.php @@ -0,0 +1,69 @@ + 'passwordsent', + ]; + + private static $allowed_actions = [ + 'passwordsent', + ]; + + /** + * Show the "password sent" page, after a user has requested + * to reset their password. + * + * @return array + */ + public function passwordsent() + { + $message = _t( + 'SilverStripe\\Security\\Security.PASSWORDRESETSENTTEXT', + "Thank you. A reset link has been sent, provided an account exists for this email address." + ); + + $email = $this->getRequest()->getVar('email'); + $message = $email + ? 'Thank you! A reset link has been sent to \''.$email.'\', provided an account exists for this email address.' + : $message; + + return [ + 'Title' => _t( + 'SilverStripe\\Security\\Security.PASSWORDRESETSENTHEADER', + "Password reset link sent".($email ? ' to \''.$email.'\'' : '') + ), + 'ElementalArea' => DBField::create_field('HTMLFragment', "

$message

"), + ]; + } + + /** + * Avoid information disclosure by displaying the same status, regardless wether the email address actually exists + * + * @param array $data + * @return HTTPResponse + */ + protected function redirectToSuccess(array $data) + { + $link = $this->link('passwordsent').'?email='.$data['Email']; + + return $this->redirect($this->addBackURLParam($link)); + } +} diff --git a/src/Extensions/NotificationsExtension.php b/src/Extensions/NotificationsExtension.php new file mode 100755 index 0000000..dd95c3f --- /dev/null +++ b/src/Extensions/NotificationsExtension.php @@ -0,0 +1,98 @@ + 'Boolean(1)', + ]; + + private static $has_many = [ + 'Notifications' => Notification::class, + ]; + + public function updateCMSFields(FieldList $fields) + { + $tab = $fields->findOrMakeTab('Root.Notifications'); + + if(!$this->owner->exists()) { + $tab->push(LiteralField::create( + 'NotificationsNotice', + '

The object must be saved before notifications can be added

' + )); + + return null; + } + + $items = $this->owner->Notifications(); + + $config = GridFieldConfig::create(); + $config->addComponents([ + new GridFieldToolbarHeader(), + new GridFieldTitleHeader(), + new GridFieldEditableColumns(), + new GridFieldAddNewInlineButton('toolbar-header-right'), + new GridFieldDetailForm(), + new GridFieldEditButton(), + new GridFieldDeleteAction(), + ]); + + $tab->setChildren(FieldList::create( + HeaderField::create('NotificationsHeader','Notifications'), + LiteralField::create( + 'CurrentNotifications', + 'Current:' + .$this->owner->renderWith('App\\Objects\\NotificationsList') + ), + CheckboxField::create('ShowNotifications'), + GridField::create( + 'Notifications', + '', + $items, + $config + ) + )); + } + + public function NotificationsToday() + { + $items = $this->owner->Notifications(); + $time = time(); + + return $items->filter([ + 'DateOn:LessThanOrEqual' => date('Y-m-d', $time), + 'DateOff:GreaterThanOrEqual' => date('Y-m-d', $time), + ]); + } +} diff --git a/src/Extensions/OpenningHoursExtension.php b/src/Extensions/OpenningHoursExtension.php new file mode 100755 index 0000000..5a26fc8 --- /dev/null +++ b/src/Extensions/OpenningHoursExtension.php @@ -0,0 +1,189 @@ + 'Boolean(1)', + 'OpenningHoursNote' => 'Varchar(255)', + ]; + + private static $has_one = [ + 'OpeningHoursPage' => SiteTree::class, + ]; + + private static $has_many = [ + 'OpeningHours' => OpeningHour::class, + 'Holidays' => Holiday::class, + ]; + + public function HoursLink() + { + return $this->owner->OpeningHoursPage()->Link(); + } + + public function updateCMSFields(FieldList $fields) + { + $tab = $fields->findOrMakeTab('Root.OpeningHours'); + + if(!$this->owner->exists()) { + $tab->push(LiteralField::create( + 'OpeningHoursNotice', + '

The object must be saved before opening hours can be added

' + )); + + return null; + } + + $hours = $this->owner->OpeningHours(); + + $config = GridFieldConfig::create(); + $config->addComponents([ + new GridFieldToolbarHeader(), + new GridFieldTitleHeader(), + new GridFieldEditableColumns(), + new GridFieldAddNewInlineButton('toolbar-header-right'), + new GridFieldDeleteAction(), + ]); + + $tab->setChildren(FieldList::create( + HeaderField::create('OpeningHours','Opening Hours'), + LiteralField::create( + 'CurrentOpeningHour', + 'Today:' + .'

' + .$this->owner->renderWith('App\\Objects\\OpeningHoursList') + .'

' + ), + CheckboxField::create('ShowOpeningHours'), + DropdownField::create( + 'OpeningHoursPageID', + 'Opening Hours Page', + SiteTree::get()->map()->toArray() + ), + /*TextareaField::create('OpenningHoursNote'), + LiteralField::create( + 'OpeningHoursNote', + '

Please, specify time ranges. For example:
' + .'Monday 10:00 AM - 2:00 PM
' + .'Monday 3:00 PM - 6:00 PM
' + .'Tuesday 12:00 AM - 2:00 PM
' + .'Tuesday 3:00 PM - 4:00 PM
' + .'...
' + .'Short day example durring holidays:
' + .'Monday 12:00 AM - 2:00 PM 12/31/2018 - 01/06/2019' + .'

' + ),*/ + GridField::create( + 'OpeningHours', + 'Opening Hours', + $hours, + $config + ) + )); + + $tab = $fields->findOrMakeTab('Root.Holidays'); + $tab->push(GridField::create( + 'Holidays', + 'Holidays', + $this->owner->Holidays(), + $config + )); + } + + /** + * Get the opening hours + * + * @return OpeningHour|DataObject|null + */ + public function OpeningHoursToday() + { + $hours = $this->owner->OpeningHours(); + $time = time(); + + $today = $hours->filter([ + 'Day' => date('l', $time), + 'DisplayStart:LessThanOrEqual' => date('Y-m-d', $time), + 'DisplayEnd:GreaterThanOrEqual' => date('Y-m-d', $time), + ]); + + return $today->exists() ? $today : $hours->filter([ + 'Day' => date('l', $time), + 'DisplayStart' => null, + 'DisplayEnd' => null, + ]); + } + + public function OpeningHoursJSON() + { + $hours = $this->owner->OpeningHours(); + $result = []; + foreach ($hours as $hour) { + $from = str_replace(':00', '', date('g:i a', strtotime($hour->getField('From')))); + $till = str_replace(':00', '', date('g:i a', strtotime($hour->getField('Till')))); + + $result['days'][$hour->getField('Day')][] = [ + 'From' => $from, + 'Till' => $till, + 'DisplayStart' => $hour->getField('DisplayStart'), + 'DisplayEnd' => $hour->getField('DisplayEnd'), + ]; + } + + $holidays = $this->owner->Holidays(); + foreach ($holidays as $holiday) { + $result['holidays'][$holiday->getField('Date')] = $holiday->getField('Title'); + } + + return json_encode($result); + } + + public function onBeforeWrite() + { + parent::onBeforeWrite(); + if ($this->owner->exists() && !$this->owner->OpeningHours()->exists()) { + $this->createOpeningHours(); + } + } + + /** + * Set up the opening hours for each day of the week + */ + private function createOpeningHours() + { + $days = OpeningHour::singleton()->dbObject('Day')->enumValues(); + foreach ($days as $day) { + $openingHour = OpeningHour::create(); + $openingHour->Day = $day; + $this->owner->OpeningHours()->add($openingHour); + } + } +} diff --git a/src/Extensions/PageFluentExtension.php b/src/Extensions/PageFluentExtension.php new file mode 100755 index 0000000..b418957 --- /dev/null +++ b/src/Extensions/PageFluentExtension.php @@ -0,0 +1,28 @@ +owner->isPublishedInLocale()) { + $query = '"' . $table . '_Localised_' . $locale->getLocale() . '"."' . $field . '"'; + } + } +} diff --git a/src/Extensions/PlaceholderFormExtension.php b/src/Extensions/PlaceholderFormExtension.php new file mode 100755 index 0000000..ded63dd --- /dev/null +++ b/src/Extensions/PlaceholderFormExtension.php @@ -0,0 +1,37 @@ +setPlaceholder($field); + } + } + + private function setPlaceholder($field) + { + if (is_a($field, TextField::class)) { + $field->setAttribute( + 'placeholder', + $field->Title() + .($field->hasClass('requiredField') ? '*' : '') + ); + $field->setTitle(''); + } + + if (is_a($field, CompositeField::class)) { + $children = $field->getChildren(); + foreach ($children as $child) { + $this->setPlaceholder($child); + } + } + } +} diff --git a/src/Extensions/ShoppingCartControllerExtension.php b/src/Extensions/ShoppingCartControllerExtension.php new file mode 100755 index 0000000..9cd2079 --- /dev/null +++ b/src/Extensions/ShoppingCartControllerExtension.php @@ -0,0 +1,24 @@ + 'Text', + 'Longitude' => 'Decimal(10, 8)', + 'Latitude' => 'Decimal(11, 8)', + 'MapZoom' => 'Int', + //'MapAPIKey' => 'Varchar(255)', + 'Description' => 'Varchar(255)', + 'Address' => 'Varchar(255)', + 'Suburb' => 'Varchar(255)', + 'State' => 'Varchar(255)', + 'ZipCode' => 'Varchar(6)', + ]; + + private static $has_one = [ + 'PrivacyPolicy' => SiteTree::class, + 'Sitemap' => SiteTree::class, + ]; + + private static $many_many = [ + 'Navigation' => SiteTree::class, + ]; + + public function updateCMSFields(FieldList $fields) + { + $img = Image::get()->filter([ + 'ParentID' => 0, + 'FileFilename' => 'qrcode.png', + ])->first(); + if ($img) { + $fields->addFieldsToTab('Root.Main', [ + LiteralField::create('QRCode', 'QR code'), + ]); + } + + $fields->addFieldsToTab('Root.Main', [ + TreeMultiselectField::create( + 'Navigation', + 'Navigation', + SiteTree::class + )->setDisableFunction(static function ($el) { + return $el->getField('ParentID') !== 0; + }), + TextareaField::create('Description', 'Website Description'), + TextareaField::create('ExtraCode', 'Extra site-wide HTML code'), + DropdownField::create( + 'PrivacyPolicyID', + 'Privacy Policy Page', + SiteTree::get()->map()->toArray() + )->setEmptyString('(Select one)'), + DropdownField::create( + 'SitemapID', + 'Sitemap Page', + SitemapPage::get()->map()->toArray() + )->setEmptyString('(Select one)'), + ]); + + $mapTab = $fields->findOrMakeTab('Root.Maps'); + $mapTab->setTitle('Address / Map'); + + $fields->addFieldsToTab('Root.Maps', [ + TextField::create('Address'), + TextField::create('Suburb', 'City'), + TextField::create('State'), + TextField::create('ZipCode'), + ]); + + if (MapboxField::getAccessToken()) { + $fields->addFieldsToTab('Root.Maps', [ + //TextField::create('MapAPIKey'), + TextField::create('MapZoom'), + MapboxField::create('Map', 'Choose a location', 'Latitude', 'Longitude'), + ]); + } else { + $fields->addFieldsToTab('Root.Maps', [ + LiteralField::create('MapNotice', '

No Map API keys specified.

') + ]); + } + + /*GoogleMapField::create( + $this->owner, + 'Location', + [ + 'show_search_box' => true, + ] + )*/ + } + + public function MapAPIKey() + { + return MapboxField::config()->get('access_token'); + } + + public function MapStyle() + { + return MapboxField::config()->get('map_style'); + } + + public function getGeoJSON() + { + return '{"type": "MarkerCollection","features": [{"type": "Feature","icon": "",' + .'"properties": {"content": "'.$this->owner->getTitle().'"},"geometry": {"type": "Point",' + .'"coordinates": ['.$this->owner->getField('Longitude').','.$this->owner->getField('Latitude').']}}]}'; + } + + public function DirectionsLink() + { + return '' + .' Get Directions'; + } + + public function getLatestBlogPosts() + { + return BlogPost::get()->sort('PublishDate DESC'); + } +} diff --git a/src/Extensions/SiteMemberLoginForm.php b/src/Extensions/SiteMemberLoginForm.php new file mode 100644 index 0000000..ac33484 --- /dev/null +++ b/src/Extensions/SiteMemberLoginForm.php @@ -0,0 +1,51 @@ +Fields(); + $actions = $this->Actions(); + + $email = $fields->fieldByName('Email'); + if ($email) { + $email + ->setAttribute('placeholder', 'your@email.com') + ->setAttribute('autocomplete', 'email') + ->setAttribute('type', 'email'); + } + + $pass = $fields->fieldByName('Password'); + if($pass) { + //$pass->setAttribute('autocomplete', 'current-password'); + $pass->setAttribute('placeholder', '**********'); + $pass->setAutofocus(true); + } + + $btn = $actions->fieldByName('action_doLogin'); + if($btn) { + $btn->setUseButtonTag(true); + $btn->setButtonContent(' '.$btn->Title()); + $btn->addExtraClass('btn-lg'); + } + + if (Director::isLive()) { + $this->enableSpamProtection(); + } + } +} diff --git a/src/Extensions/SiteTreeExtension.php b/src/Extensions/SiteTreeExtension.php new file mode 100755 index 0000000..76cda6b --- /dev/null +++ b/src/Extensions/SiteTreeExtension.php @@ -0,0 +1,24 @@ + 'Text', + ]; + + public function updateSettingsFields(FieldList $fields) + { + $fields->addFieldsToTab('Root.Settings', [ + TextareaField::create( + 'ExtraCode', + 'Extra page specific HTML code' + ), + ]); + } +} diff --git a/src/Extensions/SlideImageExtension.php b/src/Extensions/SlideImageExtension.php new file mode 100755 index 0000000..941be09 --- /dev/null +++ b/src/Extensions/SlideImageExtension.php @@ -0,0 +1,74 @@ + 'Boolean(0)', + 'DateOn' => 'Datetime', + 'DateOff' => 'Datetime', + ]; + + private $_cache = []; + + public function getElement() + { + if(!isset($this->_cache['element'])) { + $this->_cache['element'] = $this->owner->SlideshowElement(); + } + + return $this->_cache['element']; + } + + public function getSlideWidth() + { + $element = $this->getElement(); + return $element->getSlideWidth(); + } + + public function getSlideHeight() + { + $element = $this->getElement(); + return $element->getSlideHeight(); + } + + public function updateCMSFields(FieldList $fields) + { + parent::updateCMSFields($fields); + + $fields->removeByName([ + 'PageLinkID', + 'Hide', + 'DateOn', + 'DateOff', + ]); + + + $fields->dataFieldByName('Image') + ->setTitle('Image ('.$this->getSlideWidth().' x '.$this->getSlideHeight().' px)'); + + $fields->addFieldToTab('Root.Main', ToggleCompositeField::create( + 'ConfigHD', + 'Slide Settings', + [ + CheckboxField::create('Hide', 'Hide this slide? (That will hide the slide regardless of start/end fields)'), + DatetimeField::create('DateOn', 'When would you like to start showing the slide?'), + DatetimeField::create('DateOff', 'When would you like to stop showing the slide?'), + ] + )); + } +} diff --git a/src/Extensions/SocialExtension.php b/src/Extensions/SocialExtension.php new file mode 100755 index 0000000..3cde299 --- /dev/null +++ b/src/Extensions/SocialExtension.php @@ -0,0 +1,75 @@ + 'Varchar(255)', + ]; + + private static $has_one = [ + 'Facebook' => Link::class, + 'LinkedIn' => Link::class, + 'Pinterest' => Link::class, + 'Instagram' => Link::class, + 'Twitter' => Link::class, + 'PublicEmail' => Link::class, + 'PhoneNumber' => Link::class, + ]; + + public function updateCMSFields(FieldList $fields) + { + parent::updateCMSFields($fields); + + $linkFields = [ + LinkField::create('FacebookID'), + LinkField::create('LinkedInID'), + LinkField::create('PinterestID'), + LinkField::create('InstagramID'), + LinkField::create('TwitterID'), + ]; + + foreach ($linkFields as $field) { + $field->setAllowedTypes(['URL']); + } + + $fields->findOrMakeTab('Root.Social'); + + $fields->addFieldsToTab('Root.Social', [ + LinkField::create('PublicEmailID', 'Public Email') + ->setAllowedTypes(['Email']), + LinkField::create('PhoneNumberID', 'Phone Number') + ->setAllowedTypes(['Phone']), + ]); + + $fields->addFieldsToTab('Root.Social', $linkFields); + } + + public static function byPhone($phone) + { + $links = Link::get()->filter('Phone', $phone); + + if ($links->exists()) { + return Member::get()->filter( + 'PhoneNumberID', + array_keys($links->map('ID', 'Title')->toArray()) + )->first(); + } + + return null; + } +} diff --git a/src/Extensions/UserDefinedForm_HiddenClass.php b/src/Extensions/UserDefinedForm_HiddenClass.php new file mode 100644 index 0000000..a95fd3b --- /dev/null +++ b/src/Extensions/UserDefinedForm_HiddenClass.php @@ -0,0 +1,10 @@ + + */ + +use SilverStripe\Forms\GridField\GridField; +use SilverStripe\Forms\GridField\GridField_HTMLProvider; +use SilverStripe\Forms\GridField\GridField_ActionProvider; +use SilverStripe\Forms\GridField\GridField_FormAction; +use SilverStripe\Forms\GridField\GridField_SaveHandler; +use SilverStripe\Control\Controller; + +class SaveAllButton implements GridField_HTMLProvider, GridField_ActionProvider +{ + protected $targetFragment; + protected $actionName = 'saveallrecords'; + + public $buttonName; + + public $publish = true; + + public $completeMessage; + + public $removeChangeFlagOnFormOnSave = false; + + public function setButtonName($name) + { + $this->buttonName = $name; + return $this; + } + + public function setRemoveChangeFlagOnFormOnSave($flag) + { + $this->removeChangeFlagOnFormOnSave = $flag; + return $this; + } + + public function __construct($targetFragment = 'before', $publish = true, $action = 'saveallrecords') + { + $this->targetFragment = $targetFragment; + $this->publish = $publish; + $this->actionName = $action; + } + + public function getHTMLFragments($gridField) + { + $singleton = singleton($gridField->getModelClass()); + + if (!$singleton->canEdit() && !$singleton->canCreate()) { + return []; + } + + if (!$this->buttonName) { + if ($this->publish && $singleton->hasExtension('Versioned')) { + $this->buttonName = _t('GridField.SAVE_ALL_AND_PUBLISH', 'Save all and publish'); + } else { + $this->buttonName = _t('GridField.SAVE_ALL', 'Save all'); + } + } + + $button = GridField_FormAction::create( + $gridField, + $this->actionName, + $this->buttonName, + $this->actionName, + null + ); + + $button->setAttribute('data-icon', 'disk')->addExtraClass('new new-link ui-button-text-icon-primary'); + + if ($this->removeChangeFlagOnFormOnSave) { + $button->addExtraClass('js-mwm-gridfield--saveall'); + } + + return [ + $this->targetFragment => $button->Field(), + ]; + } + + public function getActions($gridField) + { + return [$this->actionName]; + } + + public function handleAction(GridField $gridField, $actionName, $arguments, $data) + { + if ($actionName == $this->actionName) { + return $this->saveAllRecords($gridField, $arguments, $data); + } + } + + protected function saveAllRecords(GridField $grid, $arguments, $data) + { + if (isset($data[$grid->Name])) { + $currValue = $grid->Value(); + $grid->setValue($data[$grid->Name]); + $model = singleton($grid->List->dataClass()); + + foreach ($grid->getConfig()->getComponents() as $component) { + if ($component instanceof GridField_SaveHandler) { + $component->handleSave($grid, $model); + } + } + + if ($this->publish) { + // Only use the viewable list items, since bulk publishing can take a toll on the system + $list = ($paginator = $grid->getConfig()->getComponentByType('GridFieldPaginator')) ? $paginator->getManipulatedData($grid, $grid->List) : $grid->List; + + $list->each( + function ($item) { + if ($item->hasExtension('Versioned')) { + $item->writeToStage('Stage'); + $item->publish('Stage', 'Live'); + } + } + ); + } + + if ($model->exists()) { + $model->delete(); + $model->destroy(); + } + + $grid->setValue($currValue); + + if (Controller::curr() && $response = Controller::curr()->Response) { + if (!$this->completeMessage) { + $this->completeMessage = _t('GridField.DONE', 'Done.'); + } + + $response->addHeader('X-Status', rawurlencode($this->completeMessage)); + } + } + } +} diff --git a/src/GraphQL/APIKeyAuthenticator.php b/src/GraphQL/APIKeyAuthenticator.php new file mode 100644 index 0000000..cef6b97 --- /dev/null +++ b/src/GraphQL/APIKeyAuthenticator.php @@ -0,0 +1,46 @@ +getHeader('apikey') !== WebpackTemplateProvider::config()['GRAPHQL_API_KEY'] + ) { + if ($member && Permission::checkMember($member, 'CMS_ACCESS')) { + return $member; + } + + throw new ValidationException('Restricted resource', 401); + } + + return Member::get()->first(); + } + + public function isApplicable(HTTPRequest $request) + { + if ($request->param('Controller') === '%$SilverStripe\GraphQL\Controller.admin') { + return false; + } + + /*if($request->getHeader('apikey')){ + return true; + }*/ + return true; + return false; + } +} diff --git a/src/GraphQL/APIKeyMiddleware.php b/src/GraphQL/APIKeyMiddleware.php new file mode 100644 index 0000000..da543b5 --- /dev/null +++ b/src/GraphQL/APIKeyMiddleware.php @@ -0,0 +1,23 @@ +getHeader('apikey') === WebpackTemplateProvider::config()['GRAPHQL_API_KEY']) { + return $next($schema, $query, $context, $params); + } + + throw new \Exception('Invalid API key token'); + } +} diff --git a/src/GraphQL/ElementTypeCreator.php b/src/GraphQL/ElementTypeCreator.php new file mode 100644 index 0000000..1559408 --- /dev/null +++ b/src/GraphQL/ElementTypeCreator.php @@ -0,0 +1,37 @@ + 'element' + ]; + } + + public function fields() + { + return [ + '_id' => ['type' => Type::nonNull(Type::id()),'resolve' => static function($object) { + return $object->ID; + }], + 'ID' => ['type' => Type::nonNull(Type::id())], + 'Title' => ['type' => Type::string()], + 'ParentID' => ['type' => Type::id()], + 'Render' => [ + 'type' => Type::string(), + 'resolve' => static function($object, array $args, $context, ResolveInfo $info) { + return $object->getController()->forTemplate()->HTML(); + } + ], + ]; + } +} diff --git a/src/GraphQL/MemberTypeCreator.php b/src/GraphQL/MemberTypeCreator.php new file mode 100644 index 0000000..ed25ab4 --- /dev/null +++ b/src/GraphQL/MemberTypeCreator.php @@ -0,0 +1,29 @@ + 'member' + ]; + } + + public function fields() + { + return [ + 'ID' => ['type' => Type::nonNull(Type::id())], + 'Email' => ['type' => Type::string()], + 'FirstName' => ['type' => Type::string()], + 'Surname' => ['type' => Type::string()], + ]; + } +} diff --git a/src/GraphQL/PageTypeCreator.php b/src/GraphQL/PageTypeCreator.php new file mode 100644 index 0000000..ad069ec --- /dev/null +++ b/src/GraphQL/PageTypeCreator.php @@ -0,0 +1,122 @@ + 'page' + ]; + } + + public function fields() + { + $elementsConnection = Connection::create('Elements') + ->setConnectionType($this->manager->getType('element')) + ->setDescription('A list of the page elements') + ->setSortableFields(['ID', 'Title']); + + return [ + '_id' => ['type' => Type::nonNull(Type::id()),'resolve' => static function($object) { + return $object->ID; + }], + 'ID' => ['type' => Type::nonNull(Type::id())], + 'Title' => ['type' => Type::string()], + 'Content' => ['type' => Type::string()], + 'Link' => ['type' => Type::string(), 'resolve' => static function($object) { + return $object->Link(); + }], + 'URLSegment' => ['type' => Type::string()], + 'ParentID' => ['type' => Type::id()], + 'ClassName' => ['type' => Type::string()], + 'CSSClass' => ['type' => Type::string(), 'resolve' => static function($object) { + return $object->CSSClass(); + }], + 'Summary' => ['type' => Type::string(), 'resolve' => static function($object) { + return $object->Summary(); + }], + 'HTML' => ['type' => Type::string(), 'resolve' => static function($object) { + // get action from request + $action = null; + + /** @var \Page $object */ + Director::set_current_page($object); + /** @var \PageController $controller */ + $controller = ModelAsController::controller_for($object); + + // find templates + $tpl = 'Page'; + $tpls = SSViewer::get_templates_by_class( + $object->ClassName, + ($action ? '_'.$action : ''), + \Page::class + ); + + foreach ($tpls as $tpl){ + if(is_array($tpl)){ + continue; + } + + $a_tpl = explode('\\',$tpl); + $last_name = array_pop($a_tpl); + $a_tpl[] = 'Layout'; + $a_tpl[] = $last_name; + $a_tpl = implode('\\', $a_tpl); + + if(SSViewer::hasTemplate($a_tpl)){ + break; + } + } + // + + $tpl = ($tpl !== 'Page') ? $tpl : 'Layout/Page'; + + $action = $action ? $action : 'index'; + /** @var HTTPRequest $request */ + $request = new HTTPRequest('GET', $object->AbsoluteLink()); + $request->setSession(new Session([])); + + // a little dirty way to make forms working + Controller::curr()->config()->set('url_segment', $object->AbsoluteLink()); + /*$controller->setRequest($request);*/ + //$request->getSession()->init($request); + + $controller->setRequest($request); + $controller->setAction($action); + //$controller->pushCurrent(); + $controller->doInit(); + + $layout = $controller->renderWith($tpl); + return $controller + ->customise(['Layout' => $layout]) + ->renderWith('GraphQLPage')->HTML(); + }], + 'Elements' => [ + 'type' => $elementsConnection->toType(), + 'args' => $elementsConnection->args(), + 'resolve' => static function($object, array $args, $context, ResolveInfo $info) use ($elementsConnection) { + return $elementsConnection->resolveList( + $object->ElementalArea()->Elements(), + $args, + $context + ); + } + ] + ]; + } +} diff --git a/src/GraphQL/PaginatedReadMembersQueryCreator.php b/src/GraphQL/PaginatedReadMembersQueryCreator.php new file mode 100644 index 0000000..a46200d --- /dev/null +++ b/src/GraphQL/PaginatedReadMembersQueryCreator.php @@ -0,0 +1,44 @@ +setConnectionType($this->manager->getType('member')) + ->setArgs([ + 'Email' => [ + 'type' => Type::string() + ] + ]) + ->setSortableFields(['ID', 'FirstName', 'Email']) + ->setConnectionResolver(static function ($object, array $args, $context, ResolveInfo $info) { + $member = Member::singleton(); + if (!$member->canView($context['currentUser'])) { + throw new \InvalidArgumentException(sprintf( + '%s view access not permitted', + Member::class + )); + } + $list = Member::get(); + + // Optional filtering by properties + if (isset($args['Email'])) { + $list = $list->filter('Email', $args['Email']); + } + + return $list; + }); + } +} diff --git a/src/GraphQL/PaginatedReadPagesQueryCreator.php b/src/GraphQL/PaginatedReadPagesQueryCreator.php new file mode 100644 index 0000000..2b6e09d --- /dev/null +++ b/src/GraphQL/PaginatedReadPagesQueryCreator.php @@ -0,0 +1,59 @@ +setConnectionType($this->manager->getType('page')) + ->setArgs([ + 'Link' => [ + 'type' => Type::string() + ] + ]) + ->setSortableFields(['Sort']) + ->setConnectionResolver(static function ($object, array $args, $context, ResolveInfo $info) { + + if (isset($args['Link'])) { + $link = $args['Link']; + + if(SiteTree::has_extension('\TractorCow\Fluent\Extension\FluentSiteTreeExtension')) { + $arr = array_filter(explode('/', $args['Link'])); + + $locale = \TractorCow\Fluent\Model\Locale::get()->filter('URLSegment', array_shift($arr))->first(); + \TractorCow\Fluent\State\FluentState::singleton()->setLocale($locale->Locale); + + $link = implode('/', $arr); + } + + + $list = ArrayList::create(); + $page = SiteTree::get_by_link($link); + $list->add($page); + } + + /*$list = \Page::get(); + + // Optional filtering by properties + if (isset($args['ID'])) { + $list = $list->filter('ID', $args['ID']); + }*/ + + return $list; + }); + } +} diff --git a/src/GraphQL/ReadMembersQueryCreator.php b/src/GraphQL/ReadMembersQueryCreator.php new file mode 100644 index 0000000..3f4683f --- /dev/null +++ b/src/GraphQL/ReadMembersQueryCreator.php @@ -0,0 +1,53 @@ + 'readMembers' + ]; + } + + public function args() + { + return [ + 'Email' => ['type' => Type::string()] + ]; + } + + public function type() + { + return Type::listOf($this->manager->getType('member')); + } + + public function resolve($object, array $args, $context, ResolveInfo $info) + { + $member = Member::singleton(); + + if (!$member->canView($context['currentUser'])) { + throw new \InvalidArgumentException(sprintf( + '%s view access not permitted', + Member::class + )); + } + $list = Member::get(); + + // Optional filtering by properties + if (isset($args['Email'])) { + $list = $list->filter('Email', $args['Email']); + } + + return $list; + } +} diff --git a/src/Models/Holiday.php b/src/Models/Holiday.php new file mode 100755 index 0000000..aad9163 --- /dev/null +++ b/src/Models/Holiday.php @@ -0,0 +1,57 @@ + 'Varchar(255)', + 'Date' => 'Date', + ]; + + private static $has_one = [ + 'Parent' => SiteConfig::class, + ]; + + + private static $summary_fields = [ + 'Title' => 'Title', + 'Date' => 'Date', + ]; + + private static $default_sort = 'Date ASC, Title ASC'; + + public function validate() + { + $result = parent::validate(); + + $exists = self::get()->filter([ + 'ID:not' => $this->ID, + 'Date' => $this->getField('Date'), + ])->exists(); + + if($exists) { + return $result->addError( + 'Holiday was defined already.', + ValidationResult::TYPE_ERROR + ); + } + + return $result; + } +} diff --git a/src/Models/Notification.php b/src/Models/Notification.php new file mode 100755 index 0000000..1b02bb8 --- /dev/null +++ b/src/Models/Notification.php @@ -0,0 +1,80 @@ + 'Varchar(255)', + 'Content' => 'Text', + 'DateOn' => 'Date', + 'DateOff' => 'Date', + 'Area' => 'Enum("Site","Site")', + ]; + + private static $has_one = [ + 'Parent' => SiteConfig::class, + 'TargetLink' => Link::class, + ]; + + private static $defaults = [ + 'Area' => 'Site', + ]; + + + private static $summary_fields = [ + 'Title' => 'Title', + 'Content' => 'Text', + 'DateOn' => 'Turn on date', + 'DateOff' => 'Turn off date', + ]; + + private static $default_sort = 'DateOn DESC, DateOff DESC, Title ASC'; + + public function getCMSFields() + { + $fields = parent::getCMSFields(); + + $fields->addFieldsToTab('Root.Main', [ + LinkField::create('TargetLinkID', 'Link'), + ]); + + return $fields; + } + + public function validate() + { + $result = parent::validate(); + + if (!$this->getField('DateOn') || !$this->getField('DateOff')) { + return $result->addError( + 'Turn on and turn off dates are required.', + ValidationResult::TYPE_ERROR + ); + } + + if (!$this->getField('Content')) { + return $result->addError( + 'Text field required.', + ValidationResult::TYPE_ERROR + ); + } + + return $result; + } +} diff --git a/src/Models/OpeningHour.php b/src/Models/OpeningHour.php new file mode 100755 index 0000000..a1fa2af --- /dev/null +++ b/src/Models/OpeningHour.php @@ -0,0 +1,90 @@ + 'Enum("Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday", "Monday")', + 'From' => 'Time', + 'Till' => 'Time', + 'Comment' => 'Varchar(255)', + 'DisplayStart' => 'Date', + 'DisplayEnd' => 'Date', + ]; + + private static $has_one = [ + 'Parent' => SiteConfig::class, + ]; + + private static $defaults = [ + 'Day' => 'Monday', + 'From' => '09:00:00', + 'Till' => '22:00:00', + ]; + + private static $summary_fields = [ + 'Day' => 'Day *', + 'From' => 'From *', + 'Till' => 'Till *', + //'Comment' => 'Comment', + 'DisplayStart' => 'Display Start', + 'DisplayEnd' => 'Display End', + ]; + + private static $default_sort = 'Day ASC, From ASC'; + + public function validate() + { + $result = parent::validate(); + + + if (!$this->getField('Day') + || !$this->getField('From') + || !$this->getField('Till') + || $this->getField('From') > $this->getField('Till') + ) { + return $result->addError( + 'Day, From and Till fields are required or wrong time range was specified.', + ValidationResult::TYPE_ERROR + ); + } + + $exists = self::get()->filter([ + 'ID:not' => $this->ID, + 'ParentID' => $this->getField('ParentID'), + 'Day' => $this->getField('Day'), + 'From:GreaterThanOrEqual' => $this->getField('From'), + 'Till:LessThanOrEqual' => $this->getField('Till'), + ])->exists(); + + if ($exists) { + return $result->addError( + 'Hours were defined already for specified range.', + ValidationResult::TYPE_ERROR + ); + } + + return $result; + } + + public function forTemplate() + { + return $this->renderWith(__CLASS__); + } +} diff --git a/src/Shop/CheckoutMapComponent.php b/src/Shop/CheckoutMapComponent.php new file mode 100755 index 0000000..fb3557e --- /dev/null +++ b/src/Shop/CheckoutMapComponent.php @@ -0,0 +1,47 @@ +process($config) + ) + ); + } + + public function validateData(Order $order, array $data) + { + } + + public function setData(Order $order, array $data) + { + } + + public function getData(Order $order) + { + return []; + } +} diff --git a/src/Shop/CheckoutNoDeliveryConfig.php b/src/Shop/CheckoutNoDeliveryConfig.php new file mode 100755 index 0000000..7c1b0a7 --- /dev/null +++ b/src/Shop/CheckoutNoDeliveryConfig.php @@ -0,0 +1,43 @@ +addComponent(CustomerDetails::create()); + + if (Checkout::member_creation_enabled() && !Security::getCurrentUser()) { + $this->addComponent(Membership::create()); + } + + if (count(GatewayInfo::getSupportedGateways()) > 1) { + $this->addComponent(Payment::create()); + } + + $this->addComponent(Notes::create()); + + $this->addComponent(CheckoutMapComponent::create()); + $this->addComponent(Terms::create()); + } +} diff --git a/src/Shop/_manifest_exclude b/src/Shop/_manifest_exclude new file mode 100755 index 0000000..c309c9d --- /dev/null +++ b/src/Shop/_manifest_exclude @@ -0,0 +1 @@ +Remove it in case u need silvershop module diff --git a/src/Tasks/BrokenFilesTask.php b/src/Tasks/BrokenFilesTask.php new file mode 100755 index 0000000..fd37367 --- /dev/null +++ b/src/Tasks/BrokenFilesTask.php @@ -0,0 +1,39 @@ +exists()) { + echo 'File name was not found at SS DB: ' + .$file->getField('Name').'
' + .PHP_EOL; + + $i++; + + continue; + } + } + + echo ($i > 0) ? + '

Missing '.$i.' files

' + : '

All files are ok!

'; + + die('Done!'); + } +} diff --git a/src/Tasks/BuildTask.php b/src/Tasks/BuildTask.php new file mode 100755 index 0000000..cfe8924 --- /dev/null +++ b/src/Tasks/BuildTask.php @@ -0,0 +1,78 @@ +render(); + } + + public function Title() + { + return $this->title; + } + + protected function setMessage($msg, $type = 'msg') + { + if(is_array($msg)) { + $type = 'list'; + } + + $this->messages[] = [$type, $msg]; + } + + public function render() + { + echo ''; + + foreach ($this->messages as $item) { + $type = $item[0]; + $msg = $item[1]; + + switch ($type) { + case 'h2': + echo '

'.$msg.'

'.PHP_EOL; + break; + case 'h3': + echo '

'.$msg.'

'.PHP_EOL; + break; + case 'list': + echo ''; + break; + default: + echo $msg.'
'.PHP_EOL; + break; + } + } + + echo '

Success!

'; + } +} diff --git a/src/Tasks/CleanContentTask.php b/src/Tasks/CleanContentTask.php new file mode 100755 index 0000000..36d09f0 --- /dev/null +++ b/src/Tasks/CleanContentTask.php @@ -0,0 +1,29 @@ +setField('Content', ''); + $p->write(); + echo '#'.$p->ID.' '.$p->getField('Title').'
'; + } + + die('Done!'); + } +} diff --git a/src/Tasks/QRCodeTask.php b/src/Tasks/QRCodeTask.php new file mode 100755 index 0000000..69616ab --- /dev/null +++ b/src/Tasks/QRCodeTask.php @@ -0,0 +1,105 @@ +'.$this->Title().''; + + echo self::generateQR(); + + die('Done!'); + } + + public static function generateQR() + { + $qrCode = new QrCode(Director::absoluteBaseURL()); + $qrCode->setSize(600); + $qrCode->setMargin(10); + + $qrCode->setWriterByName('png'); + $qrCode->setEncoding('UTF-8'); + $qrCode->setErrorCorrectionLevel(ErrorCorrectionLevel::HIGH()); + $qrCode->setForegroundColor(['r' => 0, 'g' => 0, 'b' => 0, 'a' => 0]); + $qrCode->setBackgroundColor(['r' => 255, 'g' => 255, 'b' => 255, 'a' => 0]); + $qrCode->setLabel(Director::absoluteBaseURL(), 16, null, LabelAlignment::CENTER()); + /*$qrCode->setLogoPath('/'.File::join_paths( + PUBLIC_PATH, + RESOURCES_DIR, + project(), + 'client', 'dist', 'icons', + 'apple-touch-icon-152x152.png' + )); + $qrCode->setLogoSize(152, 152);*/ + $qrCode->setValidateResult(true); + + + // Round block sizes to improve readability and make the blocks sharper in pixel based outputs (like png). + // There are three approaches: + $qrCode->setRoundBlockSize(true, QrCode::ROUND_BLOCK_SIZE_MODE_MARGIN); // The size of the qr code is shrinked, if necessary, but the size of the final image remains unchanged due to additional margin being added (default) + $qrCode->setRoundBlockSize(true, QrCode::ROUND_BLOCK_SIZE_MODE_ENLARGE); // The size of the qr code and the final image is enlarged, if necessary + $qrCode->setRoundBlockSize(true, QrCode::ROUND_BLOCK_SIZE_MODE_SHRINK); // The size of the qr code and the final image is shrinked, if necessary + + // Set additional writer options (SvgWriter example) + $qrCode->setWriterOptions(['exclude_xml_declaration' => true]); + + // Directly output the QR code + /*header('Content-Type: '.$qrCode->getContentType()); + echo $qrCode->writeString(); + die();*/ + + // Save it to a file + $qrCode->writeFile(TEMP_PATH.'/qrcode.png'); + $res = self::getAssetStore()->setFromLocalFile( + TEMP_PATH.'/qrcode.png', + 'qrcode.png', null, null, + [ + 'conflict' => AssetStore::CONFLICT_OVERWRITE, + 'visibility' => AssetStore::VISIBILITY_PUBLIC, + ] + ); + + $img = Image::get()->filter([ + 'ParentID' => 0, + 'FileFilename' => $res['Filename'], + ])->first(); + if(!$img) { + $img = Image::create(); + } + + $res['FileHash'] = $res['Hash']; + $res['FileFilename'] = $res['Filename']; + $res['ParentID'] = 0; + + $img = $img->update($res); + $img->write(); + $img->publishFile(); + + return 'QR-code
'; + } + + protected static function getAssetStore() + { + return Injector::inst()->get(AssetStore::class); + } +} diff --git a/src/Tasks/RestoreFilesTask.php b/src/Tasks/RestoreFilesTask.php new file mode 100755 index 0000000..32c0448 --- /dev/null +++ b/src/Tasks/RestoreFilesTask.php @@ -0,0 +1,47 @@ +*'; + + $files = array_diff(scandir($path), ['.','..']); + foreach ($files as $fileName) { + $file = File::get()->filter('Name', $fileName); + if (!$file->count()) { + echo 'File name was not found at SS DB: '.$fileName.'
'.PHP_EOL; + continue; + } + + foreach ($file as $f) { + if ($f->exists()) { + echo 'File #'.$f->ID.' already exists at SS file structure. '.$fileName.'
' . PHP_EOL; + continue; + } + + echo 'Found non existing at SS file system file and found it at SS DB.' + .' Creating the file #'.$f->ID.' at SS file system. "' . $fileName . '"
' . PHP_EOL; + + $f->setFromLocalFile($path.'/'.$fileName); + $f->write(); + $f->publishFile(); + } + } + + die('Success!'); + } +} diff --git a/src/Templates/DeferredRequirements.php b/src/Templates/DeferredRequirements.php new file mode 100755 index 0000000..a9afb88 --- /dev/null +++ b/src/Templates/DeferredRequirements.php @@ -0,0 +1,229 @@ + 'Auto', + 'DeferedCSS' => 'loadCSS', + 'DeferedJS' => 'loadJS', + 'WebpackActive' => 'webpackActive', + 'EmptyImgSrc' => 'emptyImageSrc', + ]; + } + + public static function Auto($class = false): string + { + $config = Config::inst()->get(self::class); + $projectName = WebpackTemplateProvider::projectName(); + $mainTheme = WebpackTemplateProvider::mainTheme(); + $mainTheme = $mainTheme ?: $projectName; + + $dir = Path::join( + Director::publicFolder(), + RESOURCES_DIR, + $projectName, + 'client', + 'dist' + ); + $cssPath = Path::join($dir, 'css'); + $jsPath = Path::join($dir, 'js'); + + // Initialization + Requirements::block(THIRDPARTY_DIR.'/jquery/jquery.js'); + /*if (defined('FONT_AWESOME_DIR')) { + Requirements::block(FONT_AWESOME_DIR.'/css/lib/font-awesome.min.css'); + }*/ + Requirements::set_force_js_to_bottom(true); + + // Main libs + if (!$config['nojquery']) { + self::loadJS( + '//ajax.googleapis.com/ajax/libs/jquery/' + .$config['jquery_version'].'/jquery.min.js' + ); + } + + if (!$config['noreact']) { + if (!Director::isDev()) { + self::loadJS('https://unpkg.com/react@17/umd/react.production.min.js'); + self::loadJS('https://unpkg.com/react-dom@17/umd/react-dom.production.min.js'); + } else { + self::loadJS('https://unpkg.com/react@17/umd/react.development.js'); + self::loadJS('https://unpkg.com/react-dom@17/umd/react-dom.development.js'); + } + } + + // App libs + if (!$config['nofontawesome']) { + $v = !isset($config['fontawesome_version']) || !$config['fontawesome_version'] + ? Config::inst()->get(FontAwesomeField::class, 'version') + : $config['fontawesome_version']; + + self::loadCSS('//use.fontawesome.com/releases/v'.$v.'/css/all.css'); + } + + self::loadCSS($mainTheme.'.css'); + + // hot reloading + /*if (self::webpackActive()) { + self::loadJS('hot.js'); + }*/ + + self::loadJS($mainTheme.'.js'); + + // Custom controller requirements + $curr_class = $class ?: get_class(Controller::curr()); + if (isset($config['custom_requirements'][$curr_class])) { + foreach ($config['custom_requirements'][$curr_class] as $file) { + if (strpos($file, '.css')) { + self::loadCSS($file); + } + if (strpos($file, '.js')) { + self::loadJS($file); + } + } + } + + $curr_class = str_replace('\\', '.', $curr_class); + + // Controller requirements + $themePath = Path::join($cssPath, $mainTheme.'_'.$curr_class . '.css'); + $projectPath = Path::join($cssPath, $projectName.'_'.$curr_class . '.css'); + if ($mainTheme && file_exists($themePath)) { + self::loadCSS($mainTheme.'_'.$curr_class . '.css'); + } elseif (file_exists($projectPath)) { + self::loadCSS($projectName.'_'.$curr_class . '.css'); + } + + $themePath = Path::join($jsPath, $mainTheme.'_'.$curr_class . '.js'); + $projectPath = Path::join($jsPath, $projectName.'_'.$curr_class . '.js'); + if ($mainTheme && file_exists($themePath)) { + self::loadJS($mainTheme.'_'.$curr_class . '.js'); + } elseif (file_exists($projectPath)) { + self::loadJS($projectName.'_'.$curr_class . '.js'); + } + + return self::forTemplate(); + } + + public static function loadCSS($css): void + { + $external = (mb_strpos($css, '//') === 0 || mb_strpos($css, 'http') === 0); + if ($external) { + self::$css[] = $css; + } else { + WebpackTemplateProvider::loadCSS($css); + } + } + + public static function loadJS($js): void + { + /*$external = (mb_substr($js, 0, 2) === '//' || mb_substr($js, 0, 4) === 'http'); + if ($external || (self::getDeferred() && !self::_webpackActive())) {*/ + // webpack supposed to load external JS + if (self::getDeferred() && !self::webpackActive()) { + self::$js[] = $js; + } else { + WebpackTemplateProvider::loadJS($js); + } + } + + public static function webpackActive(): bool + { + return WebpackTemplateProvider::isActive(); + } + + public static function setDeferred($bool): void + { + Config::inst()->set(__CLASS__, 'deferred', $bool); + } + + public static function getDeferred(): bool + { + return self::config()['deferred']; + } + + public static function forTemplate(): string + { + $result = ''; + self::$css = array_unique(self::$css); + foreach (self::$css as $css) { + $result .= ''; + } + + self::$js = array_unique(self::$js); + foreach (self::$js as $js) { + $result .= ''; + } + + $result .= + ''; + + return $result; + } + + private static function get_url($url): string + { + $config = self::config(); + + // external URL + if (strpos($url, '//') !== false) { + return $url; + } + + $path = WebpackTemplateProvider::toPublicPath($url); + + $absolutePath = Director::getAbsFile($path); + $hash = sha1_file($absolutePath); + + $version = $config['version'] ? '&v='.$config['version'] : ''; + //$static_domain = $config['static_domain']; + //$static_domain = $static_domain ?: ''; + + return Controller::join_links(WebpackTemplateProvider::toPublicPath($url), '?m='.$hash.$version); + } + + public static function emptyImageSrc(): string + { + return 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; + } + + public static function config(): array + { + return Config::inst()->get(__CLASS__); + } +} diff --git a/src/Templates/WebpackTemplateProvider.php b/src/Templates/WebpackTemplateProvider.php new file mode 100755 index 0000000..0a77e06 --- /dev/null +++ b/src/Templates/WebpackTemplateProvider.php @@ -0,0 +1,153 @@ + 'isActive', + 'WebpackCSS' => 'loadCSS', + 'WebpackJS' => 'loadJS', + 'ResourcesURL' => 'resourcesURL', + 'ProjectName' => 'themeName', + ]; + } + + /** + * Load CSS file + * @param $path + */ + public static function loadCSS($path): void + { + /*if (self::isActive()) { + return; + }*/ + + Requirements::css(self::_getPath($path)); + } + + /** + * Load JS file + * @param $path + */ + public static function loadJS($path): void + { + Requirements::javascript(self::_getPath($path)); + } + + public static function projectName(): string + { + return Config::inst()->get(ModuleManifest::class, 'project'); + } + + public static function mainTheme() + { + $themes = Config::inst()->get(SSViewer::class, 'themes'); + return is_array($themes) && $themes[0] !== '$public' && $themes[0] !== '$default' ? $themes[0] : false; + } + + public static function resourcesURL($link = null): string + { + $cfg = self::config(); + + if ($cfg['webp'] && !self::isActive()) { + $link = str_replace(['.png','.jpg','.jpeg'], '.webp', $link); + } + + return Controller::join_links( + Director::baseURL(), + '/resources/', + self::projectName(), + $cfg['dist'], + 'img', + $link + ); + } + + + /** + * Checks if dev mode is enabled and if webpack server is online + * @return bool + */ + public static function isActive(): bool + { + $cfg = self::config(); + return Director::isDev() && @fsockopen( + $cfg['HOSTNAME'], + $cfg['PORT'] + ); + } + + protected static function _getPath($path): string + { + return self::isActive() && strpos($path, '//') === false ? + self::_toDevServerPath($path) : + self::toPublicPath($path); + } + + protected static function _toDevServerPath($path): string + { + $cfg = self::config(); + return sprintf( + '%s%s:%s/%s', + ($cfg['HTTPS'] ? 'https://' : 'http://'), + $cfg['HOSTNAME'], + $cfg['PORT'], + basename($path) + //Controller::join_links($cfg['APPDIR'], $cfg['SRC'], basename($path)) + ); + } + + public static function toPublicPath($path): string + { + $cfg = self::config(); + return strpos($path, '//') === false ? + Controller::join_links( + RESOURCES_DIR, + self::projectName(), + $cfg['dist'], + (strpos($path, '.css') ? 'css' : 'js'), + $path + ) + : $path; + } + + public static function config(): array + { + return Config::inst()->get(__CLASS__); + } +} diff --git a/src/Tests/TestServer.php b/src/Tests/TestServer.php new file mode 100755 index 0000000..5167417 --- /dev/null +++ b/src/Tests/TestServer.php @@ -0,0 +1,142 @@ +table{width:100%}table td,table th{border:1px solid #dedede}'; + + echo '

Testing Server

'; + echo self::success('BASE_PATH: '.BASE_PATH.''); + echo self::success('PHP: '.phpversion().''); + + $v = Deprecation::dump_settings()['version']; + if ($v) { + echo self::success('SilverStipe version: '.$v.''); + } else { + echo self::success('SilverStipe version unknown: ' + .(file_exists(BASE_PATH.'/framework') ? '3.x.x' : '4.x.x') + .''); + } + + echo self::success('Memory limit: '.ini_get('memory_limit').''); + + + if (is_writable(TEMP_FOLDER)) { + echo self::success('TMP (cache) dir '.TEMP_FOLDER.' dir is writable.'); + } else { + echo self::error('TMP (cache) dir '.TEMP_FOLDER.' dir is no writable!'); + } + + echo '

Testing Uploads

'; + $maxUpload = ini_get('upload_max_filesize'); + $maxPost = ini_get('post_max_size'); + + echo self::success('PHP max upload size: '.$maxUpload); + echo self::success('PHP max post size: '.$maxPost); + echo self::success('So calculated max upload size: '.self::formatBytes(min( + self::memstring2bytes($maxUpload), + self::memstring2bytes($maxPost) + ))); + + $defaultSizes = Config::inst()->get(Upload_Validator::class, 'default_max_file_size'); + if ($defaultSizes) { + if (!is_array($defaultSizes)) { + echo self::error('default_max_file_size miss-configuration, plz fix'); + var_dump($defaultSizes); + die(); + } + + echo '

Configured limits:

' + .''; + foreach ($defaultSizes as $k => $size) { + echo ''; + } + echo '
FileSize limit
'.$k.''.$size.'
'; + } + + if (is_writable(ASSETS_DIR)) { + echo self::success('Assets dir '.ASSETS_DIR.' dir is writable.'); + } else { + echo self::error('Assets dir '.ASSETS_DIR.' dir is no writable!'); + } + + if (function_exists('imagewebp')) { + echo self::success('WebP is available'); + } else { + echo self::error('WebP is not available'); + } + + + die(); + } + + + public static function formatBytes($size, $precision = 2) + { + $base = log($size, 1024); + $suffixes = array('', 'K', 'M', 'G', 'T'); + + return round(pow(1024, $base - floor($base)), $precision) . $suffixes[(string)floor($base)]; + } + + public static function error($text) + { + return 'ERROR: '.$text.'
'; + } + + public static function success($text) + { + return 'SUCCESS: '.$text.'
'; + } + + public static function warning($text) + { + return 'WARNING: '.$text.'
'; + } + + public static function renderValidation($result) + { + echo '

'; + $msgs = $result->getMessages(); + foreach ($msgs as $msg) { + echo self::error($msg['fieldName'].': '.$msg['message']); + } + echo '

'; + } + + public static function memstring2bytes($memString) + { + // Remove non-unit characters from the size + $unit = preg_replace('/[^bkmgtpezy]/i', '', $memString); + // Remove non-numeric characters from the size + $size = preg_replace('/[^0-9\.]/', '', $memString); + + if ($unit) { + // Find the position of the unit in the ordered string which is the power + // of magnitude to multiply a kilobyte by + return round($size * pow(1024, stripos('bkmgtpezy', $unit[0]))); + } + + return round($size); + } +} diff --git a/src/Traits/PaginatedListing.php b/src/Traits/PaginatedListing.php new file mode 100755 index 0000000..3242b45 --- /dev/null +++ b/src/Traits/PaginatedListing.php @@ -0,0 +1,18 @@ +getRequest()->requestVars(); + $vars = array_filter($vars); + $vars['page'] = $pageID ? $pageID : '2'; + + return $this->Link('?'.http_build_query($vars)); + } +} diff --git a/src/Widgets/BannerWidget.php b/src/Widgets/BannerWidget.php new file mode 100755 index 0000000..af46ce1 --- /dev/null +++ b/src/Widgets/BannerWidget.php @@ -0,0 +1,66 @@ +'; + private static $table_name = 'BannerWidget'; + + private static $has_one = [ + 'Image' => Image::class, + 'Link' => Link::class, + ]; + + private static $owns = [ + 'Image', + 'Link', + ]; + + public function getCMSFields() + { + $fields = parent::getCMSFields(); + + $fields->push(UploadField::create('Image', 'Image (minimal width 301px)') + ->setAllowedFileCategories(['image/supported'])); + + $fields->push(LinkField::create('LinkID', 'Link')); + + return $fields; + } + + private $_random; + public function Random() + { + if (!$this->_random) { + $this->_random = self::get()->filter('Enabled', true)->sort('RAND()')->first(); + } + + return $this->_random; + } + + public function onBeforeWrite() + { + $title = $this->getField('Title'); + $img = $this->Image(); + if(!$title && $img) { + $this->setField('Title', $img->getTitle()); + } + + parent::onBeforeWrite(); + } +} diff --git a/src/Widgets/ContentWidget.php b/src/Widgets/ContentWidget.php new file mode 100755 index 0000000..04e7d43 --- /dev/null +++ b/src/Widgets/ContentWidget.php @@ -0,0 +1,37 @@ +'; + private static $table_name = 'ContentWidget'; + + private static $db = [ + 'Text' => 'HTMLText', + ]; + + public function getCMSFields() + { + $fields = parent::getCMSFields(); + + $fields->push(HTMLEditorField::create('Text')); + + return $fields; + } +} diff --git a/src/Widgets/ElementWidget.php b/src/Widgets/ElementWidget.php new file mode 100755 index 0000000..d03fe7e --- /dev/null +++ b/src/Widgets/ElementWidget.php @@ -0,0 +1,60 @@ +'; + private static $table_name = 'ElementWidget'; + + private static $has_one = [ + 'Element' => BaseElement::class, + ]; + + public function getCMSFields() + { + $fields = parent::getCMSFields(); + + $fields->push( + DropdownField::create( + 'ElementID', + 'Displayed Element', + BaseElement::get() + ->filter(['AvailableGlobally' => true]) + ->exclude(['ClassName' => ElementList::class]) + ) + /*TreeDropdownField::create( + 'ElementID', + 'Displayed Element', + SiteTree::class + )->setFilterFunction(static function($el){ + return (bool) $el->getField('ElementalArea')->Elements()->count(); + })*/ + ); + + return $fields; + } + + public function SimpleClassName() + { + $el = $this->getField('Element'); + var_dump($el); + die(); + return $el->getSimpleClassName(); + } +} diff --git a/src/Widgets/LinksWidget.php b/src/Widgets/LinksWidget.php new file mode 100755 index 0000000..522976b --- /dev/null +++ b/src/Widgets/LinksWidget.php @@ -0,0 +1,56 @@ +'; + private static $table_name = 'LinksWidget'; + + private static $many_many = [ + 'Links' => Link::class, + ]; + + private static $owns = [ + 'Links', + ]; + + public function getCMSFields() + { + $fields = parent::getCMSFields(); + + if($this->ID) { + $fields->push(GridField::create( + 'Links', + '', + $this->Links(), + GridFieldConfig_RecordEditor::create() + )); + }else{ + $fields->push(LiteralField::create( + 'Note', + '

Note: The widget needs to be saved before adding a link.' + .' Enter the Title and click "+ Create" button at the bottom left corner of the screen

') + ); + } + + return $fields; + } +} diff --git a/src/Widgets/SubmenuWidget.php b/src/Widgets/SubmenuWidget.php new file mode 100755 index 0000000..eff3a4d --- /dev/null +++ b/src/Widgets/SubmenuWidget.php @@ -0,0 +1,53 @@ +'; + private static $table_name = 'SubmenuWidget'; + + private static $db = [ + 'TopLevelSubmenu' => 'Boolean(1)', + ]; + + public function getPage() + { + $area = $this->Parent(); + return \Page::get()->filter('SideBarID', $area->ID)->first(); + } + + public function getSubmenu() + { + $page = $this->getPage(); + + if(!$this->getField('TopLevelSubmenu')) { + return $page->Children(); + } + + return $page->Level(1)->Children(); + } + + public function getCMSFields() + { + $fields = parent::getCMSFields(); + + $fields->push(CheckboxField::create( + 'TopLevelSubmenu', + 'Display sub-menu starting from the top level (otherwise current page children will be displayed)' + )); + + return $fields; + } +} diff --git a/src/Widgets/WidgetAreaField.php b/src/Widgets/WidgetAreaField.php new file mode 100755 index 0000000..83af016 --- /dev/null +++ b/src/Widgets/WidgetAreaField.php @@ -0,0 +1,157 @@ +setTypes($blockTypes); + + $config = GridFieldConfig_Base::create(); + + $config->getComponentByType(GridFieldDataColumns::class)->setDisplayFields([ + 'Icon' => '', + 'Title' => 'Title', + 'Enabled' => 'Enabled', + ])->setFieldFormatting([ + 'Icon' => static function ($v, Widget $item) { + return ''.$item::config()->get('icon').''; + }, + 'Enabled' => static function ($v, Widget $item) { + return $item->getField('Enabled') ? 'Yes' : 'No'; + }, + ]); + + $config->addComponent(new GridFieldEditButton()); + $config->addComponent(new GridFieldDeleteAction(false)); + $config->addComponent(new GridFieldDetailForm(null, false, false)); + $config->addComponent(new GridFieldSortableRows('Sort')); + + if (!empty($blockTypes)) { + /** @var GridFieldAddNewMultiClass $adder */ + $adder = Injector::inst()->create(GridFieldAddNewMultiClass::class); + $adder->setClasses($blockTypes); + $config->addComponent($adder); + } + + // By default, no need for a title on the editor. If there is more than one area then use `setTitle` to describe + parent::__construct($name, '', $area->Widgets(), $config); + + $this->area = $area; + $this->addExtraClass('element-editor__container no-change-track'); + } + + /** + * @param array $types + * + * @return $this + */ + public function setTypes($types) + { + $this->types = $types; + + return $this; + } + + /** + * @return array + */ + public function getTypes() + { + $types = $this->types; + + $this->extend('updateGetTypes', $types); + + return $types; + } + + /** + * @return ElementalArea + */ + public function getArea() + { + return $this->area; + } + + public function saveInto(DataObjectInterface $dataObject) + { + parent::saveInto($dataObject); + + $elementData = $this->Value(); + $idPrefixLength = strlen(sprintf(ElementalAreaController::FORM_NAME_TEMPLATE, '')); + + if (!$elementData) { + return; + } + + foreach ($elementData as $form => $data) { + // Extract the ID + $elementId = (int) substr($form, $idPrefixLength); + + // @var BaseElement $element + $element = $this->getArea()->Widgets()->byID($elementId); + + if (!$element) { + // Ignore invalid elements + continue; + } + + $data = ElementalAreaController::removeNamespacesFromFields($data, $element->ID); + + $element->updateFromFormData($data); + $element->write(); + } + } +} diff --git a/src/Widgets/WidgetExtension.php b/src/Widgets/WidgetExtension.php new file mode 100755 index 0000000..0176722 --- /dev/null +++ b/src/Widgets/WidgetExtension.php @@ -0,0 +1,50 @@ + 'Boolean(1)', + ]; + + public function updateCMSFields(FieldList $fields) + { + parent::updateCMSFields($fields); + // Add a combined field for "Title" and "Displayed" checkbox in a Bootstrap input group + $fields->removeByName('ShowTitle'); + $fields->replaceField( + 'Title', + TextCheckboxGroupField::create() + ->setName('Title') + ); + $fields->push(TreeDropdownField::create( + 'MovePageID', 'Move widget to page', SiteTree::class + )); + } + + public function onBeforeWrite() + { + $obj = $this->owner; + $moveID = $obj->MovePageID; + if ($moveID) { + $page = \Page::get()->byID($moveID); + if($page) { + $sidebarID = $page->getField('SideBarID'); + if($sidebarID) { + $obj->setField('ParentID', $sidebarID); + } + } + } + + parent::onBeforeWrite(); + } +} diff --git a/src/Widgets/WidgetPageExtension.php b/src/Widgets/WidgetPageExtension.php new file mode 100755 index 0000000..9b5f3a4 --- /dev/null +++ b/src/Widgets/WidgetPageExtension.php @@ -0,0 +1,49 @@ +findOrMakeTab('Root.Widgets'); + + $tab->setTitle('Sidebar'); + + $tab->removeByName('SideBar'); + + $widgetTypes = WidgetAreaEditor::create('Sidebar')->AvailableWidgets(); + $available = []; + /** @var Widget $type */ + foreach ($widgetTypes as $type) { + $available[get_class($type)] = $type->getCMSTitle(); + } + + $tab->push(WidgetAreaField::create( + 'SideBar', + $this->owner->Sidebar(), + $available + )); + } + + public function onBeforeWrite() + { + parent::onBeforeWrite(); + + if (!$this->owner->getField('SideBarID')) { + $area = WidgetArea::create(); + $area->write(); + + $this->owner->setField('SideBarID', $area->ID); + } + } +} diff --git a/templates/A2nt/CMSNiceties/Widgets/BannerWidget.ss b/templates/A2nt/CMSNiceties/Widgets/BannerWidget.ss new file mode 100755 index 0000000..67a35c9 --- /dev/null +++ b/templates/A2nt/CMSNiceties/Widgets/BannerWidget.ss @@ -0,0 +1,8 @@ +$Title +<% if $Link %> + <% with $Link %> + target="_blank"<% end_if %> class="stretched-link"> + $Up.Title + + <% end_with %> +<% end_if %> diff --git a/templates/A2nt/CMSNiceties/Widgets/ContentWidget.ss b/templates/A2nt/CMSNiceties/Widgets/ContentWidget.ss new file mode 100755 index 0000000..9080bb9 --- /dev/null +++ b/templates/A2nt/CMSNiceties/Widgets/ContentWidget.ss @@ -0,0 +1,3 @@ +<% if $Text %> +
$Text
+<% end_if %> diff --git a/templates/A2nt/CMSNiceties/Widgets/ElementWidget.ss b/templates/A2nt/CMSNiceties/Widgets/ElementWidget.ss new file mode 100755 index 0000000..eb383fc --- /dev/null +++ b/templates/A2nt/CMSNiceties/Widgets/ElementWidget.ss @@ -0,0 +1,9 @@ +<% if $Element %> + <% with $Element %> +
+
+ $forTemplate +
+
+ <% end_with %> +<% end_if %> diff --git a/templates/A2nt/CMSNiceties/Widgets/SubmenuWidget.ss b/templates/A2nt/CMSNiceties/Widgets/SubmenuWidget.ss new file mode 100755 index 0000000..bfb4240 --- /dev/null +++ b/templates/A2nt/CMSNiceties/Widgets/SubmenuWidget.ss @@ -0,0 +1,18 @@ +<% if $Submenu %> + +<% end_if %> diff --git a/templates/BreadcrumbsTemplate.ss b/templates/BreadcrumbsTemplate.ss new file mode 100755 index 0000000..9663015 --- /dev/null +++ b/templates/BreadcrumbsTemplate.ss @@ -0,0 +1,21 @@ +<% if $Pages %> +
+ +
+<% end_if %> diff --git a/templates/GraphQLPage.ss b/templates/GraphQLPage.ss new file mode 100755 index 0000000..f563df4 --- /dev/null +++ b/templates/GraphQLPage.ss @@ -0,0 +1 @@ +<% include MainContent Layout=$Layout %> diff --git a/templates/Page.ss b/templates/Page.ss new file mode 100755 index 0000000..d18ce72 --- /dev/null +++ b/templates/Page.ss @@ -0,0 +1,29 @@ + + +<%-- manifest="/cache.appcache" --%> + + <% include MetaHead %> + + + data-default-lng="$Longitude" data-default-lat="$Latitude"<% end_with %>> +
+ <% include First %> + +
+ + + +
+ <% include MainContent Layout=$Layout %> +
+
+ + + + <% include Last %> + + diff --git a/templates/WidgetHolder.ss b/templates/WidgetHolder.ss new file mode 100755 index 0000000..328562b --- /dev/null +++ b/templates/WidgetHolder.ss @@ -0,0 +1,4 @@ +