Your IP : 216.73.216.190


Current Path : /proc/1908984/root/proc/2603263/cwd/
Upload File :
Current File : //proc/1908984/root/proc/2603263/cwd/com_gsd.tar

css/fastedit.css000064400000002120152344346010007646 0ustar00.gsdFastEdit .fast-edit-dropdown{display:inline-block;position:relative}.gsdFastEdit table{max-width:600px;margin-bottom:0}.gsdFastEdit table:hover tr,.gsdFastEdit table:hover td{background:transparent}.gsdFastEdit .items{margin-top:20px}.gsdFastEdit.working{pointer-events:none}.gsdFastEdit table td{vertical-align:middle}.gsdFastEdit .text-right{text-align:right}.gsdFastEdit .gsd-btn-toolbar [class^="icon-"]{margin:0}.gsdFastEdit .modal-body{overflow:hidden !important}.gsdFastEdit .icon-new{margin-right:5px}.gsdFastEdit .gsdID{opacity:.5}.gsdFastEdit .nowrap{white-space:nowrap}.gsdFastEdit .dropdown-menu{width:100%;padding:0;border-radius:5px}.gsdFastEdit .dropdown-menu li:first-child a{border-radius:5px 5px 0 0}.gsdFastEdit .dropdown-menu li:last-child a{border-radius:0 0 5px 5px}.gsdFastEdit .dropdown-menu a{display:block;padding:8px 10px;color:inherit !important;line-height:1;text-decoration:none;border-bottom:solid 1px #efefef}.gsdFastEdit .dropdown-menu a:hover{background-color:#efefef}fieldset#fieldset-gsd{border:none;padding:0;margin:0}fieldset#fieldset-gsd legend{display:none}


css/joomla3.css000064400000003650152344346010007420 0ustar00.subform{margin-left:-180px}.subform .subform-child{display:-webkit-box;display:-ms-flexbox;display:flex}.subform .subform-child .controls{margin:0}.subform .subform-child .controls>*{width:100% !important;-webkit-box-sizing:border-box;box-sizing:border-box}.subform .subform-child .controls>input{height:28px}.nr-main-container{background-color:#f0f0f0}.nr-main-container .tf-notices:not(:empty){padding:20px}.nr-main-container .tf-notices:not(:empty) .tf-notice:last-of-type{margin-bottom:0}.subhead-collapse{-webkit-box-shadow:0 2px 2px 2px rgba(0,0,0,0.06);box-shadow:0 2px 2px 2px rgba(0,0,0,0.06);position:relative;z-index:1}.nr-app-sidebar{background-color:#fff;width:220px !important;padding:15px 25px;-webkit-box-shadow:2px 0 2px 2px rgba(0,0,0,0.06);box-shadow:2px 0 2px 2px rgba(0,0,0,0.06);position:relative}.nr-app-sidebar .nav ul{list-style:none;margin:0;padding:0;width:100%}.nr-app-sidebar .nav li{border-bottom:solid 1px #eaeaea}.nr-app-sidebar .nav li.active>a{color:green;font-weight:bold;pointer-events:none}.nr-app-sidebar .nav a{display:block;color:#777;padding:13.88889px 0;text-decoration:none;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;line-height:1}.nr-app-sidebar .nav a:hover{opacity:.8}.nr-app-sidebar .nav a .icon{width:12px;height:auto;margin-right:12.5px;opacity:.8}#status{display:none}body #system-message-container{margin:0 -20px;padding-left:0}body #system-message-container,body #system-message-container>*{-webkit-box-sizing:border-box;box-sizing:border-box}body #system-message-container .alert{border-radius:0;padding:15px 25px;line-height:1}body #system-message-container>.alert:last-child{margin:0}.subhead-collapse{margin-bottom:0px !important}.nr-app-config{margin:0 !important}#attrib-gsd .controls .gsdFastEdit{margin-left:-180px}#k2TabPlugins .controls .gsdFastEdit{min-height:330px}.download_key_actions a{margin-bottom:0 !important}


css/joomla4 (copy).css000064400000000046152344346010010471 0ustar00.nr-main-container{background:none}


css/joomla4.css000064400000000046152344346010007415 0ustar00.nr-main-container{background:none}


css/openinghours.css000064400000003137152344346010010574 0ustar00.gsd-oh-container{margin-top:15px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.gsd-oh-container .clockpicker{margin-left:-5px;display:inline-block}.gsd-oh-container .clockpicker input{width:35px;padding:4px 8px}.gsd-oh-container>:not(:last-child){margin-bottom:12px}.gsd-oh-container .gsd-oh-row{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;min-height:28px}.gsd-oh-container .gsd-oh-row input[type="checkbox"]{margin:0 8px 0 0}.gsd-oh-container .gsd-oh-row label{margin:0 10px 0 0;width:90px}.gsd-oh-container .gsd-oh-row .oh-picker.first{display:none}.gsd-oh-container .gsd-oh-row .oh-picker.is-visible{display:block}.gsd-oh-container .gsd-oh-row a{display:inline-block;margin-left:10px;text-decoration:none;color:#3071a9}.gsd-oh-container .gsd-oh-row a.is-hidden{display:none}.gsd-oh-container .gsd-oh-row a:before{color:#3071a9}.gsd-oh-container .gsd-oh-row a.gsd-oh-more-btn,.gsd-oh-container .gsd-oh-row a.gsd-oh-add-btn{float:left;width:auto}.gsd-oh-container .gsd-oh-row a.gsd-oh-add-btn{margin-left:0}.gsd-oh-container .gsd-oh-row a.gsd-oh-add-btn span{display:inline-block;margin-left:5px;font-family:var(--body-font-family, Arial)}.gsd-oh-container .gsd-oh-row a.gsd-oh-add-btn span.is-hidden{display:none}.gsd-oh-container .gsd-oh-row .oh-more{margin-left:20px;display:none}.gsd-oh-container .gsd-oh-row .oh-more.is-visible{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.nr-app.j4 .gsd-oh-container .clockpicker input{width:60px}


css/styles.css000064400000144435152344346010007406 0ustar00@-webkit-keyframes spinAround{from{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes spinAround{from{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.is-clearfix:after{clear:both;content:" ";display:table}.is-pulled-left{float:left !important}.is-pulled-right{float:right !important}.is-clipped{overflow:hidden !important}.is-overlay{bottom:0;left:0;position:absolute;right:0;top:0}.is-size-1{font-size:3rem !important}.is-size-2{font-size:2.5rem !important}.is-size-3{font-size:2rem !important}.is-size-4{font-size:1.5rem !important}.is-size-5{font-size:1.25rem !important}.is-size-6{font-size:1rem !important}.is-size-7{font-size:.75rem !important}@media screen and (max-width: 768px){.is-size-1-mobile{font-size:3rem !important}.is-size-2-mobile{font-size:2.5rem !important}.is-size-3-mobile{font-size:2rem !important}.is-size-4-mobile{font-size:1.5rem !important}.is-size-5-mobile{font-size:1.25rem !important}.is-size-6-mobile{font-size:1rem !important}.is-size-7-mobile{font-size:.75rem !important}}@media screen and (min-width: 769px), print{.is-size-1-tablet{font-size:3rem !important}.is-size-2-tablet{font-size:2.5rem !important}.is-size-3-tablet{font-size:2rem !important}.is-size-4-tablet{font-size:1.5rem !important}.is-size-5-tablet{font-size:1.25rem !important}.is-size-6-tablet{font-size:1rem !important}.is-size-7-tablet{font-size:.75rem !important}}@media screen and (max-width: 1023px){.is-size-1-touch{font-size:3rem !important}.is-size-2-touch{font-size:2.5rem !important}.is-size-3-touch{font-size:2rem !important}.is-size-4-touch{font-size:1.5rem !important}.is-size-5-touch{font-size:1.25rem !important}.is-size-6-touch{font-size:1rem !important}.is-size-7-touch{font-size:.75rem !important}}@media screen and (min-width: 1024px){.is-size-1-desktop{font-size:3rem !important}.is-size-2-desktop{font-size:2.5rem !important}.is-size-3-desktop{font-size:2rem !important}.is-size-4-desktop{font-size:1.5rem !important}.is-size-5-desktop{font-size:1.25rem !important}.is-size-6-desktop{font-size:1rem !important}.is-size-7-desktop{font-size:.75rem !important}}@media screen and (min-width: 1216px){.is-size-1-widescreen{font-size:3rem !important}.is-size-2-widescreen{font-size:2.5rem !important}.is-size-3-widescreen{font-size:2rem !important}.is-size-4-widescreen{font-size:1.5rem !important}.is-size-5-widescreen{font-size:1.25rem !important}.is-size-6-widescreen{font-size:1rem !important}.is-size-7-widescreen{font-size:.75rem !important}}@media screen and (min-width: 1408px){.is-size-1-fullhd{font-size:3rem !important}.is-size-2-fullhd{font-size:2.5rem !important}.is-size-3-fullhd{font-size:2rem !important}.is-size-4-fullhd{font-size:1.5rem !important}.is-size-5-fullhd{font-size:1.25rem !important}.is-size-6-fullhd{font-size:1rem !important}.is-size-7-fullhd{font-size:.75rem !important}}.has-text-centered{text-align:center !important}@media screen and (max-width: 768px){.has-text-centered-mobile{text-align:center !important}}@media screen and (min-width: 769px), print{.has-text-centered-tablet{text-align:center !important}}@media screen and (min-width: 769px) and (max-width: 1023px){.has-text-centered-tablet-only{text-align:center !important}}@media screen and (max-width: 1023px){.has-text-centered-touch{text-align:center !important}}@media screen and (min-width: 1024px){.has-text-centered-desktop{text-align:center !important}}@media screen and (min-width: 1024px) and (max-width: 1215px){.has-text-centered-desktop-only{text-align:center !important}}@media screen and (min-width: 1216px){.has-text-centered-widescreen{text-align:center !important}}@media screen and (min-width: 1216px) and (max-width: 1407px){.has-text-centered-widescreen-only{text-align:center !important}}@media screen and (min-width: 1408px){.has-text-centered-fullhd{text-align:center !important}}.has-text-justified{text-align:justify !important}@media screen and (max-width: 768px){.has-text-justified-mobile{text-align:justify !important}}@media screen and (min-width: 769px), print{.has-text-justified-tablet{text-align:justify !important}}@media screen and (min-width: 769px) and (max-width: 1023px){.has-text-justified-tablet-only{text-align:justify !important}}@media screen and (max-width: 1023px){.has-text-justified-touch{text-align:justify !important}}@media screen and (min-width: 1024px){.has-text-justified-desktop{text-align:justify !important}}@media screen and (min-width: 1024px) and (max-width: 1215px){.has-text-justified-desktop-only{text-align:justify !important}}@media screen and (min-width: 1216px){.has-text-justified-widescreen{text-align:justify !important}}@media screen and (min-width: 1216px) and (max-width: 1407px){.has-text-justified-widescreen-only{text-align:justify !important}}@media screen and (min-width: 1408px){.has-text-justified-fullhd{text-align:justify !important}}.has-text-left{text-align:left !important}@media screen and (max-width: 768px){.has-text-left-mobile{text-align:left !important}}@media screen and (min-width: 769px), print{.has-text-left-tablet{text-align:left !important}}@media screen and (min-width: 769px) and (max-width: 1023px){.has-text-left-tablet-only{text-align:left !important}}@media screen and (max-width: 1023px){.has-text-left-touch{text-align:left !important}}@media screen and (min-width: 1024px){.has-text-left-desktop{text-align:left !important}}@media screen and (min-width: 1024px) and (max-width: 1215px){.has-text-left-desktop-only{text-align:left !important}}@media screen and (min-width: 1216px){.has-text-left-widescreen{text-align:left !important}}@media screen and (min-width: 1216px) and (max-width: 1407px){.has-text-left-widescreen-only{text-align:left !important}}@media screen and (min-width: 1408px){.has-text-left-fullhd{text-align:left !important}}.has-text-right{text-align:right !important}@media screen and (max-width: 768px){.has-text-right-mobile{text-align:right !important}}@media screen and (min-width: 769px), print{.has-text-right-tablet{text-align:right !important}}@media screen and (min-width: 769px) and (max-width: 1023px){.has-text-right-tablet-only{text-align:right !important}}@media screen and (max-width: 1023px){.has-text-right-touch{text-align:right !important}}@media screen and (min-width: 1024px){.has-text-right-desktop{text-align:right !important}}@media screen and (min-width: 1024px) and (max-width: 1215px){.has-text-right-desktop-only{text-align:right !important}}@media screen and (min-width: 1216px){.has-text-right-widescreen{text-align:right !important}}@media screen and (min-width: 1216px) and (max-width: 1407px){.has-text-right-widescreen-only{text-align:right !important}}@media screen and (min-width: 1408px){.has-text-right-fullhd{text-align:right !important}}.is-capitalized{text-transform:capitalize !important}.is-lowercase{text-transform:lowercase !important}.is-uppercase{text-transform:uppercase !important}.is-italic{font-style:italic !important}.has-text-white{color:#fff !important}a.has-text-white:hover,a.has-text-white:focus{color:#e6e6e6 !important}.has-text-black{color:#0a0a0a !important}a.has-text-black:hover,a.has-text-black:focus{color:#000 !important}.has-text-light{color:#f5f5f5 !important}a.has-text-light:hover,a.has-text-light:focus{color:#dbdbdb !important}.has-text-dark{color:#363636 !important}a.has-text-dark:hover,a.has-text-dark:focus{color:#1c1c1c !important}.has-text-primary{color:#00d1b2 !important}a.has-text-primary:hover,a.has-text-primary:focus{color:#009e86 !important}.has-text-link{color:#3273dc !important}a.has-text-link:hover,a.has-text-link:focus{color:#205bbc !important}.has-text-info{color:#209cee !important}a.has-text-info:hover,a.has-text-info:focus{color:#0f81cc !important}.has-text-success{color:#23d160 !important}a.has-text-success:hover,a.has-text-success:focus{color:#1ca64c !important}.has-text-warning{color:#ffdd57 !important}a.has-text-warning:hover,a.has-text-warning:focus{color:#ffd324 !important}.has-text-danger{color:#ff3860 !important}a.has-text-danger:hover,a.has-text-danger:focus{color:#ff0537 !important}.has-text-black-bis{color:#121212 !important}.has-text-black-ter{color:#242424 !important}.has-text-grey-darker{color:#363636 !important}.has-text-grey-dark{color:#4a4a4a !important}.has-text-grey{color:#7a7a7a !important}.has-text-grey-light{color:#b5b5b5 !important}.has-text-grey-lighter{color:#dbdbdb !important}.has-text-white-ter{color:#f5f5f5 !important}.has-text-white-bis{color:#fafafa !important}.has-text-weight-light{font-weight:300 !important}.has-text-weight-normal{font-weight:400 !important}.has-text-weight-semibold{font-weight:600 !important}.has-text-weight-bold{font-weight:700 !important}.is-block{display:block !important}@media screen and (max-width: 768px){.is-block-mobile{display:block !important}}@media screen and (min-width: 769px), print{.is-block-tablet{display:block !important}}@media screen and (min-width: 769px) and (max-width: 1023px){.is-block-tablet-only{display:block !important}}@media screen and (max-width: 1023px){.is-block-touch{display:block !important}}@media screen and (min-width: 1024px){.is-block-desktop{display:block !important}}@media screen and (min-width: 1024px) and (max-width: 1215px){.is-block-desktop-only{display:block !important}}@media screen and (min-width: 1216px){.is-block-widescreen{display:block !important}}@media screen and (min-width: 1216px) and (max-width: 1407px){.is-block-widescreen-only{display:block !important}}@media screen and (min-width: 1408px){.is-block-fullhd{display:block !important}}.is-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}@media screen and (max-width: 768px){.is-flex-mobile{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}}@media screen and (min-width: 769px), print{.is-flex-tablet{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}}@media screen and (min-width: 769px) and (max-width: 1023px){.is-flex-tablet-only{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}}@media screen and (max-width: 1023px){.is-flex-touch{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}}@media screen and (min-width: 1024px){.is-flex-desktop{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}}@media screen and (min-width: 1024px) and (max-width: 1215px){.is-flex-desktop-only{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}}@media screen and (min-width: 1216px){.is-flex-widescreen{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}}@media screen and (min-width: 1216px) and (max-width: 1407px){.is-flex-widescreen-only{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}}@media screen and (min-width: 1408px){.is-flex-fullhd{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}}.is-inline{display:inline !important}@media screen and (max-width: 768px){.is-inline-mobile{display:inline !important}}@media screen and (min-width: 769px), print{.is-inline-tablet{display:inline !important}}@media screen and (min-width: 769px) and (max-width: 1023px){.is-inline-tablet-only{display:inline !important}}@media screen and (max-width: 1023px){.is-inline-touch{display:inline !important}}@media screen and (min-width: 1024px){.is-inline-desktop{display:inline !important}}@media screen and (min-width: 1024px) and (max-width: 1215px){.is-inline-desktop-only{display:inline !important}}@media screen and (min-width: 1216px){.is-inline-widescreen{display:inline !important}}@media screen and (min-width: 1216px) and (max-width: 1407px){.is-inline-widescreen-only{display:inline !important}}@media screen and (min-width: 1408px){.is-inline-fullhd{display:inline !important}}.is-inline-block{display:inline-block !important}@media screen and (max-width: 768px){.is-inline-block-mobile{display:inline-block !important}}@media screen and (min-width: 769px), print{.is-inline-block-tablet{display:inline-block !important}}@media screen and (min-width: 769px) and (max-width: 1023px){.is-inline-block-tablet-only{display:inline-block !important}}@media screen and (max-width: 1023px){.is-inline-block-touch{display:inline-block !important}}@media screen and (min-width: 1024px){.is-inline-block-desktop{display:inline-block !important}}@media screen and (min-width: 1024px) and (max-width: 1215px){.is-inline-block-desktop-only{display:inline-block !important}}@media screen and (min-width: 1216px){.is-inline-block-widescreen{display:inline-block !important}}@media screen and (min-width: 1216px) and (max-width: 1407px){.is-inline-block-widescreen-only{display:inline-block !important}}@media screen and (min-width: 1408px){.is-inline-block-fullhd{display:inline-block !important}}.is-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}@media screen and (max-width: 768px){.is-inline-flex-mobile{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media screen and (min-width: 769px), print{.is-inline-flex-tablet{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media screen and (min-width: 769px) and (max-width: 1023px){.is-inline-flex-tablet-only{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media screen and (max-width: 1023px){.is-inline-flex-touch{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media screen and (min-width: 1024px){.is-inline-flex-desktop{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media screen and (min-width: 1024px) and (max-width: 1215px){.is-inline-flex-desktop-only{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media screen and (min-width: 1216px){.is-inline-flex-widescreen{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media screen and (min-width: 1216px) and (max-width: 1407px){.is-inline-flex-widescreen-only{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media screen and (min-width: 1408px){.is-inline-flex-fullhd{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}.is-hidden{display:none !important}@media screen and (max-width: 768px){.is-hidden-mobile{display:none !important}}@media screen and (min-width: 769px), print{.is-hidden-tablet{display:none !important}}@media screen and (min-width: 769px) and (max-width: 1023px){.is-hidden-tablet-only{display:none !important}}@media screen and (max-width: 1023px){.is-hidden-touch{display:none !important}}@media screen and (min-width: 1024px){.is-hidden-desktop{display:none !important}}@media screen and (min-width: 1024px) and (max-width: 1215px){.is-hidden-desktop-only{display:none !important}}@media screen and (min-width: 1216px){.is-hidden-widescreen{display:none !important}}@media screen and (min-width: 1216px) and (max-width: 1407px){.is-hidden-widescreen-only{display:none !important}}@media screen and (min-width: 1408px){.is-hidden-fullhd{display:none !important}}.is-invisible{visibility:hidden !important}@media screen and (max-width: 768px){.is-invisible-mobile{visibility:hidden !important}}@media screen and (min-width: 769px), print{.is-invisible-tablet{visibility:hidden !important}}@media screen and (min-width: 769px) and (max-width: 1023px){.is-invisible-tablet-only{visibility:hidden !important}}@media screen and (max-width: 1023px){.is-invisible-touch{visibility:hidden !important}}@media screen and (min-width: 1024px){.is-invisible-desktop{visibility:hidden !important}}@media screen and (min-width: 1024px) and (max-width: 1215px){.is-invisible-desktop-only{visibility:hidden !important}}@media screen and (min-width: 1216px){.is-invisible-widescreen{visibility:hidden !important}}@media screen and (min-width: 1216px) and (max-width: 1407px){.is-invisible-widescreen-only{visibility:hidden !important}}@media screen and (min-width: 1408px){.is-invisible-fullhd{visibility:hidden !important}}.is-marginless{margin:0 !important}.is-paddingless{padding:0 !important}.is-radiusless{border-radius:0 !important}.is-shadowless{-webkit-box-shadow:none !important;box-shadow:none !important}.is-unselectable{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.column{display:block;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-ms-flex-negative:1;flex-shrink:1;padding:.75rem}.columns.is-mobile>.column.is-narrow{-webkit-box-flex:0;-ms-flex:none;flex:none}.columns.is-mobile>.column.is-full{-webkit-box-flex:0;-ms-flex:none;flex:none;width:100%}.columns.is-mobile>.column.is-three-quarters{-webkit-box-flex:0;-ms-flex:none;flex:none;width:75%}.columns.is-mobile>.column.is-two-thirds{-webkit-box-flex:0;-ms-flex:none;flex:none;width:66.6666%}.columns.is-mobile>.column.is-half{-webkit-box-flex:0;-ms-flex:none;flex:none;width:50%}.columns.is-mobile>.column.is-one-third{-webkit-box-flex:0;-ms-flex:none;flex:none;width:33.3333%}.columns.is-mobile>.column.is-one-quarter{-webkit-box-flex:0;-ms-flex:none;flex:none;width:25%}.columns.is-mobile>.column.is-one-fifth{-webkit-box-flex:0;-ms-flex:none;flex:none;width:20%}.columns.is-mobile>.column.is-two-fifths{-webkit-box-flex:0;-ms-flex:none;flex:none;width:40%}.columns.is-mobile>.column.is-three-fifths{-webkit-box-flex:0;-ms-flex:none;flex:none;width:60%}.columns.is-mobile>.column.is-four-fifths{-webkit-box-flex:0;-ms-flex:none;flex:none;width:80%}.columns.is-mobile>.column.is-offset-three-quarters{margin-left:75%}.columns.is-mobile>.column.is-offset-two-thirds{margin-left:66.6666%}.columns.is-mobile>.column.is-offset-half{margin-left:50%}.columns.is-mobile>.column.is-offset-one-third{margin-left:33.3333%}.columns.is-mobile>.column.is-offset-one-quarter{margin-left:25%}.columns.is-mobile>.column.is-offset-one-fifth{margin-left:20%}.columns.is-mobile>.column.is-offset-two-fifths{margin-left:40%}.columns.is-mobile>.column.is-offset-three-fifths{margin-left:60%}.columns.is-mobile>.column.is-offset-four-fifths{margin-left:80%}.columns.is-mobile>.column.is-1{-webkit-box-flex:0;-ms-flex:none;flex:none;width:8.33333%}.columns.is-mobile>.column.is-offset-1{margin-left:8.33333%}.columns.is-mobile>.column.is-2{-webkit-box-flex:0;-ms-flex:none;flex:none;width:16.66667%}.columns.is-mobile>.column.is-offset-2{margin-left:16.66667%}.columns.is-mobile>.column.is-3{-webkit-box-flex:0;-ms-flex:none;flex:none;width:25%}.columns.is-mobile>.column.is-offset-3{margin-left:25%}.columns.is-mobile>.column.is-4{-webkit-box-flex:0;-ms-flex:none;flex:none;width:33.33333%}.columns.is-mobile>.column.is-offset-4{margin-left:33.33333%}.columns.is-mobile>.column.is-5{-webkit-box-flex:0;-ms-flex:none;flex:none;width:41.66667%}.columns.is-mobile>.column.is-offset-5{margin-left:41.66667%}.columns.is-mobile>.column.is-6{-webkit-box-flex:0;-ms-flex:none;flex:none;width:50%}.columns.is-mobile>.column.is-offset-6{margin-left:50%}.columns.is-mobile>.column.is-7{-webkit-box-flex:0;-ms-flex:none;flex:none;width:58.33333%}.columns.is-mobile>.column.is-offset-7{margin-left:58.33333%}.columns.is-mobile>.column.is-8{-webkit-box-flex:0;-ms-flex:none;flex:none;width:66.66667%}.columns.is-mobile>.column.is-offset-8{margin-left:66.66667%}.columns.is-mobile>.column.is-9{-webkit-box-flex:0;-ms-flex:none;flex:none;width:75%}.columns.is-mobile>.column.is-offset-9{margin-left:75%}.columns.is-mobile>.column.is-10{-webkit-box-flex:0;-ms-flex:none;flex:none;width:83.33333%}.columns.is-mobile>.column.is-offset-10{margin-left:83.33333%}.columns.is-mobile>.column.is-11{-webkit-box-flex:0;-ms-flex:none;flex:none;width:91.66667%}.columns.is-mobile>.column.is-offset-11{margin-left:91.66667%}.columns.is-mobile>.column.is-12{-webkit-box-flex:0;-ms-flex:none;flex:none;width:100%}.columns.is-mobile>.column.is-offset-12{margin-left:100%}@media screen and (max-width: 768px){.column.is-narrow-mobile{-webkit-box-flex:0;-ms-flex:none;flex:none}.column.is-full-mobile{-webkit-box-flex:0;-ms-flex:none;flex:none;width:100%}.column.is-three-quarters-mobile{-webkit-box-flex:0;-ms-flex:none;flex:none;width:75%}.column.is-two-thirds-mobile{-webkit-box-flex:0;-ms-flex:none;flex:none;width:66.6666%}.column.is-half-mobile{-webkit-box-flex:0;-ms-flex:none;flex:none;width:50%}.column.is-one-third-mobile{-webkit-box-flex:0;-ms-flex:none;flex:none;width:33.3333%}.column.is-one-quarter-mobile{-webkit-box-flex:0;-ms-flex:none;flex:none;width:25%}.column.is-one-fifth-mobile{-webkit-box-flex:0;-ms-flex:none;flex:none;width:20%}.column.is-two-fifths-mobile{-webkit-box-flex:0;-ms-flex:none;flex:none;width:40%}.column.is-three-fifths-mobile{-webkit-box-flex:0;-ms-flex:none;flex:none;width:60%}.column.is-four-fifths-mobile{-webkit-box-flex:0;-ms-flex:none;flex:none;width:80%}.column.is-offset-three-quarters-mobile{margin-left:75%}.column.is-offset-two-thirds-mobile{margin-left:66.6666%}.column.is-offset-half-mobile{margin-left:50%}.column.is-offset-one-third-mobile{margin-left:33.3333%}.column.is-offset-one-quarter-mobile{margin-left:25%}.column.is-offset-one-fifth-mobile{margin-left:20%}.column.is-offset-two-fifths-mobile{margin-left:40%}.column.is-offset-three-fifths-mobile{margin-left:60%}.column.is-offset-four-fifths-mobile{margin-left:80%}.column.is-1-mobile{-webkit-box-flex:0;-ms-flex:none;flex:none;width:8.33333%}.column.is-offset-1-mobile{margin-left:8.33333%}.column.is-2-mobile{-webkit-box-flex:0;-ms-flex:none;flex:none;width:16.66667%}.column.is-offset-2-mobile{margin-left:16.66667%}.column.is-3-mobile{-webkit-box-flex:0;-ms-flex:none;flex:none;width:25%}.column.is-offset-3-mobile{margin-left:25%}.column.is-4-mobile{-webkit-box-flex:0;-ms-flex:none;flex:none;width:33.33333%}.column.is-offset-4-mobile{margin-left:33.33333%}.column.is-5-mobile{-webkit-box-flex:0;-ms-flex:none;flex:none;width:41.66667%}.column.is-offset-5-mobile{margin-left:41.66667%}.column.is-6-mobile{-webkit-box-flex:0;-ms-flex:none;flex:none;width:50%}.column.is-offset-6-mobile{margin-left:50%}.column.is-7-mobile{-webkit-box-flex:0;-ms-flex:none;flex:none;width:58.33333%}.column.is-offset-7-mobile{margin-left:58.33333%}.column.is-8-mobile{-webkit-box-flex:0;-ms-flex:none;flex:none;width:66.66667%}.column.is-offset-8-mobile{margin-left:66.66667%}.column.is-9-mobile{-webkit-box-flex:0;-ms-flex:none;flex:none;width:75%}.column.is-offset-9-mobile{margin-left:75%}.column.is-10-mobile{-webkit-box-flex:0;-ms-flex:none;flex:none;width:83.33333%}.column.is-offset-10-mobile{margin-left:83.33333%}.column.is-11-mobile{-webkit-box-flex:0;-ms-flex:none;flex:none;width:91.66667%}.column.is-offset-11-mobile{margin-left:91.66667%}.column.is-12-mobile{-webkit-box-flex:0;-ms-flex:none;flex:none;width:100%}.column.is-offset-12-mobile{margin-left:100%}}@media screen and (min-width: 769px), print{.column.is-narrow,.column.is-narrow-tablet{-webkit-box-flex:0;-ms-flex:none;flex:none}.column.is-full,.column.is-full-tablet{-webkit-box-flex:0;-ms-flex:none;flex:none;width:100%}.column.is-three-quarters,.column.is-three-quarters-tablet{-webkit-box-flex:0;-ms-flex:none;flex:none;width:75%}.column.is-two-thirds,.column.is-two-thirds-tablet{-webkit-box-flex:0;-ms-flex:none;flex:none;width:66.6666%}.column.is-half,.column.is-half-tablet{-webkit-box-flex:0;-ms-flex:none;flex:none;width:50%}.column.is-one-third,.column.is-one-third-tablet{-webkit-box-flex:0;-ms-flex:none;flex:none;width:33.3333%}.column.is-one-quarter,.column.is-one-quarter-tablet{-webkit-box-flex:0;-ms-flex:none;flex:none;width:25%}.column.is-one-fifth,.column.is-one-fifth-tablet{-webkit-box-flex:0;-ms-flex:none;flex:none;width:20%}.column.is-two-fifths,.column.is-two-fifths-tablet{-webkit-box-flex:0;-ms-flex:none;flex:none;width:40%}.column.is-three-fifths,.column.is-three-fifths-tablet{-webkit-box-flex:0;-ms-flex:none;flex:none;width:60%}.column.is-four-fifths,.column.is-four-fifths-tablet{-webkit-box-flex:0;-ms-flex:none;flex:none;width:80%}.column.is-offset-three-quarters,.column.is-offset-three-quarters-tablet{margin-left:75%}.column.is-offset-two-thirds,.column.is-offset-two-thirds-tablet{margin-left:66.6666%}.column.is-offset-half,.column.is-offset-half-tablet{margin-left:50%}.column.is-offset-one-third,.column.is-offset-one-third-tablet{margin-left:33.3333%}.column.is-offset-one-quarter,.column.is-offset-one-quarter-tablet{margin-left:25%}.column.is-offset-one-fifth,.column.is-offset-one-fifth-tablet{margin-left:20%}.column.is-offset-two-fifths,.column.is-offset-two-fifths-tablet{margin-left:40%}.column.is-offset-three-fifths,.column.is-offset-three-fifths-tablet{margin-left:60%}.column.is-offset-four-fifths,.column.is-offset-four-fifths-tablet{margin-left:80%}.column.is-1,.column.is-1-tablet{-webkit-box-flex:0;-ms-flex:none;flex:none;width:8.33333%}.column.is-offset-1,.column.is-offset-1-tablet{margin-left:8.33333%}.column.is-2,.column.is-2-tablet{-webkit-box-flex:0;-ms-flex:none;flex:none;width:16.66667%}.column.is-offset-2,.column.is-offset-2-tablet{margin-left:16.66667%}.column.is-3,.column.is-3-tablet{-webkit-box-flex:0;-ms-flex:none;flex:none;width:25%}.column.is-offset-3,.column.is-offset-3-tablet{margin-left:25%}.column.is-4,.column.is-4-tablet{-webkit-box-flex:0;-ms-flex:none;flex:none;width:33.33333%}.column.is-offset-4,.column.is-offset-4-tablet{margin-left:33.33333%}.column.is-5,.column.is-5-tablet{-webkit-box-flex:0;-ms-flex:none;flex:none;width:41.66667%}.column.is-offset-5,.column.is-offset-5-tablet{margin-left:41.66667%}.column.is-6,.column.is-6-tablet{-webkit-box-flex:0;-ms-flex:none;flex:none;width:50%}.column.is-offset-6,.column.is-offset-6-tablet{margin-left:50%}.column.is-7,.column.is-7-tablet{-webkit-box-flex:0;-ms-flex:none;flex:none;width:58.33333%}.column.is-offset-7,.column.is-offset-7-tablet{margin-left:58.33333%}.column.is-8,.column.is-8-tablet{-webkit-box-flex:0;-ms-flex:none;flex:none;width:66.66667%}.column.is-offset-8,.column.is-offset-8-tablet{margin-left:66.66667%}.column.is-9,.column.is-9-tablet{-webkit-box-flex:0;-ms-flex:none;flex:none;width:75%}.column.is-offset-9,.column.is-offset-9-tablet{margin-left:75%}.column.is-10,.column.is-10-tablet{-webkit-box-flex:0;-ms-flex:none;flex:none;width:83.33333%}.column.is-offset-10,.column.is-offset-10-tablet{margin-left:83.33333%}.column.is-11,.column.is-11-tablet{-webkit-box-flex:0;-ms-flex:none;flex:none;width:91.66667%}.column.is-offset-11,.column.is-offset-11-tablet{margin-left:91.66667%}.column.is-12,.column.is-12-tablet{-webkit-box-flex:0;-ms-flex:none;flex:none;width:100%}.column.is-offset-12,.column.is-offset-12-tablet{margin-left:100%}}@media screen and (max-width: 1023px){.column.is-narrow-touch{-webkit-box-flex:0;-ms-flex:none;flex:none}.column.is-full-touch{-webkit-box-flex:0;-ms-flex:none;flex:none;width:100%}.column.is-three-quarters-touch{-webkit-box-flex:0;-ms-flex:none;flex:none;width:75%}.column.is-two-thirds-touch{-webkit-box-flex:0;-ms-flex:none;flex:none;width:66.6666%}.column.is-half-touch{-webkit-box-flex:0;-ms-flex:none;flex:none;width:50%}.column.is-one-third-touch{-webkit-box-flex:0;-ms-flex:none;flex:none;width:33.3333%}.column.is-one-quarter-touch{-webkit-box-flex:0;-ms-flex:none;flex:none;width:25%}.column.is-one-fifth-touch{-webkit-box-flex:0;-ms-flex:none;flex:none;width:20%}.column.is-two-fifths-touch{-webkit-box-flex:0;-ms-flex:none;flex:none;width:40%}.column.is-three-fifths-touch{-webkit-box-flex:0;-ms-flex:none;flex:none;width:60%}.column.is-four-fifths-touch{-webkit-box-flex:0;-ms-flex:none;flex:none;width:80%}.column.is-offset-three-quarters-touch{margin-left:75%}.column.is-offset-two-thirds-touch{margin-left:66.6666%}.column.is-offset-half-touch{margin-left:50%}.column.is-offset-one-third-touch{margin-left:33.3333%}.column.is-offset-one-quarter-touch{margin-left:25%}.column.is-offset-one-fifth-touch{margin-left:20%}.column.is-offset-two-fifths-touch{margin-left:40%}.column.is-offset-three-fifths-touch{margin-left:60%}.column.is-offset-four-fifths-touch{margin-left:80%}.column.is-1-touch{-webkit-box-flex:0;-ms-flex:none;flex:none;width:8.33333%}.column.is-offset-1-touch{margin-left:8.33333%}.column.is-2-touch{-webkit-box-flex:0;-ms-flex:none;flex:none;width:16.66667%}.column.is-offset-2-touch{margin-left:16.66667%}.column.is-3-touch{-webkit-box-flex:0;-ms-flex:none;flex:none;width:25%}.column.is-offset-3-touch{margin-left:25%}.column.is-4-touch{-webkit-box-flex:0;-ms-flex:none;flex:none;width:33.33333%}.column.is-offset-4-touch{margin-left:33.33333%}.column.is-5-touch{-webkit-box-flex:0;-ms-flex:none;flex:none;width:41.66667%}.column.is-offset-5-touch{margin-left:41.66667%}.column.is-6-touch{-webkit-box-flex:0;-ms-flex:none;flex:none;width:50%}.column.is-offset-6-touch{margin-left:50%}.column.is-7-touch{-webkit-box-flex:0;-ms-flex:none;flex:none;width:58.33333%}.column.is-offset-7-touch{margin-left:58.33333%}.column.is-8-touch{-webkit-box-flex:0;-ms-flex:none;flex:none;width:66.66667%}.column.is-offset-8-touch{margin-left:66.66667%}.column.is-9-touch{-webkit-box-flex:0;-ms-flex:none;flex:none;width:75%}.column.is-offset-9-touch{margin-left:75%}.column.is-10-touch{-webkit-box-flex:0;-ms-flex:none;flex:none;width:83.33333%}.column.is-offset-10-touch{margin-left:83.33333%}.column.is-11-touch{-webkit-box-flex:0;-ms-flex:none;flex:none;width:91.66667%}.column.is-offset-11-touch{margin-left:91.66667%}.column.is-12-touch{-webkit-box-flex:0;-ms-flex:none;flex:none;width:100%}.column.is-offset-12-touch{margin-left:100%}}@media screen and (min-width: 1024px){.column.is-narrow-desktop{-webkit-box-flex:0;-ms-flex:none;flex:none}.column.is-full-desktop{-webkit-box-flex:0;-ms-flex:none;flex:none;width:100%}.column.is-three-quarters-desktop{-webkit-box-flex:0;-ms-flex:none;flex:none;width:75%}.column.is-two-thirds-desktop{-webkit-box-flex:0;-ms-flex:none;flex:none;width:66.6666%}.column.is-half-desktop{-webkit-box-flex:0;-ms-flex:none;flex:none;width:50%}.column.is-one-third-desktop{-webkit-box-flex:0;-ms-flex:none;flex:none;width:33.3333%}.column.is-one-quarter-desktop{-webkit-box-flex:0;-ms-flex:none;flex:none;width:25%}.column.is-one-fifth-desktop{-webkit-box-flex:0;-ms-flex:none;flex:none;width:20%}.column.is-two-fifths-desktop{-webkit-box-flex:0;-ms-flex:none;flex:none;width:40%}.column.is-three-fifths-desktop{-webkit-box-flex:0;-ms-flex:none;flex:none;width:60%}.column.is-four-fifths-desktop{-webkit-box-flex:0;-ms-flex:none;flex:none;width:80%}.column.is-offset-three-quarters-desktop{margin-left:75%}.column.is-offset-two-thirds-desktop{margin-left:66.6666%}.column.is-offset-half-desktop{margin-left:50%}.column.is-offset-one-third-desktop{margin-left:33.3333%}.column.is-offset-one-quarter-desktop{margin-left:25%}.column.is-offset-one-fifth-desktop{margin-left:20%}.column.is-offset-two-fifths-desktop{margin-left:40%}.column.is-offset-three-fifths-desktop{margin-left:60%}.column.is-offset-four-fifths-desktop{margin-left:80%}.column.is-1-desktop{-webkit-box-flex:0;-ms-flex:none;flex:none;width:8.33333%}.column.is-offset-1-desktop{margin-left:8.33333%}.column.is-2-desktop{-webkit-box-flex:0;-ms-flex:none;flex:none;width:16.66667%}.column.is-offset-2-desktop{margin-left:16.66667%}.column.is-3-desktop{-webkit-box-flex:0;-ms-flex:none;flex:none;width:25%}.column.is-offset-3-desktop{margin-left:25%}.column.is-4-desktop{-webkit-box-flex:0;-ms-flex:none;flex:none;width:33.33333%}.column.is-offset-4-desktop{margin-left:33.33333%}.column.is-5-desktop{-webkit-box-flex:0;-ms-flex:none;flex:none;width:41.66667%}.column.is-offset-5-desktop{margin-left:41.66667%}.column.is-6-desktop{-webkit-box-flex:0;-ms-flex:none;flex:none;width:50%}.column.is-offset-6-desktop{margin-left:50%}.column.is-7-desktop{-webkit-box-flex:0;-ms-flex:none;flex:none;width:58.33333%}.column.is-offset-7-desktop{margin-left:58.33333%}.column.is-8-desktop{-webkit-box-flex:0;-ms-flex:none;flex:none;width:66.66667%}.column.is-offset-8-desktop{margin-left:66.66667%}.column.is-9-desktop{-webkit-box-flex:0;-ms-flex:none;flex:none;width:75%}.column.is-offset-9-desktop{margin-left:75%}.column.is-10-desktop{-webkit-box-flex:0;-ms-flex:none;flex:none;width:83.33333%}.column.is-offset-10-desktop{margin-left:83.33333%}.column.is-11-desktop{-webkit-box-flex:0;-ms-flex:none;flex:none;width:91.66667%}.column.is-offset-11-desktop{margin-left:91.66667%}.column.is-12-desktop{-webkit-box-flex:0;-ms-flex:none;flex:none;width:100%}.column.is-offset-12-desktop{margin-left:100%}}@media screen and (min-width: 1216px){.column.is-narrow-widescreen{-webkit-box-flex:0;-ms-flex:none;flex:none}.column.is-full-widescreen{-webkit-box-flex:0;-ms-flex:none;flex:none;width:100%}.column.is-three-quarters-widescreen{-webkit-box-flex:0;-ms-flex:none;flex:none;width:75%}.column.is-two-thirds-widescreen{-webkit-box-flex:0;-ms-flex:none;flex:none;width:66.6666%}.column.is-half-widescreen{-webkit-box-flex:0;-ms-flex:none;flex:none;width:50%}.column.is-one-third-widescreen{-webkit-box-flex:0;-ms-flex:none;flex:none;width:33.3333%}.column.is-one-quarter-widescreen{-webkit-box-flex:0;-ms-flex:none;flex:none;width:25%}.column.is-one-fifth-widescreen{-webkit-box-flex:0;-ms-flex:none;flex:none;width:20%}.column.is-two-fifths-widescreen{-webkit-box-flex:0;-ms-flex:none;flex:none;width:40%}.column.is-three-fifths-widescreen{-webkit-box-flex:0;-ms-flex:none;flex:none;width:60%}.column.is-four-fifths-widescreen{-webkit-box-flex:0;-ms-flex:none;flex:none;width:80%}.column.is-offset-three-quarters-widescreen{margin-left:75%}.column.is-offset-two-thirds-widescreen{margin-left:66.6666%}.column.is-offset-half-widescreen{margin-left:50%}.column.is-offset-one-third-widescreen{margin-left:33.3333%}.column.is-offset-one-quarter-widescreen{margin-left:25%}.column.is-offset-one-fifth-widescreen{margin-left:20%}.column.is-offset-two-fifths-widescreen{margin-left:40%}.column.is-offset-three-fifths-widescreen{margin-left:60%}.column.is-offset-four-fifths-widescreen{margin-left:80%}.column.is-1-widescreen{-webkit-box-flex:0;-ms-flex:none;flex:none;width:8.33333%}.column.is-offset-1-widescreen{margin-left:8.33333%}.column.is-2-widescreen{-webkit-box-flex:0;-ms-flex:none;flex:none;width:16.66667%}.column.is-offset-2-widescreen{margin-left:16.66667%}.column.is-3-widescreen{-webkit-box-flex:0;-ms-flex:none;flex:none;width:25%}.column.is-offset-3-widescreen{margin-left:25%}.column.is-4-widescreen{-webkit-box-flex:0;-ms-flex:none;flex:none;width:33.33333%}.column.is-offset-4-widescreen{margin-left:33.33333%}.column.is-5-widescreen{-webkit-box-flex:0;-ms-flex:none;flex:none;width:41.66667%}.column.is-offset-5-widescreen{margin-left:41.66667%}.column.is-6-widescreen{-webkit-box-flex:0;-ms-flex:none;flex:none;width:50%}.column.is-offset-6-widescreen{margin-left:50%}.column.is-7-widescreen{-webkit-box-flex:0;-ms-flex:none;flex:none;width:58.33333%}.column.is-offset-7-widescreen{margin-left:58.33333%}.column.is-8-widescreen{-webkit-box-flex:0;-ms-flex:none;flex:none;width:66.66667%}.column.is-offset-8-widescreen{margin-left:66.66667%}.column.is-9-widescreen{-webkit-box-flex:0;-ms-flex:none;flex:none;width:75%}.column.is-offset-9-widescreen{margin-left:75%}.column.is-10-widescreen{-webkit-box-flex:0;-ms-flex:none;flex:none;width:83.33333%}.column.is-offset-10-widescreen{margin-left:83.33333%}.column.is-11-widescreen{-webkit-box-flex:0;-ms-flex:none;flex:none;width:91.66667%}.column.is-offset-11-widescreen{margin-left:91.66667%}.column.is-12-widescreen{-webkit-box-flex:0;-ms-flex:none;flex:none;width:100%}.column.is-offset-12-widescreen{margin-left:100%}}@media screen and (min-width: 1408px){.column.is-narrow-fullhd{-webkit-box-flex:0;-ms-flex:none;flex:none}.column.is-full-fullhd{-webkit-box-flex:0;-ms-flex:none;flex:none;width:100%}.column.is-three-quarters-fullhd{-webkit-box-flex:0;-ms-flex:none;flex:none;width:75%}.column.is-two-thirds-fullhd{-webkit-box-flex:0;-ms-flex:none;flex:none;width:66.6666%}.column.is-half-fullhd{-webkit-box-flex:0;-ms-flex:none;flex:none;width:50%}.column.is-one-third-fullhd{-webkit-box-flex:0;-ms-flex:none;flex:none;width:33.3333%}.column.is-one-quarter-fullhd{-webkit-box-flex:0;-ms-flex:none;flex:none;width:25%}.column.is-one-fifth-fullhd{-webkit-box-flex:0;-ms-flex:none;flex:none;width:20%}.column.is-two-fifths-fullhd{-webkit-box-flex:0;-ms-flex:none;flex:none;width:40%}.column.is-three-fifths-fullhd{-webkit-box-flex:0;-ms-flex:none;flex:none;width:60%}.column.is-four-fifths-fullhd{-webkit-box-flex:0;-ms-flex:none;flex:none;width:80%}.column.is-offset-three-quarters-fullhd{margin-left:75%}.column.is-offset-two-thirds-fullhd{margin-left:66.6666%}.column.is-offset-half-fullhd{margin-left:50%}.column.is-offset-one-third-fullhd{margin-left:33.3333%}.column.is-offset-one-quarter-fullhd{margin-left:25%}.column.is-offset-one-fifth-fullhd{margin-left:20%}.column.is-offset-two-fifths-fullhd{margin-left:40%}.column.is-offset-three-fifths-fullhd{margin-left:60%}.column.is-offset-four-fifths-fullhd{margin-left:80%}.column.is-1-fullhd{-webkit-box-flex:0;-ms-flex:none;flex:none;width:8.33333%}.column.is-offset-1-fullhd{margin-left:8.33333%}.column.is-2-fullhd{-webkit-box-flex:0;-ms-flex:none;flex:none;width:16.66667%}.column.is-offset-2-fullhd{margin-left:16.66667%}.column.is-3-fullhd{-webkit-box-flex:0;-ms-flex:none;flex:none;width:25%}.column.is-offset-3-fullhd{margin-left:25%}.column.is-4-fullhd{-webkit-box-flex:0;-ms-flex:none;flex:none;width:33.33333%}.column.is-offset-4-fullhd{margin-left:33.33333%}.column.is-5-fullhd{-webkit-box-flex:0;-ms-flex:none;flex:none;width:41.66667%}.column.is-offset-5-fullhd{margin-left:41.66667%}.column.is-6-fullhd{-webkit-box-flex:0;-ms-flex:none;flex:none;width:50%}.column.is-offset-6-fullhd{margin-left:50%}.column.is-7-fullhd{-webkit-box-flex:0;-ms-flex:none;flex:none;width:58.33333%}.column.is-offset-7-fullhd{margin-left:58.33333%}.column.is-8-fullhd{-webkit-box-flex:0;-ms-flex:none;flex:none;width:66.66667%}.column.is-offset-8-fullhd{margin-left:66.66667%}.column.is-9-fullhd{-webkit-box-flex:0;-ms-flex:none;flex:none;width:75%}.column.is-offset-9-fullhd{margin-left:75%}.column.is-10-fullhd{-webkit-box-flex:0;-ms-flex:none;flex:none;width:83.33333%}.column.is-offset-10-fullhd{margin-left:83.33333%}.column.is-11-fullhd{-webkit-box-flex:0;-ms-flex:none;flex:none;width:91.66667%}.column.is-offset-11-fullhd{margin-left:91.66667%}.column.is-12-fullhd{-webkit-box-flex:0;-ms-flex:none;flex:none;width:100%}.column.is-offset-12-fullhd{margin-left:100%}}.columns{margin-left:-.75rem;margin-right:-.75rem;margin-top:-.75rem}.columns:last-child{margin-bottom:-.75rem}.columns:not(:last-child){margin-bottom:calc(1.5rem - .75rem)}.columns.is-centered{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.columns.is-gapless{margin-left:0;margin-right:0;margin-top:0}.columns.is-gapless>.column{margin:0;padding:0 !important}.columns.is-gapless:not(:last-child){margin-bottom:1.5rem}.columns.is-gapless:last-child{margin-bottom:0}.columns.is-mobile{display:-webkit-box;display:-ms-flexbox;display:flex}.columns.is-multiline{-ms-flex-wrap:wrap;flex-wrap:wrap}.columns.is-vcentered{-webkit-box-align:center;-ms-flex-align:center;align-items:center}@media screen and (min-width: 769px), print{.columns:not(.is-desktop){display:-webkit-box;display:-ms-flexbox;display:flex}}@media screen and (min-width: 1024px){.columns.is-desktop{display:-webkit-box;display:-ms-flexbox;display:flex}}.columns.is-variable{--columnGap: 0.75rem;margin-left:calc(-1 * var(--columnGap));margin-right:calc(-1 * var(--columnGap))}.columns.is-variable .column{padding-left:var(--columnGap);padding-right:var(--columnGap)}.columns.is-variable.is-0{--columnGap: 0rem}.columns.is-variable.is-1{--columnGap: .25rem}.columns.is-variable.is-2{--columnGap: .5rem}.columns.is-variable.is-3{--columnGap: .75rem}.columns.is-variable.is-4{--columnGap: 1rem}.columns.is-variable.is-5{--columnGap: 1.25rem}.columns.is-variable.is-6{--columnGap: 1.5rem}.columns.is-variable.is-7{--columnGap: 1.75rem}.columns.is-variable.is-8{--columnGap: 2rem}.tile{-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;display:block;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-ms-flex-negative:1;flex-shrink:1;min-height:-webkit-min-content;min-height:-moz-min-content;min-height:min-content}.tile.is-ancestor{margin-left:-0.75rem;margin-right:-0.75rem;margin-top:-0.75rem}.tile.is-ancestor:last-child{margin-bottom:-0.75rem}.tile.is-ancestor:not(:last-child){margin-bottom:0.75rem}.tile.is-child{margin:0 !important}.tile.is-parent{padding:0.75rem}.tile.is-vertical{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.tile.is-vertical>.tile.is-child:not(:last-child){margin-bottom:1.5rem !important}@media screen and (min-width: 769px), print{.tile:not(.is-child){display:-webkit-box;display:-ms-flexbox;display:flex}.tile.is-1{-webkit-box-flex:0;-ms-flex:none;flex:none;width:8.33333%}.tile.is-2{-webkit-box-flex:0;-ms-flex:none;flex:none;width:16.66667%}.tile.is-3{-webkit-box-flex:0;-ms-flex:none;flex:none;width:25%}.tile.is-4{-webkit-box-flex:0;-ms-flex:none;flex:none;width:33.33333%}.tile.is-5{-webkit-box-flex:0;-ms-flex:none;flex:none;width:41.66667%}.tile.is-6{-webkit-box-flex:0;-ms-flex:none;flex:none;width:50%}.tile.is-7{-webkit-box-flex:0;-ms-flex:none;flex:none;width:58.33333%}.tile.is-8{-webkit-box-flex:0;-ms-flex:none;flex:none;width:66.66667%}.tile.is-9{-webkit-box-flex:0;-ms-flex:none;flex:none;width:75%}.tile.is-10{-webkit-box-flex:0;-ms-flex:none;flex:none;width:83.33333%}.tile.is-11{-webkit-box-flex:0;-ms-flex:none;flex:none;width:91.66667%}.tile.is-12{-webkit-box-flex:0;-ms-flex:none;flex:none;width:100%}}.nr-main-header,.nr-box,.well.nr-well{background-color:#fff;padding:25px;border:solid 1px #dedbdb;border-radius:0}table.nr-app-stats{width:100%}table.nr-app-stats th,table.nr-app-stats td{padding:12.5px 0;border:solid 1px #eaeaea;border-left:none;border-right:none}table.nr-app-stats tbody>tr:first-child td{border-top:none}table.nr-app-stats tbody>tr:last-child td{border-bottom:none}.bar{display:block;height:4px;width:100%;background-color:#eaeaea;margin:4px 0}.bar span{height:100%;background-color:#d8862a;display:block}.socialNav{list-style:none;margin:0;padding:0}.download_key_actions a{margin-bottom:5px}.nr-app-addons{margin:0 0 -18px -180px}.nr-app-addons .btn{white-space:nowrap;outline:none}.nr-app-addons .addonTitle{font-weight:bold;margin-bottom:3px}.nr-app-addons [class^="icon-"]{margin:0}.nr-app-addons .addonButtons{text-align:right;white-space:nowrap}.nr-app-addons .addonButtons .btn{overflow:hidden;text-overflow:ellipsis;margin:0 3px;text-decoration:none}.nr-app-addons .addonImg{-webkit-box-sizing:border-box;box-sizing:border-box;text-align:center;width:70px}.nr-app-addons .addonImg img{width:100%}#wrapper .nr-app-addons{margin-left:0}.nr-main-content{padding:25px}.nr-main-header{margin:-1px !important}.nr-main-header h2{margin-top:0;margin-bottom:5px;font-size:1.5em}.nr-main-header p{margin:0}.nr-box{position:relative}.nr-box .nr-box-title{font-weight:bold;margin-bottom:12.5px;padding-bottom:12.5px;border-bottom:solid 1px #eaeaea}.nr-box .nr-box-title div{font-weight:normal;font-size:12px;padding-top:2px;opacity:.7}.nr-box .nr-box-title a{color:inherit;font-weight:bold}@media screen and (min-width: 769px), print{.nr-box.nr-box-hr{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.nr-box.nr-box-hr>*{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-ms-flex-negative:1;flex-shrink:1}.nr-box.nr-box-hr .nr-box-title{margin:0;padding:0;border:none}}.nr-box p:last-child{margin:0}.nr-box.no-padd{padding:0 !important}.nr-row{margin:0 -20px;display:-webkit-box;display:-ms-flexbox;display:flex}.nr-row>*{width:100%}table.nrTable{margin:0;width:100%}table.nrTable .text-center{text-align:center}table.nrTable .text-right{text-align:right}table.nrTable th{color:#888}table.nrTable td,table.nrTable th{vertical-align:middle;border-color:#eaeaea;padding:10px}table.nrTable tbody tr.error>td{background-color:#f9f0f0}table.nrTable.noBorder td,table.nrTable.noBorder th{padding:7px 0;border:none}.nr-app-form{margin:0}.nr-app-form .pagination{margin-top:25px}.nr-app-config{margin:-15px;margin-bottom:0}.nr-app-config table{margin-top:-10px}.nr-app-config table tr:first-child td{border-top:none}.nr-app-config .nav-tabs,.nr-app-config joomla-tab>div[role=tablist]{margin:-25px !important;margin-bottom:25px !important}.nr-app-config .form-horizontal .control-label{white-space:nowrap}joomla-tab-element{background:none !important;padding:0 !important}.well.nr-well h4{margin-bottom:5px;font-size:1.2em}.well.nr-well .well-desc{opacity:.7;border-bottom:solid 1px #eaeaea;padding-bottom:20px;margin-bottom:25px;font-size:.9em}.well.nr-well:last-child{margin-bottom:0}#tabTabs,joomla-tab>div[role=tablist]{background-color:#fff;border:solid 1px #dedbdb;border-left:none;border-right:none;-webkit-box-shadow:none;box-shadow:none;width:auto !important}#tabTabs li,joomla-tab>div[role=tablist] li{margin:0;width:auto}#tabTabs .active a,joomla-tab>div[role=tablist] .active a{border-bottom-color:green}#tabTabs a,#tabTabs>button[role=tab],joomla-tab>div[role=tablist] a,joomla-tab>div[role=tablist]>button[role=tab]{outline:none;color:inherit;border:none;border-right:solid 1px #dedbdb !important;border-bottom:solid 2px transparent;border-radius:0;margin:0;padding:1.35em 1.5em;line-height:1;-webkit-box-shadow:none;box-shadow:none}#tabTabs a:hover,#tabTabs>button[role=tab]:hover,joomla-tab>div[role=tablist] a:hover,joomla-tab>div[role=tablist]>button[role=tab]:hover{background:none}#tabTabs>button[role=tab],joomla-tab>div[role=tablist]>button[role=tab]{border-bottom:0}#tabTabs>button[role=tab]:hover:after,#tabTabs>button[role=tab]:after,joomla-tab>div[role=tablist]>button[role=tab]:hover:after,joomla-tab>div[role=tablist]>button[role=tab]:after{height:2px !important;background:transparent}#tabTabs>button[role=tab][aria-expanded=true],joomla-tab>div[role=tablist]>button[role=tab][aria-expanded=true]{background:none;font-weight:normal}#tabTabs>button[role=tab][aria-expanded=true]:after,joomla-tab>div[role=tablist]>button[role=tab][aria-expanded=true]:after{background:green}#tabTabs a.active,joomla-tab>div[role=tablist] a.active{background:none;border-bottom-color:green}.view-config .nav-tabs+.tab-content{padding:0;background:none;border:none;-webkit-box-shadow:none;box-shadow:none}body .container-main{padding-bottom:0}.nr-app form{margin:0}.nr-app form .choices{min-width:270px;max-width:320px}.nr-app form .choices[data-type="multiple"] .choices__inner{padding:.1rem .5rem;min-height:auto}.nr-app form .choices[data-type="multiple"] .choices__item{font-size:13px}.nr-app form .choices[data-type="multiple"] .choices__input{margin-left:6px;line-height:1.6}.nr-app form .choices[data-type="select-one"] .choices__item button{display:none}.nr-app form .choices .choices__group .choices__heading{font-size:16px;color:#333}.nr-app form .choices .choices__group .choices__heading:empty{display:none}.nr-app form .choices.is-open .choices__list--dropdown{-webkit-box-shadow:0 3px 0 0.2rem #eaeaea;box-shadow:0 3px 0 0.2rem #eaeaea;border-bottom-color:#39f;border-left-color:#39f;border-right-color:#39f}.nr-app form .choices .choices__input{font-size:14px !important}.nr-app form .choices .choices__input::-webkit-input-placeholder{opacity:.5}.nr-app form .choices .choices__input::-moz-placeholder{opacity:.5}.nr-app form .choices .choices__input:-ms-input-placeholder{opacity:.5}.nr-app form .choices .choices__input::-ms-input-placeholder{opacity:.5}.nr-app form .choices .choices__input::placeholder{opacity:.5}body{overflow-x:hidden}body,body .container-popup{padding-top:0;padding-bottom:0;background-color:#f0f0f0}a.disabled{pointer-events:none}.boxColor{position:relative;width:20px;height:20px;background:url(../../../media/jui/img/jquery.minicolors.png) -80px 0;border:solid 1px #ccc;display:block;border-radius:3px;overflow:hidden}.boxColor span{display:block;width:100%;height:100%;-webkit-box-shadow:inset 0 9px 0 rgba(255,255,255,0.1);box-shadow:inset 0 9px 0 rgba(255,255,255,0.1)}.nr-number{font-size:38px;line-height:1}.button{text-decoration:none !important}#jform_override label[for='jform_override0']{background:#f3f3f3 !important;color:inherit !important}.subform-repeatable table{margin-bottom:0}.subform-repeatable table .subform-repeatable-group .controls{padding-right:0 !important}.subform-repeatable .btn-toolbar .btn-group a{margin-left:0}.subform.openinghours .subform-child{margin-left:180px}.subform.openinghours .subform-child .control-group+.control-group{margin-top:10px !important}.subform.openinghours .subform-child .control-group:first-child .control-label{display:none}.subform.openinghours .subform-child .control-group.field-spacer{padding-bottom:0 !important;border-bottom:none !important}.subform.openinghours .subform-child .control-group.field-spacer .control-label{padding-right:0 !important}.subform.openinghours .subform-child .control-group .nrtoggle input{position:absolute}.subform .movie-duration-item{margin-bottom:10px}.subform{display:-webkit-box;display:-ms-flexbox;display:flex}.subform .controls{margin:0}.subform .control-group{margin:0 !important}.subform .subform-child{margin-left:10px;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;display:-webkit-box;display:-ms-flexbox;display:flex;gap:10px}.subform .subform-child>.control-group{-webkit-box-flex:1;-ms-flex:1;flex:1}.subform .subform-child>.control-group>.control-label{display:none}.subform .subform-child .controls{margin:0}.subform .subform-child .controls>*{max-width:100% !important;width:100%}.subform .subform-repeatable-wrapper td{vertical-align:middle}.subform .subform-repeatable-wrapper input{-webkit-box-sizing:border-box;box-sizing:border-box;height:auto}.subform-fullwidth input{width:100%;max-width:100%}.gsdtt{display:-webkit-box;display:-ms-flexbox;display:flex;width:100%}.gsdtt #url{-webkit-box-flex:1;-ms-flex:1;flex:1;background-color:#eaeaea;border:1px solid #ccc;padding:8px 15px;border-radius:0;margin:0;margin-right:10px}.gsdtt input,.gsdtt button{height:35px;line-height:1}.gsdtt button{min-width:100px}.nr-app{color:#4a4a4a;font-size:14px}.nr-app label{font-size:1em;color:inherit}.mod .mod-head{font-weight:bold;margin-bottom:5px}.mod:not(:last-child){border-bottom:solid 1px #eaeaea;padding-bottom:25px;margin-bottom:25px}.field-spacer{border-bottom:solid 1px #efeff0;margin-top:35px;padding-bottom:7px;margin-bottom:15px !important}.field-spacer label{margin:0;cursor:default;font-weight:bold}.subform.hide-custom-repeater-label>.subform-child>.control-group>.control-label{display:none}.subform-fullwidth input,.howToRepeater .form-control{width:100% !important;max-width:100% !important}.howToRepeater th{width:25% !important}.howToRepeater td,.howToRepeater th{padding-left:0;padding-right:30px}.howToRepeater .control-group{margin:0}


js/addons.js000064400000001047152344346010006772 0ustar00document.addEventListener("DOMContentLoaded",function(){document.addEventListener("click",function(t){var e=t.target.closest(".pluginState");if(e){t.preventDefault();var n=e.closest(".nr-app-addons"),s=e.closest("tr").dataset.id,i=e.querySelector("span"),a=i.classList.contains("icon-publish")?0:1;fetch(n.dataset.base+"index.php?option=com_gsd&view=gsd&format=raw&do=pluginState&plugin_id="+s+"&state="+a).then(function(t){return t.text()}).then(function(t){i.removeAttribute("class"),i.classList.add("1"==t?"icon-publish":"icon-unpublish")})}})});

js/fastedit.js000064400000003506152344346010007327 0ustar00var GSD_Fast_Edit=function(){function t(){this.el=document.querySelector(".gsdFastEdit"),this.options=Joomla.getOptions("gsd_fastedit"),this.baseURL=this.options.base_url+"/index.php?option=com_gsd&view=gsd&format=raw",this.thing=this.options.thing_id,this.plugin=this.options.plugin,this.modal=this.el.querySelector(".modal"),this.init()}var i,e=t.prototype;function n(t,e){return i.apply(this,arguments)}return e.init=function(){this.thing&&this.get(),this.initEvents()},e.initEvents=function(){document.addEventListener("click",function(t){this.handleModalOpen(t),this.removeItem(t)}.bind(this))},e.handleModalOpen=function(t){var e=t.target.closest('a[href="#gsdModal"]');if(e){var i=!e.classList.contains("btn-secondary"),n=i?Joomla.JText._("GSD_ADD_SNIPPET"):Joomla.JText._("GSD_EDIT_SNIPPET");if(this.modal.querySelector(".modal-header h3").innerHTML=n,i)var s=e.closest(".gsdFastEdit").dataset.src+"&contenttype="+e.dataset.contenttype;else s=e.dataset.src;this.modal.querySelector("iframe").src=s,this.modal.querySelector("iframe").addEventListener("load",function(){this.get()}.bind(this))}},e.removeItem=function(t){var e=t.target.closest(".gsdRemove");e&&(t.preventDefault(),confirm(Joomla.JText._("NR_ARE_YOU_SURE"))&&this._remove(e.closest("tr").dataset.pk))},e.get=function(){var e=this;this.fetch("&plugin="+this.plugin+"&thing="+this.thing+"&assignment="+this.options.plugin_assignment_name,function(t){e.el.querySelector(".items").innerHTML=t})},e._remove=function(t){var e=this;this.fetch("&do=delete&pk="+t,function(){e.get()})},e.fetch=(i=function(t,e){var i=this;this.el.classList.add("working"),fetch(this.baseURL+t).then(function(t){return t.text()}).then(function(t){i.el.classList.remove("working"),e(t)})},n.toString=function(){return i.toString()},n),t}();document.addEventListener("DOMContentLoaded",function(){new GSD_Fast_Edit});

js/openinghours.js000064400000001625152344346010010244 0ustar00document.addEventListener("DOMContentLoaded",function(){document.addEventListener("click",function(e){var s=e.target;if(s.closest(".gsd-oh-add-btn"))s.closest(".gsd-oh-row").querySelector(".oh-picker.first").classList.add("is-visible"),s.closest(".gsd-oh-row").querySelector(".gsd-oh-add-btn").classList.add("is-hidden"),(t=s.closest(".gsd-oh-row").querySelector(".gsd-oh-more-btn")).classList.remove("is-hidden"),e.preventDefault();else if(s.closest(".gsd-oh-more-btn")){(t=s.closest(".gsd-oh-more-btn")).classList.add("is-hidden"),t.nextElementSibling.classList.toggle("is-visible"),e.preventDefault()}else if(s.closest(".gsd-oh-remove-btn")){var t;(t=s.closest(".gsd-oh-row").querySelector(".gsd-oh-more-btn")).classList.remove("is-hidden"),s.closest(".oh-more").classList.toggle("is-visible"),s.closest(".oh-more").querySelectorAll('input[type="text"]').forEach(function(e){e.value=""}),e.preventDefault()}})});

js/script.js000064400000000222152344346010007020 0ustar00jQuery(function(n){n("#content").length&&(h=n(window).height()-n("#content").position().top+10,n(".nr-main-container").css({"min-height":h}))});

GSD/Helper/JReviews.php000064400000001554152345574300010664 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

namespace GSD\Helper;

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\Factory;

class JReviews
{
    public static function getListing($id)
    {
        try {
            $db = Factory::getDbo();

            $query = $db->getQuery(true)
                ->select('*')
                ->from('#__jreviews_content')
                ->where('contentid = ' . $db->q($id));
    
            $db->setQuery($query);
    
            return $db->loadAssoc();
        } catch (\Exception $exception) {}

        return null;
    }
}GSD/Schemas/Schemas/Article.php000064400000001631152345574300012234 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2022 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace GSD\Schemas\Schemas;

// No direct access
defined('_JEXEC') or die;

use GSD\Helper;

class Article extends \GSD\Schemas\Base
{
    /**
     * Return all the schema properties
     *
     * @return void
     */
    protected function initProps()
    {
        $props = [
            'publisherName' => $this->data->get('publisher_name', Helper::getSiteName()),
            'publisherLogo' => Helper::cleanImage(Helper::absURL($this->data->get('publisher_logo', Helper::getSiteLogo())))
        ];

        $this->data->loadArray($props);

        parent::initProps();
    }
}GSD/Schemas/Schemas/Book.php000064400000000706152345574300011545 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2022 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace GSD\Schemas\Schemas;

// No direct access
defined('_JEXEC') or die;

class Book extends \GSD\Schemas\Base
{}GSD/Schemas/Schemas/Course.php000064400000004401152345574300012107 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2022 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace GSD\Schemas\Schemas;

// No direct access
defined('_JEXEC') or die;

use GSD\Helper;
use Joomla\String\StringHelper;
class Course extends \GSD\Schemas\Base
{
    /**
     * A key => value array with schema properties that needs to be renamed.
     * 
     * The left value represents the name of the property as defined in the schema's XML file.
     * The right value represents the name of the property as it's expected in JSON class.
     *  
     * @Todo - We should rename all properties directly in each schema XML file and then get rid of this property.
     * 
     * @var array
     */
    protected $rename_properties = [
        'country'     => 'addressCountry',
        'address'     => 'streetAddress',
        'locality'    => 'addressLocality',
        'region'      => 'addressRegion',
        'postal_code' => 'postalCode',
        'start_date'  => 'startDate',
        'end_date'    => 'endDate'
    ];

    /**
     * Return all the schema properties
     *
     * @return void
     */
    protected function initProps()
    {
        $props = [
            'courseWorkload' => Helper::convert_to_ISO8601($this->data->get('courseWorkload'), 'H'),
            'validFrom' => Helper::date($this->data->get('validFrom'), true),
            'start_date' => Helper::date($this->data->get('start_date'), true),
            'end_date'   => Helper::date($this->data->get('end_date'), true),
        ];

        $this->data->loadArray($props);

        parent::initProps();
    }

    /**
     * Beyond the default housekeeping, limit the characters in the headline property to 110 in order to comply with Google's guidelines.
     * 
     * Reference: https://developers.google.com/search/docs/appearance/structured-data/course
     *
     * @return void
     */
    protected function cleanProps()
    {
        parent::cleanProps();

        $this->data->set('description', StringHelper::substr($this->data->get('description'), 0, 500));
    }
}GSD/Schemas/Schemas/Custom_Code.php000064400000004240152345574300013054 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2022 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace GSD\Schemas\Schemas;

// No direct access
defined('_JEXEC') or die;

use Joomla\Registry\Registry;

class Custom_Code extends \GSD\Schemas\Base
{
    /**
     * Return all the schema properties
     *
     * @return void
     */
    protected function initProps()
    {
        parent::initProps();

        // Since v5.3.1, the SchemaCleaner supports removing structured data also from the <head> that does not have the data-type="gsd" property.
        // In order to prevent the user defined custom code from being removed, we need to add the data-type property to every custom JSON+LD script.  
        $safe_custom_code = str_replace('<script type="application', '<script data-type="gsd" type="application', $this->data->get('custom_code'));
        $this->data->set('custom_code', $safe_custom_code);
    }

    /**
     * Since in the Custom Code we do not have a real array but a string, the result is not passed into the cleanProps() method, thus, we may end up
     * with unescaped characters and HTML that can break the structured data. With this override, we filter all payload props before they get replaced in the snippet.
     * 
     * Consider this as a temporary workaround. In addition, it's worth consideration to filter payload props on all Schemas by default, so we don't need to do so later.
     *
     * @param   object  $payload
     * 
     * @return  void
     */
    public function onPayloadPrepare(&$payload)
    {
        $props = $payload->toArray();

        array_walk_recursive($props, function(&$prop)
        {
            if (!is_null($prop)) // Make PHP 8.1 happy.
            {
                $this->cleanProp($prop);
            }
        });

        $payload = new Registry($props);
    }

    /**
     * Do not clean custom script
     *
     * @return void
     */
    protected function cleanProps()
    {
    }
}GSD/Schemas/Schemas/Event.php000064400000004365152345574300011741 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2022 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace GSD\Schemas\Schemas;

// No direct access
defined('_JEXEC') or die;

use GSD\Helper;

class Event extends \GSD\Schemas\Base
{
    /**
     * A key => value array with schema properties that needs to be renamed.
     * 
     * The left value represents the name of the property as defined in the schema's XML file.
     * The right value represents the name of the property as it's expected in JSON class.
     *  
     * @Todo - We should rename all properties directly in each schema XML file and then get rid of this property.
     * 
     * @var array
     */
    protected $rename_properties = [
        'locationAddress' => 'streetAddress'
    ];

    /**
     * Return all the schema properties
     *
     * @return void
     */
    protected function initProps()
    {
        $props = [
            'startDate'     => Helper::date($this->data['startDate'], true),
            'endDate'       => Helper::date($this->data['endDate'], true),
            'startDateTime' => Helper::date($this->data['offerStartDate'], true),
            'price'         => $this->getPrice()
        ];

        $this->data->loadArray($props);

        parent::initProps();
    }

    /**
     * Detect price range or single price. 
     *
     * @return mixed
     */
    private function getPrice()
    {
        $price = $this->data->get('offerPrice');

        // The offerPrice should not be included in the structured data only when it's disabled. The price of '0.00' should be still displayed in the structured data.
        if ($price === false)
        {
            return;
        }

        if (is_scalar($price) && strpos($price, '-') !== false)
        {
            $price = explode('-', $price, 2);
        }

        if (is_array($price))
        {
            return [
                Helper::formatPrice($price[0]), 
                Helper::formatPrice($price[1])
            ];
        }

        return Helper::formatPrice($price);
    }
}GSD/Schemas/Schemas/FAQ.php000064400000012335152345574300011263 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2022 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace GSD\Schemas\Schemas;

// No direct access
defined('_JEXEC') or die;

use GSD\Helper;
use NRFramework\DOMCrawler;

class FAQ extends \GSD\Schemas\Base
{
    /**
     * Return all the schema properties
     *
     * @return void
     */
    protected function initProps()
    {
        $mode = $this->data->get('mode', 'auto');

        $faq = $this->data['faq_repeater_fields'];
        
        $allowed_tags = '<h1><h2><h3><h4><h5><h6><br><ol><ul><li><p><a><div><b><strong><i><em>';
        
        $faqData = [];

        switch ($mode)
        {
            // Manual Mode
            case 'manual':
                foreach ($faq as $item)
                {
                    $question = trim($item->question);
                    $question = preg_replace('/\s\s+/', ' ', $question);
                    $question = strip_tags($question);

                    $answer = trim($item->answer);
                    $answer = strip_tags($answer, $allowed_tags);

                    $faqData[] = [
                        'question' => $question,
                        'answer'   => $answer
                    ];
                }
                break;

            // Auto Mode
            case 'auto':
                $question_selector = $this->data->get('question_selector', '.question');
                $answer_selector = $this->data->get('answer_selector', '.answer');

                // Find questions and answersr
                $questions = $this->crawl($question_selector);
                $answers = $this->crawl($answer_selector);

                // Combine the Q&A
                if (count($questions) && count($answers))
                {
                    $counter = 0;
                    foreach ($questions as $q)
                    {
                        $question = trim($q['value']);

                        $answer = isset($answers[$counter]['html']) ? $answers[$counter]['html'] : '';

                        // Remove spaces, new lines, invalid HTML tags and empty paragraphs.
                        $answer = preg_replace('/\s\s+/', ' ', $answer);
                        $answer = strip_tags($answer, $allowed_tags);
                        $answer = preg_replace('/<p>\s*<\/p>/', '', $answer);
                        $answer = trim($answer);

                        $faqData[] = [
                            'question' => $question,
                            'answer' => $answer
                        ];

                        $counter++;
                    }
                } else 
                {
                    Helper::log([
                        'Error'             => 'No FAQs found',
                        'Question Selector' => $question_selector,
                        'Questions Found'   => count($questions),
                        'Answer Selector'   => $answer_selector,
                        'Answers Found'     => count($answers)
                    ]);
                }
        }
        
        $this->data->set('faqs', $faqData);

        parent::initProps();
    }

    /**
	 * Find the FAQ content using XPath based on the provided selector
	 * 
	 * @param  string  $content   The content to search
	 * @param  string  $selector  The selector used for the search
	 * 
	 * @return array
	 */
	private function crawl($selector)
	{
		$data = [];
		$crawler = new DOMCrawler();

		if ($nodes = $crawler->filter($selector)->nodes)
		{
			foreach ($nodes as $node)
			{
				// Remove attributes from node
				$html = $this->removeAttributesFromNode($node);

				$data[] = [
					'html' => $html,
					'value' => $node->nodeValue
                ];
			}
		}
		return $data;
	}

    /**
	 * Loop through all elements in the node that have an attribute
	 * and remove them
	 * 
	 * @param   DOMElement  $domNode
	 * 
	 * @return  string
	 */
	private function removeAttributesFromNode($domNode)
	{
		// Loop through all elements that have an attribute and remove it
		$dom = new \DOMDocument;
		$content = $domNode->ownerDocument->saveHTML($domNode);
		$dom->loadHTML(self::stringToUTF8($content));
		$xpath = new \DOMXPath($dom);
		$nodes = $xpath->query('//@*');
		foreach ($nodes as $node)
		{
			// Skip the href attribute which is allowed
			if ($node->nodeName == 'href')
			{
				// Fix relative URLs.
				$url = Helper::absURL($node->nodeValue);
				$node->parentNode->setAttribute('href', $url);
				continue;
			}

			$node->parentNode->removeAttribute($node->nodeName);
		}
		
		// return inner child only
		$html = '';
		foreach($dom->getElementsByTagName('body')->item(0)->firstChild->childNodes as $node) {
			$html .= $dom->saveHTML($node);
		}

		return $html;
	}

    /**
	 * Convert a string to UTF8 encoding
	 * 
	 * @param  string
	 * 
	 * @return string
	 */
	private function stringToUTF8($string)
	{
		if (!function_exists('mb_convert_encoding'))
		{
			return $string;
		}

		return mb_encode_numericentity(iconv('UTF-8', 'UTF-8', $string), [0x80, 0x10FFFF, 0, 0x1FFFFF], 'UTF-8');
	}
}GSD/Schemas/Schemas/FactCheck.php000064400000004375152345574300012474 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2022 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace GSD\Schemas\Schemas;

// No direct access
defined('_JEXEC') or die;

use GSD\Helper;
use Joomla\String\StringHelper;

class FactCheck extends \GSD\Schemas\Base
{
    /**
     * Return all the schema properties
     *
     * @return void
     */
    protected function initProps()
    {
        parent::initProps();

        switch ($this->data['factcheckRating'])
        {
            // there is no textual representation for zero (0)
            case '1':
                $textRating = 'False';
                break;

            case '2':
                $textRating = 'Mostly false';
                break;

            case '3':
                $textRating = 'Half true';
                break;

            case '4':
                $textRating = 'Mostly true';
                break;

            case '5':
                $textRating = 'True';
                break;

            default:
                $textRating = 'Hard to categorize';
        }

        $props = [
            'claimDatePublished'   => Helper::date($this->data['claimDatePublished'], true),
            'factcheckURL'         => $this->data['multiple'] ? $this->data['url'] . $this->data['anchorName'] : $this->data['url'],
            'bestFactcheckRating'  => $this->data['factcheckRating'] != '-1' ? '5' : '-1',
            'worstFactcheckRating' => $this->data['factcheckRating'] != '-1' ? '1' : '-1',
            'alternateName'        => $textRating
        ];

        $this->data->loadArray($props);
    }

    /**
     * Beyond the default housekeeping, limit the characters in the headline property to 110 in order to comply with Google's guidelines.
     *
     * Reference: https://developers.google.com/search/docs/appearance/structured-data/factcheck
     * 
     * @return void
     */
    protected function cleanProps()
    {
        parent::cleanProps();

        $this->data->set('title', StringHelper::substr($this->data->get('title'), 0, 75));
    }
}GSD/Schemas/Schemas/HowTo.php000064400000005607152345574300011720 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2023 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace GSD\Schemas\Schemas;

// No direct access
defined('_JEXEC') or die;

use GSD\Helper;
use GSD\Schemas\Base;
use NRFramework\DOMCrawler;

class HowTo extends Base
{
    /**
     * The HTML tags allowed to be used in certain schema properties, such as the headline and the description.
     *
     * @var mixed
     */
    protected $allowed_HTML_tags = '<p><br><ul><li><strong><em><b>';

    /**
     * Return all the schema properties
     *
     * @return void
     */
    protected function initProps()
    {
        $this->data->set('totalTime', $this->data['totalTime'] ? 'PT' . $this->data['totalTime'] . 'M' : null);
        
        $steps = [];

        switch ($this->data->get('mode', 'auto'))
        {
            case 'manual':
                $steps = array_values(json_decode(json_encode($this->data['howto_repeater']), true));
                break;

            // Auto Mode
            case 'auto':
                $crawler = new DOMCrawler();
                
                $names  = $crawler->readCSSSelectorField($this->data->get('name_selector'), false);
                $texts  = $crawler->readCSSSelectorField($this->data->get('text_selector'), false);
                $images = $crawler->readCSSSelectorField($this->data->get('image_selector'), false);
                $urls   = $crawler->readCSSSelectorField($this->data->get('url_selector'), false);

                $steps = array_map(function($name, $text, $image, $url)
                {
                    return [
                        'name'  => $name,
                        'text'  => $text,
                        'image' => $image,
                        'url'   => $url
                    ];
                }, $names, $texts, $images, $urls);
        }

        // Prepare steps
        $steps = array_map(function($step)
        {
            return [
                'name'  => isset($step['name']) ? $step['name'] : '',
                'text'  => isset($step['text']) ? $step['text'] : '',
                'image' => isset($step['image']) ? Helper::absURL($step['image']) : '',
                'url'   => isset($step['url']) ? Helper::absURL($step['url']) : ''
            ];
        }, $steps);

        $this->data->set('step', $steps);

        parent::initProps();
    }

    /**
	 * Convert a string to UTF8 encoding
	 * 
	 * @param  string
	 * 
	 * @return string
	 */
	private function stringToUTF8($string)
	{
		if (!function_exists('mb_convert_encoding'))
		{
			return $string;
		}

		return mb_encode_numericentity(iconv('UTF-8', 'UTF-8', $string), [0x80, 0x10FFFF, 0, 0x1FFFFF], 'UTF-8');
	}
}GSD/Schemas/Schemas/JobPosting.php000064400000003620152345574300012727 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2022 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace GSD\Schemas\Schemas;

// No direct access
defined('_JEXEC') or die;

use GSD\Helper;

class JobPosting extends \GSD\Schemas\Base
{
    /**
     * A key => value array with schema properties that needs to be renamed.
     * 
     * The left value represents the name of the property as defined in the schema's XML file.
     * The right value represents the name of the property as it's expected in JSON class.
     *  
     * @Todo - We should rename all properties directly in each schema XML file and then get rid of this property.
     * 
     * @var array
     */
    protected $rename_properties = [
        'locality'    => 'addressLocality',
        'region'      => 'addressRegion',
        'postal_code' => 'postalCode'
    ];

    /**
     * The HTML tags allowed to be used in certain schema properties, such as the headline and the description.
     *
     * @var mixed
     */
    protected $allowed_HTML_tags = '<p><br><ul><li>';

    /**
     * Return all the schema properties
     *
     * @return void
     */
    protected function initProps()
    {
        $props = [
            'hiring_organization_logo' => Helper::cleanImage(Helper::absURL($this->data['hiring_organization_logo'])),
            'valid_through'            => Helper::date($this->data['valid_through'], true),
            'salary'                   => $this->data['salary'] ? (strpos($this->data['salary'], '-') === false ? Helper::formatPrice($this->data['salary']) : explode('-', $this->data['salary'])) : '',
        ];

        $this->data->loadArray($props);

        parent::initProps();
    }
}GSD/Schemas/Schemas/LocalBusiness.php000064400000001636152345574300013424 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2022 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace GSD\Schemas\Schemas;

// No direct access
defined('_JEXEC') or die;

use GSD\Helper;

class LocalBusiness extends \GSD\Schemas\Base
{
    /**
     * Return all the schema properties
     *
     * @return void
     */
    protected function initProps()
    {
        $props = [
            'name'   => $this->data->get('name', Helper::getSiteName()),
            'geo'    => array_map('trim', explode(',', $this->data->get('geo', ''), 2)),
            'review' => $this->data->get('reviews')
        ];

        $this->data->loadArray($props);

        parent::initProps();
    }
}GSD/Schemas/Schemas/Movie.php000064400000003105152345574300011726 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2022 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace GSD\Schemas\Schemas;

// No direct access
defined('_JEXEC') or die;

use GSD\Helper;

class Movie extends \GSD\Schemas\Base
{
    /**
     * Return all the schema properties
     *
     * @return void
     */
    protected function initProps()
    {
        $props = [
            'genre'     => $this->readRepeatableValue('genre'),
            'creators'  => $this->readRepeatableValue('creators'),
            'directors' => $this->readRepeatableValue('directors'),
            'actors'    => $this->readRepeatableValue('actors'),
            'duration'  => !empty($this->data['duration']) ? 'PT' . $this->data['duration'] . 'M' : null,
            'review'    => $this->data['reviews'],
        ];

        $this->data->loadArray($props);

        parent::initProps();
    }

    private function readRepeatableValue($prop)
    {
        $items = $this->data->get($prop, '');
        $found = [];

        if (!empty($items) && is_string($items))
        {
            $items = explode(',', $items);

            foreach ($items as $item)
            {
                $found[] = (object) [
                    'name' => $item
                ];
            }
        } else 
        {
            $found = $items;
        }

        return $found;
    }
}GSD/Schemas/Schemas/Person.php000064400000000711152345574300012115 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2022 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace GSD\Schemas\Schemas;

// No direct access
defined('_JEXEC') or die;

class Person extends \GSD\Schemas\Base
{
}GSD/Schemas/Schemas/Product.php000064400000004067152345574300012277 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2022 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace GSD\Schemas\Schemas;

// No direct access
defined('_JEXEC') or die;

use GSD\Helper;
use NRFramework\Functions;

class Product extends \GSD\Schemas\Base
{
    /**
     * Return all the schema properties
     *
     * @return void
     */
    protected function initProps()
    {
        $props = [
            'offerPrice'      => $this->getPrice(),
            'priceValidUntil' => Helper::date($this->data->get('priceValidUntil', '2100-12-31T10:00:00')),
            'weight'          => $this->data->get('weight'),
            'weightUnit'      => $this->data->get('weight_unit'),
            'brand'           => $this->data->get('brand', Helper::getSiteName()),
            'gtin'            => $this->data->get('gtin'),

            // Fallback to 'sku' property to prevent structured data warning. 
            'mpn'             => $this->data->get('mpn', $this->data->get('sku')),
        ];

        $this->data->loadArray($props);

        parent::initProps();
    }

    /**
     * Detect price range or single price. 
     *
     * @return mixed
     */
    private function getPrice()
    {
        $price = $this->data->get('offerPrice');

        // The offerPrice should not be included in the structured data only when it's disabled. The price of '0.00' should be still displayed in the structured data.
        if ($price === false)
        {
            return;
        }

        if (is_scalar($price) && strpos($price, '-') !== false)
        {
            $price = explode('-', $price, 2);
        }

        if (is_array($price))
        {
            return [
                Helper::formatPrice($price[0]), 
                Helper::formatPrice($price[1])
            ];
        }

        return Helper::formatPrice($price);
    }
}GSD/Schemas/Schemas/Recipe.php000064400000002375152345574300012066 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2022 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace GSD\Schemas\Schemas;

// No direct access
defined('_JEXEC') or die;

use GSD\Helper;

class Recipe extends \GSD\Schemas\Base
{
    /**
     * Return all the schema properties
     *
     * @return void
     */
    protected function initProps()
    {
        $props = [
            'prepTime'     => $this->data['prepTime'] ? 'PT' . $this->data['prepTime'] . 'M' : null,
            'cookTime'     => $this->data['cookTime'] ? 'PT' . $this->data['cookTime'] . 'M' : null,
            'totalTime'    => $this->data['totalTime'] ? 'PT' . $this->data['totalTime'] . 'M' : null,
            'ingredient'   => Helper::makeArrayFromNewLine(strip_tags($this->data['ingredient'] ? $this->data['ingredient'] : '')),
            'instructions' => Helper::makeArrayFromNewLine(strip_tags($this->data['instructions'] ? $this->data['instructions'] : ''))
        ];

        $this->data->loadArray($props);

        parent::initProps();
    }
}GSD/Schemas/Schemas/Review.php000064400000007050152345574300012113 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2022 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace GSD\Schemas\Schemas;

// No direct access
defined('_JEXEC') or die;

use GSD\Helper;
use NRFramework\Functions;
use Joomla\CMS\Factory;

class Review extends \GSD\Schemas\Base
{
    /**
     * A key => value array with schema properties that needs to be renamed.
     * 
     * The left value represents the name of the property as defined in the schema's XML file.
     * The right value represents the name of the property as it's expected in JSON class.
     *  
     * @Todo - We should rename all properties directly in each schema XML file and then get rid of this property.
     * 
     * @var array
     */
    protected $rename_properties = [
        'address' => 'streetAddress'
    ];

    /**
     * Return all the schema properties
     *
     * @return void
     */
    protected function initProps()
    {
        // We need a better and more dynamic way to handle Repeatable Field values.
        // We can move this block to MappingOptions somehow.
        $actors = $this->data->get('actors', '');
        $actors_ = [];

        if (!empty($actors) && is_string($actors))
        {
            $actors = explode(',', $actors);

            foreach ($actors as $actor)
            {
                $actors_[] = (object)[
                    'name' => $actor
                ];
            }
        } else 
        {
            $actors_ = $actors;
        }

        $props = [
            'itemReviewedPublishedDate' => $this->data['item_reviewed_published_date'],
            'movie_director'   => $this->data['item_reviewed_movie_director'],
            'product_sku'      => $this->data['item_reviewed_product_sku'],
            'product_brand'    => $this->data['item_reviewed_product_brand'],
            'product_description' => $this->data['item_reviewed_product_description'],
            'currency'         => $this->data['item_reviewed_product_currency'],
            'condition'        => $this->data['item_reviewed_product_offeritemcondition'],
            'availability'     => $this->data['item_reviewed_product_offeravailability'],
            'offerprice'       => $this->data['item_reviewed_product_offerprice'],
            'pricevaliduntil'  => $this->data['item_reviewed_product_pricevaliduntil'],
            'book_author'      => $this->data['item_reviewed_book_author'],
            'book_author_url'  => $this->data['item_reviewed_book_author_url'],
            'book_isbn'        => $this->data['item_reviewed_book_isbn'],
            'review'           => $this->data['reviews'],
            'actors'           => $actors_,
            'language_code'    => explode('-', Factory::getLanguage()->getTag())[0]
        ];

        $this->data->loadArray($props);

        parent::initProps();
    }

    /**
     * This method runs everytime a structured data item is saved in the backend. 
     *
     * @param  array    $data   The data to be stored in the database
     * 
     * @return void
     */
    public function onSave(&$data)
    {
        parent::onSave($data);

        if ($data['item_reviewed_product_pricevaliduntil']['option'] == 'fixed')
        {
            $data['item_reviewed_product_pricevaliduntil']['fixed'] = Functions::dateToUTC($data['item_reviewed_product_pricevaliduntil']['fixed']);
        }
    }
}GSD/Schemas/Schemas/Service.php000064400000003641152345574300012254 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2022 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace GSD\Schemas\Schemas;

// No direct access
defined('_JEXEC') or die;

use Joomla\CMS\Uri\Uri;
use GSD\Helper;

class Service extends \GSD\Schemas\Base
{   
    /**
     * A key => value array with schema properties that needs to be renamed.
     * 
     * The left value represents the name of the property as defined in the schema's XML file.
     * The right value represents the name of the property as it's expected in JSON class.
     *  
     * @Todo - We should rename all properties directly in each schema XML file and then get rid of this property.
     * 
     * @var array
     */
    protected $rename_properties = [
        'provider_country'       => 'addressCountry',
        'provider_streetAddress' => 'streetAddress',
        'provider_city'          => 'addressLocality',
        'provider_addressRegion' => 'addressRegion',
        'provider_postalCode'    => 'postalCode'
    ];
    
    /**
     * The HTML tags allowed to be used in certain schema properties, such as the headline and the description.
     *
     * @var string
     */
    protected $allowed_HTML_tags = '<p><br><ul><li><h1><h2><h3><h4><h5><strong><em><b>';

    /**
     * Return all the schema properties
     *
     * @return void
     */
    protected function initProps()
    {
        $props = [
            'offerPrice'     => Helper::formatPrice($this->data['offerPrice']),
            'provider_image' => Helper::cleanImage($this->data['provider_image']),
            'phone'          => $this->data['provider_phone'],
        ];

        $this->data->loadArray($props);

        parent::initProps();
    }
}GSD/Schemas/Schemas/Video.php000064400000002664152345574300011726 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2022 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace GSD\Schemas\Schemas;

// No direct access
defined('_JEXEC') or die;

use GSD\Helper;

class Video extends \GSD\Schemas\Base
{
    /**
     * A key => value array with schema properties that needs to be renamed.
     * 
     * The left value represents the name of the property as defined in the schema's XML file.
     * The right value represents the name of the property as it's expected in JSON class.
     *  
     * @Todo - We should rename all properties directly in each schema XML file and then get rid of this property.
     * 
     * @var array
     */
    protected $rename_properties = [
        'headline'   => 'name',
        'image'      => 'thumbnailUrl',
        'publish_up' => 'uploadDate'
    ];

    /**
     * Return all the schema properties
     *
     * @return void
     */
    protected function initProps()
    {
        $props = [
            'publish_up' => Helper::date($this->data->get('publish_up'), true),
            'image' => Helper::cleanImage(Helper::absURL($this->data->get('image')))
        ];

        $this->data->loadArray($props);

        parent::initProps();
    }
}GSD/Schemas/Base.php000064400000024272152345574300010146 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2022 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace GSD\Schemas;

// No direct access
defined('_JEXEC') or die;

use GSD\Helper;
use Joomla\Registry\Registry;
use NRFramework\Functions;
use NRFramework\Cache;
use GSD\MappingOptions;
use Joomla\CMS\Uri\Uri;

class Base
{
    /**
     * The schema properties
     *
     * @var object
     */
    protected $data;

    /**
     * The HTML tags allowed to be used in certain schema properties, such as the headline and the description.
     *
     * @var mixed
     */
    protected $allowed_HTML_tags = null;

    /**
     * A key => value array with schema properties that needs to be renamed.
     * 
     * The left value represents the name of the property as defined in the schema's XML file.
     * The right value represents the name of the property as it's expected in JSON class.
     *  
     * @Todo - We should rename all properties directly in each schema XML file and then get rid of this property.
     * 
     * @var array
     */
    protected $rename_properties;

    /**
     * Class constructor
     *
     * @param Registry $data The schema properties
     */
    public function __construct($data = null)
    {
        $this->setData($data);
    }

    /**
     * Return all schema properties
     *
     * @return Registry
     */
    public function get()
    {
        $this->initProps();
        $this->cleanProps();

        return $this->data;
    }

    public function setData($data)
    {
        $this->data = $data;
        return $this;
    }

    /**
     * Run a housekeeping on each property. Remove unwanted HTML tags and whitespace and encode remaining HTML.
     *
     * @return void
     */
    protected function cleanProps()
    {   
        $props = $this->data->toArray();

        array_walk_recursive($props, function(&$prop)
        {
            if (!is_null($prop)) // Make PHP 8.1 happy.
            {
                $this->cleanProp($prop);
            }
        });

        $this->data = new Registry($props);
    }

    /**
     * Make text safe to be used in a JSON-LD script
     *
     * @param  text $prop   The text to clean
     * 
     * @return void
     */
    protected function cleanProp(&$prop)
    {
        // Remove all <script> tags and their content
        $prop = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $prop);

        // Remove invalid HTML tags
        $prop = strip_tags($prop, $this->allowed_HTML_tags);

        // Convert remaining HTML tags into HTML entities to prevent structured data errors.
        $prop = htmlspecialchars($prop, ENT_QUOTES, 'UTF-8');

        // Remove whitespace
        $prop = preg_replace('/(\s)+/s', ' ', $prop);

        // Remove whitespace from the beginning and end of the prop
        $prop = trim($prop);
    }
    
    /**
     * Prepare common schema properties.
     * 
     * - Rename properties
     * - Add timezone offset and format dates to ISO8601
     * - Strip HTML tags from certain properties
     * - Convert relative paths to absolute URLs
     *
     * @return void
     */
    protected function initProps()
    {
        $this->renameProperties();
        $this->fixMultivalueProperties();
        $this->fixPriceRangeProperties();

        // Fix dates in the Reviews property. Used in schemas: Product, Movie, Local Business
        if ($reviews = $this->data->get('reviews'))
        {
            foreach ($reviews as &$review)
            {
                if (!isset($review['datePublished']))
                {
                    continue;
                }
    
                // Convert date to ISO8601
                $review['datePublished'] = Helper::date($review['datePublished'], true);
            }
    
            $this->data->set('reviews', $reviews);
        }

        // Common properties
        $props = [
            'contentType'   => $this->getName(),
            // Make sure the @id property is unique, to prevent structured data awkwardly merged by the Google Structured Data Testing Tool
            'id'            => Uri::current() . '#' .  $this->getName() . $this->data['snippet_id'],
            'title'         => $this->data['headline'],
            'description'   => $this->data['description'],
            'image'         => Helper::cleanImage(Helper::absURL($this->data->get('image'))), 

            // Author / Publisher
            'authorType'   => 'Person',
            'authorName'   => $this->data['author'],
            'authorUrl'    => isset($this->data['authorUrl']) ? $this->data['authorUrl'] : Uri::current(),

            // Rating
            'ratingValue'   => $this->data['rating_value'],
            'reviewCount'   => $this->data['review_count'],
            'bestRating'    => $this->data['bestRating'],
            'worstRating'   => $this->data['worstRating'],

            // Dates
            'datePublished' => Helper::date($this->data['publish_up'], true),
            'dateCreated'   => Helper::date($this->data['created'], true),
            'dateModified'  => Helper::date($this->data['modified'], true),

            // Site based
            'url'           => Uri::current(),
            'siteurl'       => Helper::getSiteURL(),
            'sitename'      => Helper::getSiteName(),
        ];

        $this->data->merge(new Registry($props));
    }

    /**
     * Some schema properties are declared with the wrong name in Schema XML files. With this method, we attemp to rename those properties with the proper name expected by the JSON class.
     * 
     * @todo Rename all properties in XML files and create a migration script that will update users database. Then, we can get get rid of this method.
     *
     * @return void
     */
    private function renameProperties()
    {
        if (!$this->rename_properties)
        {
            return;
        }

        foreach ($this->rename_properties as $old_property_name => $new_property_name)
        {
            if (!isset($this->data[$old_property_name]))
            {
                continue;
            }

            $this->data[$new_property_name] = $this->data[$old_property_name];

            // Remove old property as we no longer need it.
            unset($this->data[$old_property_name]);
        }
    }

    /**
     * Return the name of this schema type
     *
     * @return string
     */
    private function getName()
    {
        $reflect = new \ReflectionClass($this);
        return strtolower($reflect->getShortName());
    }

    // Temporary workaround. See comments in the Custom_Code class.
    public function onPayloadPrepare(&$payload) {}

    /**
     * This method runs everytime a structured data item is saved in the backend. 
     *
     * @param  array    $data   The data to be stored in the database
     * 
     * @return void
     */
    public function onSave(&$data)
    {
        if (!$data)
        {
            return;
        }

        foreach ($data as $optionKey => &$optionValue)
        {
            $commonDateFieldNames = [
                'publish_up',
                'modified',
                'created',
                'valid_through',
                'validFrom',
                'priceValidUntil'
            ];

            // Skip certain field names
            if (in_array($optionKey, ['birthDate']))
            {
                continue;
            }

            // Find date fields by their name.
            if (strpos(strtolower($optionKey), 'date') === false && !in_array($optionKey, $commonDateFieldNames))
            {
                continue;
            }

            // Only when the mapping option is using the "Fixed Dates" option
            // The "Custom Option" is ignored as it may include a shortcode or some other formatted value.
            if ($optionValue['option'] !== 'fixed' || empty($optionValue['fixed']))
            {
                continue;
            }

            $optionValue['fixed'] = Functions::dateToUTC($optionValue['fixed']);
        }
    }

    /**
     * Finds all properties that accept multiple values per line and convert the string into an array.
     *
     * @return void
     */
    private function fixPriceRangeProperties()
    {
        foreach ($this->getXMLFields() as $key => $field)
        {
            if (!isset($field['real_type']) || $field['real_type'] !== 'pricerange')
            {
                continue;
            }

            if (!$currentValue = $this->data->get($key))
            {
                continue;
            }

            $newValue = explode('-', $currentValue, 2);

            if (count($newValue) == 1)
            {
                $newValue = [$currentValue, $currentValue];
            }

            $this->data->set($key, $newValue);
        }
    }

    /**
     * Finds all properties that accept multiple values per line and convert the string into an array.
     *
     * @return void
     */
    private function fixMultivalueProperties()
    {
        foreach ($this->getXMLFields() as $key => $field)
        {
            if (!isset($field['custom_value_multiple']))
            {
                continue;
            }

            $newValue = Helper::makeArrayFromNewLine($this->data->get($key));

            if (!$newValue || count($newValue) == 1)
            {
                continue;
            }

            $this->data->set($key, $newValue);
        }
    }

    /**
     * Returns a list of all schema properties declared in the XML file
     *
     * @return array
     */
    private function getXMLFields()
    {
        $hash = md5('xmlFields' . $this->getName());

        if (Cache::has($hash))
        {
            return Cache::get($hash);
        }

        $xmlItems = simplexml_load_file(JPATH_ADMINISTRATOR . '/components/com_gsd/models/forms/contenttypes/' . $this->getName() . '.xml');

        $fields = [];

        foreach ($xmlItems->fieldset->fields->field as $field)
        {
            $field = (array) $field;
            $field = $field["@attributes"];
            $fields[$field['name']] = $field;
        }

        return Cache::set($hash, $fields);
    }
}GSD/Schemas/Helper.php000064400000002743152345574300010512 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2022 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace GSD\Schemas;

// No direct access
defined('_JEXEC') or die;

use Joomla\Filesystem\Folder;

class Helper
{
    /**
     * Bootup a schema type class instance
     *
     * @param   string      $type   The name of the schema, eg: article, product
     * @param   Registry   $data   The schema properties
     * 
     * @return  object
     */
    public static function getInstance($type, $data = null)
    {
        $classPath = '\\GSD\\Schemas\\Schemas\\';
        $type = strtolower($type);

        // Try to find the class using the given name
        $className = $classPath . ucfirst($type);

        if (!class_exists($className))
        {
            // Try to find the class by searching all files in the file system
            $files = Folder::files(__DIR__ . '/Schemas');
    
            foreach ($files as $file)
            {
                $fileStripExt = str_replace('.php', '', $file);
    
                if (strtolower($fileStripExt) == $type)
                {
                    $className = $classPath . $fileStripExt;
                    break;
                }
            }
        }

        return new $className($data);
    }
}GSD/Apps.php000064400000002324152345574300006606 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

namespace GSD;

use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Language\Text;

defined('_JEXEC') or die('Restricted access');

class Apps
{
	public static function getApp($name, $data = null)
	{
		if (!$plugin = PluginHelper::getPlugin('gsd', $name))
		{
			throw new \RuntimeException(Text::sprintf('GSD_PLUGIN_NOT_FOUND', $name));
		}

		// On Joomla 4, use bootPlugin()
		if (defined('nrJ4'))
		{
			$app = Factory::getApplication()->bootPlugin($plugin->name, $plugin->type);

		} else 
		{
			// On Joomla 3, use the old classic way to boot up a plugin
			// TODO: Remove when J3 support is dropped
			$name = 'plg' . $plugin->type . $plugin->name;
	
			require_once JPATH_PLUGINS . '/gsd/' . $plugin->name . '/' . $plugin->name . '.php';
	
			$dispatcher = \JEventDispatcher::getInstance();

			$app = new $name($dispatcher, (array) $plugin);
		}

		return $app;
	}
}GSD/Helper.php000064400000036301152345574300007124 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace GSD;

defined('_JEXEC') or die('Restricted Access');

use GSD\Json;
use NRFramework\Cache;
use Joomla\Registry\Registry;
use Joomla\String\StringHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Multilanguage;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Date\Date;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\CMS\Table\Table;
use Joomla\CMS\Object\CMSObject;
use Joomla\CMS\Plugin\PluginHelper;

/**
 * 
 *  Google Structured Data Helper Class
 */
class Helper
{
	/**
	 *  Plugin Params
	 *
	 *  @var  Registry
	 */
	public static $params;

	/**
	 *  Log Messages
	 *
	 *  @var  array
	 */
	public static $log;

	/**
	 *  Get all available Content Types
	 *
	 *  @return  array
	 */
	public static function getContentTypes()
	{
        $json = new Json();
        return $json->getContentTypes();
	}

	/**
	 *  Returns an array with crumbs
	 *
	 *  @return  array
	 */
	public static function getCrumbs($hometext, $addhome = true)
	{
		$pathway = Factory::getApplication()->getPathway();
		$items   = $pathway->getPathWay();
		$menu    = Factory::getApplication()->getMenu();
		$lang    = Factory::getLanguage();
		$count   = count($items);

		if (!$count)
		{
			return false;
		}

		// We don't use $items here as it references JPathway properties directly
		$crumbs = [];

		for ($i = 0; $i < $count; $i++)
		{
			// Note: In some cases, the link in the last crumb (current page) is returned empty by Joomla. 
			// We don't want to skip over this crumb as it represents the crumb of the current page which needs to be included in the schema. 
			// Thus, we skip only null link properties, so the empty crumb is included in the list.
			if (is_null($items[$i]->link) || !$items[$i]->name)
			{
				continue;
			}

			$crumbName = stripslashes(htmlspecialchars(strip_tags($items[$i]->name), ENT_COMPAT, 'UTF-8'));

			// Remove [icon] shortcodes added by 3rd party plugins
			$crumbName = preg_replace('#\[icon\].*?\[\/icon\]#', '', $crumbName);

			$crumbs[$i] = (object) [
				'name' => trim($crumbName),
				'link' => self::route($items[$i]->link)
 			];
		}

		// Add Home item
		if ($addhome)
		{
			// Look for the home menu
			$home = Multilanguage::isEnabled() ? $menu->getDefault($lang->getTag()) : $menu->getDefault();

			$item       = new \stdClass;
			$item->name = htmlspecialchars($hometext);
			$item->link = self::route('index.php?Itemid=' . $home->id);

			array_unshift($crumbs, $item);
		}

		// Fix last item's missing URL to make Google Markup Tool happy
		end($crumbs);
		if (empty($crumbs->link))
		{
			$crumbs[key($crumbs)]->link = Uri::current();
		}

		// Convert relative URLs to absolute URLs
		foreach ($crumbs as $key => &$crumb)
		{
			// JFilters seems to make the "link" property a \Joomla\CMS\Uri\Uri object in some cases, why?
			$link = is_string($crumb->link) ? $crumb->link : ($crumb->link instanceof \Joomla\CMS\Uri\Uri ? $crumb->link->toString() : '');

			if (!$link)
			{
				continue;
			}

			$crumb->link = self::absURL($crumb->link);
		}

		return $crumbs;
	}

	/**
	 *  Makes text safe for JSON outpout
	 *
	 *  @param   string   $text   The text 
	 *  @param   integer  $limit  Limit characters
	 *
	 *  @return  string
	 */
	public static function makeTextSafe($text, $allowed_tags = null, $limit = 0)
	{
		if (empty($text))
		{
			return;
		}

		// Remove <script> tags
		$text = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $text);

		// Strip HTML tags/comments and minify
		$text = strip_tags($text, $allowed_tags);

		// There are some plugins that parse their shortcodes while Joomla! is booting like on the onAfterRender event instead of the onContentPrepare event
		// which GSD can control. So, if one of these shortcodes is parsed in the generated structured data, the page is very likely to broke and the only
		// way to prevent that from happening is to remove these shortcodes using regex.
		$text = preg_replace([
			// System - Zen Shortcodes
			'/{(\/?)zen-(.*?)}/m', 									   
			// System - wbAMP 
			'/{wbamp-(show|hide) start}(.*?){wbamp-(hide|show) end}/s' 
		], '', $text);
		
		// Minify Text
		$text = self::minify($text);

		// Limit characters length
		if ($limit > 0)
		{
			$text = StringHelper::substr($text, 0, $limit);
		}

        return trim($text);
	}

	/**
	 *  Minify String
	 *
	 *  @param   string  $string  The string to be minified
	 *
	 *  @return  string           The minified string
	 */
	public static function minify($string)
	{
    	return preg_replace('/(\s)+/s', ' ', $string);
	}

	/**
	 *  Returns absolute URL
	 *
	 *  @param   string  $url  The URL
	 *
	 *  @return  string
	 */
	public static function absURL($url)
	{
		if (!is_string($url))
		{
			return '';
		}
		
		$url = Uri::getInstance($url);

		// Return the original URL if we're manipulating an external URL
		if (in_array($url->getScheme(), ['https', 'http']))
		{
			return $url->toString();
		}

		$url = str_replace([Uri::root(), Uri::root(true)], '', $url->toString());
		$url = ltrim($url, '/');
		
		return Uri::root() . $url;
	}

	/**
	 *  Returns URLs based on the Force SSL global configuration
	 *
	 *  @param   string   $route  The route for which we want a URL
	 *  @param   boolean  $xhtml  If we want the output to be in XHTML
	 *
	 *  @return  string           The absolute url
	 */
	public static function route($route, $xhtml = true)
	{
		$siteSSL = Factory::getConfig()->get('force_ssl');
		$sslFlag = 2;

		// the force_ssl value in the global configuration needs
		// to be 2 for the frontend to also be under HTTPS
		if (($siteSSL == 2) || (isset($_SERVER['HTTPS']) && !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'))
		{
			$sslFlag = 1;
		}
		
		return Route::_($route, $xhtml, $sslFlag);
	}

	/**
	 *  Transform a UTC date to ISO8601 format. 
	 *  
	 *  The timezone part describes the hours that already have been counted towards the time.
	 * 
	 *  Let's say we have an article published on 2012-04-10 19:31:00 local date Athens +02:00.
	 * 	The date should be displayed in the structured data as 2012-04-10T19:31:00+02:00
	 * 
	 *  References:
	 *  https://developers.google.com/search/docs/data-types/event#time-date-incorrect
	 *  https://github.com/Yoast/wordpress-seo/issues/12765
	 *  https://wordpress.org/support/topic/structured-data-wrong-time
	 *
	 *  @param   Date  $date  
	 *
	 *  @return  Date
	 */
	public static function date($date, $modify_offset = false)
	{
		$date = is_string($date) ? trim($date) : $date;

		if (empty($date) || is_null($date) || $date == '0000-00-00 00:00:00')
		{
			return null;
		}

		// Skip if date is already in ISO8601 format
		if (strpos($date, 'T') !== false)
		{
			return $date;
		}

		try {
			$tz = new \DateTimeZone(Factory::getApplication()->getCfg('offset', 'UTC'));

			if ($modify_offset)
			{
				$date_ = new Date($date);
				$date_->setTimezone($tz);
			} else 
			{
				$date_ = new Date($date, $tz);
			}
	
			return $date_->toISO8601(true);

		} catch (\Exception $e) {
			return $date;
		}
	}

	/**
	 * Formats price according to https://schema.org/price
	 * 
	 * @param  mixed $price    Use '.' rather than ',' to indicate a decimal point.
	 *
	 * @return float 2 Decimal point float price
	 */
	public static function formatPrice($price)
	{
		// Prevent "A non well formed numeric value encountered" error.
		$price = str_replace(',', '.', (string) $price);

		return number_format((float) $price, 2, '.', '');
	}

	/**
	 *  Determine if the user is viewing the front page
	 *
	 *  @return  boolean
	 */
	public static function isFrontPage()
	{
		$menu = Factory::getApplication()->getMenu();
		$lang = Factory::getLanguage()->getTag();
		return ($menu->getActive() == $menu->getDefault($lang));
	}

    /**
     *  Logs messages to log file
     *
     *  @param   object  $type  The log type
     *
     *  @return  void
     */
    public static function log($msg)
    {
		self::$log[] = $msg;
    }

    /**
     *  Renders Backend's Sidebar
     *
     *  @return  string  The HTML output
     */
    public static function renderSideBar()
    {
		

		$data = array(
			'view'  => Factory::getApplication()->input->get('view', 'gsd'),
			'items' => array(
				array(
					'label' => 'NR_DASHBOARD',
					'url'   => 'index.php?option=com_gsd',
					'icon'  => 'dashboard',
					'view'  => 'gsd'
				),
				array(
				 	'label' => 'GSD_ITEMS',
					'url'   => 'index.php?option=com_gsd&view=items',
					'icon'  => 'list',
					'view'  => 'items,item'
				),
				array(
				 	'label' => 'GSD_CONFIG',
					'url'   => 'index.php?option=com_gsd&view=config&layout=edit',
					'icon'  => 'options',
					'view'  => 'config'
				),
				array(
				 	'label'  => 'NR_DOCUMENTATION',
					'url'    => 'https://www.tassos.gr/joomla-extensions/google-structured-data-markup/docs/',
					'icon'   => 'file-2',
					'target' => 'blank'
				),
				array(
				 	'label'  => 'NR_SUPPORT',
					'url'    => 'http://www.tassos.gr/contact',
					'icon'   => 'help',
					'target' => 'blank'
				)
			)
		);

        return LayoutHelper::render('sidebar', $data);
    }

    /**
     *  Get website name
     *
     *  @return  string  Site URL
     */
    public static function getSiteName()
    {
        return self::getParams()->get('sitename_name', Factory::getConfig()->get('sitename'));
    }
    
    /**
     *  Returns the Site Logo URL
     *
     *  @return  string
     */
    public static function getSiteLogo()
    {
        if (!$logo = self::getParams()->get('logo_file', null))
        {
            return;
        }

        return Uri::root() . $logo;
    }

	/**
	 *  Get website URL
	 *
	 *  @return  string  Site URL
	 */
	public static function getSiteURL()
	{
		return self::getParams()->get('sitename_url', Uri::root());
	}

    /**
     *  Get Plugin Parameters
     *
     *  @return  Registry
     */
    public static function getParams()
    {
    	if (self::$params)
		{
			return self::$params;
		}

		Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_gsd/tables');
		
		$table = Table::getInstance('Config', 'GSDTable');
		$table->load('config');

		return (self::$params = new Registry($table->params));
    }

    /**
     *  Returns permissions
     *
     *  @return  object
     */
    public static function getActions()
    {
        $user = Factory::getUser();
        $result = new CMSObject();
        $assetName = 'com_gsd';

        $actions = array(
            'core.admin', 'core.manage', 'core.create', 'core.edit', 'core.edit.state', 'core.delete'
        );

        foreach ($actions as $action)
        {
            $result->set($action, $user->authorise($action, $assetName));
        }

        return $result;
    }

    /**
     *  Loads all GSD plugins and triggers an event
     *
     *  @return  mixed
     */
    public static function event($name, $arguments = [])
    {
		PluginHelper::importPlugin('gsd');
		PluginHelper::importPlugin('system');

    	return Factory::getApplication()->triggerEvent($name, $arguments);
    }

    /**
     *  Get list with all available plugins
     *
     *  @return  array
     */
    public static function getPlugins()
    {
		return array_filter(self::event('onGSDGetType'));
    }

    /**
     *  Get the 1st found plugin's alias
     *
     *  @return  string  The plugin's alias
     */
    public static function getDefaultPlugin()
    {
		$plugins = self::getPlugins();
        return $plugins[0]['alias'];
    }

	/**
     *  Returns active component alias
     *
     *  @return  mixed String on success, false on failure
     */
    public static function getComponentAlias()
    {
        if (!$option = Factory::getApplication()->input->get('option'))
        {
            return;
        }

        $optionParts = explode('_', $option);
        
        return isset($optionParts[1]) ? $optionParts[1] : false;
    }

	/**
     *  Checks whether the plugin is a Pro version
     *
     *  @return  boolean  
     */
    public static function isPro()
    {
    	return \NRFramework\Functions::extensionHasProInstalled('plg_system_gsd');
	}
	
    /**
     *  Split string into array on each new line character
     *
     *  @param   string  $str  The string to split
     *
     *  @return  array
     */
    public static function makeArrayFromNewLine($str)
    {
		// Sanity check
		if (empty($str))
		{
			return $str;
		}

        $array = preg_split("/\\r\\n|\\r|\\n/", $str);

        if (!$array)
        {
            return $str;
        }

        return array_values(array_filter($array));
	}
	
	/**
	 * Convert an array to UTF8 encoding
	 *
	 * @param  array	The array to convert
	 *
	 * @return array
	 */
	public static function arrayToUTF8($array)
	{
		if (!is_array($array) || !function_exists('mb_convert_encoding'))
		{
			return $array;
		}

		array_walk_recursive($array, function(&$value, $key)
		{
			if (is_string($value))
			{
				$value = mb_convert_encoding($value, 'utf8');
			}
		});

		return $array;
	}

	/**
	 * Get first image src attribute from a string
	 *
	 * @param  string $text  The text to search for an image
	 *
	 * @return mixed  string on success, null on failure
	 */
	public static function getFirstImageFromString($text)
	{
		if (empty($text))
		{
			return;
		}

		preg_match('/<img.+src=[\'"](?P<src>.+?)[\'"].*>/i', $text, $image);
		return !empty($image) && isset($image['src']) ? $image['src'] : '';
	}
	
	/**
	 * Returns the weekdays
	 * 
	 * @param  boolean  $capitalize	 Capitalizes the first letter of each day
	 * 
	 * @return array
	 */
	public static function getWeekdays($capitalize = false)
	{
		$days = array('monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday');

		if ($capitalize)
		{
			$days = array_map('ucfirst', $days);
		}
		
		return $days;
	}

    /**
     * This is a joke. Joomla 4's media field started including width and height information in the path. 
     * So, we need to clean the path before we can use it. 
     * 
     * images/headers/blue-flower.jpg#joomlaImage://local-images/headers/blue-flower.jpg?width=700&height=180)
     * 
     * @param  string $path
     * 
     * @return string
     */
    public static function cleanImage($path)
    {
		$path = Helper::absURL($path);
        return defined('nrJ4') ? \Joomla\CMS\Helper\MediaHelper::getCleanMediaFieldValue($path) : $path;
    }

	public static function convert_to_ISO8601($value, $fallback_interval = 'M')
	{
		// Skip values already in ISO8601 format.
		if (substr($value, 0, 1) == 'P')
		{
			return $value;
		}

		return 'PT' . $value . $fallback_interval;
	}
}GSD/Json.php000064400000146620152345574300006624 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace GSD;

defined('_JEXEC') or die('Restricted Access');

use Joomla\Registry\Registry;
use Joomla\CMS\Text\Text;

/**
 *  Google Structured Data JSON generator
 */
class JSON
{
	/**
	 *  Content Type Data
	 *
	 *  @var  object
	 */
	private $data;

    /**
     *  List of available content types
     *
     *  @var  array
     */
    private $contentTypes = [
        
        'book',
        'course',
        'event',
        'product',
        'movie',
        'recipe',
        'review',
        'factcheck',
        'video',
        'jobposting',
        'custom_code',
        'faq',
        'howto',
        'localbusiness',
        'service',
        'person',
        
        'article'
    ];

	/**
	 *  Class Constructor
	 *
	 *  @param  object  $data
	 */
	public function __construct($data = null)
	{
		$this->setData($data);
	}

    /**
     *  Get Content Types List
     *
     *  @return  array
     */
    public function getContentTypes()
    {
        $types = $this->contentTypes;
        asort($types);

        // Move Custom Code option to the end
        if ($customCodeIndex = array_search('custom_code', $types))
        {
            unset($types[$customCodeIndex]);
            $types[] = 'custom_code';
        }

        return $types;
    }

	/**
	 *  Set Data
	 *
	 *  @param  array  $data
	 */
	public function setData($data)
	{
		if (is_array($data))
		{
			$this->data = new Registry($data);
		} else 
        {
            $this->data = $data;
        }

		return $this;
	}

	/**
	 *  Get Content Type result
	 *
	 *  @return  string
	 */
	public function generate()
	{
        $contentTypeMethod = 'contentType' . $this->data->get('contentType');

        // Make sure we have a valid Content Type
		if (!method_exists($this, $contentTypeMethod) || !$content = $this->$contentTypeMethod())
		{
            return;
		}

        // In case we have a string (See Custom Code), return the original content.
        if (is_string($content))
        {
            return $content;
        }

        Helper::event('onGSDSchemaBeforeGenerate', [&$content, $this->data]);

        // Sanity check
        if (!$content)
        {
            return;
        }

        // Remove null and empty properties
        $content = $this->clean($content);

        // In case we have an array, transform it into JSON-LD format.
        // Always prepend the @context property
        $content = ['@context' => 'https://schema.org'] + $content;

        // We do not use JSON_NUMERIC_CHECK here because it doesn't respect numbers starting with 0. 
        // Bug: https://bugs.php.net/bug.php?id=70680
        $json_string = json_encode($content, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

        // Detect issues with the encoding
        if (json_last_error() !== JSON_ERROR_NONE)
        {
            $json_string = Text::sprintf('JSON Error: %s.', json_last_error_msg()) . ' ' . $content;
        }

return '
<script type="application/ld+json" data-type="gsd">
'
    . $json_string .
'
</script>';
    }
    
    /**
     * Filter resursively an array by removing empty, false and null properties while preserving 0 values.
     *
     * @param  array $input
     *
     * @return array
     */
    private function clean($input)
    { 
        foreach ($input as &$value) 
        {
            if (is_array($value)) 
            { 
                $value = self::clean($value);
            }
        }

        // We use a custom callback here because the default behavior of array_filter removes 0 values as well.
        return array_filter($input, function($value)
        {
            // Remove also orphan array properties
            if (is_array($value) && count($value) == 1 && isset($value['@type']))
            {
                return false;
            }

            return ($value !== null && $value !== false && $value !== ''); 
        });
    }

    /**
     * Constructs the HowTo Schema Type
     * 
     * @return  array
     */
    private function contentTypeHowTo()
    {
        $steps = array_map(function($step)
        {
            return array_merge(['@type' => 'HowToStep'], $step);
        }, $this->data->get('step'));

        $tools = array_map(function($tool)
        {
            return [
                '@type' => 'HowToTool',
                'name' => $tool
            ];
        }, (array) $this->data->get('tool'));

        $supply = array_map(function($supply)
        {
            return [
                '@type' => 'HowToSupply',
                'name' => $supply
            ];
        }, (array) $this->data->get('supply'));

        return [
            '@type' => 'HowTo',
            'image' => [
                '@type' => 'ImageObject',
                'url' => $this->data->get('image')
            ],
            'name' => $this->data->get('name'),
            'totalTime' => $this->data->get('totalTime'),
            'estimatedCost' => [
                '@type' => 'MonetaryAmount',
                'currency' => $this->data->get('estimatedCostCurrency'),
                'value' => $this->data->get('estimatedCost')
            ],
            'supply' => $supply,
            'tool' => $tools,
            'step' => $steps
        ];
    }   

    /**
     * Constructs the FAQ Snippet
     * 
     * @return  array
     */
    private function contentTypeFAQ()
    {
        $faq = $this->data->get('faqs');

        // If there are no FAQ data, return
        if (count($faq) == 0)
        {
            return;
        }

        $faqData = [];

        foreach ($faq as $item)
        {
            $faqData[] = [
                '@type' => 'Question',
                'name'  => $item['question'],
                'acceptedAnswer' => [
                    '@type'      => 'Answer',
                    'text'       => $item['answer']
                ]
            ];
        }

        return [
            '@type'      => 'FAQPage',
            'mainEntity' => $faqData
        ];
    }

    /**
     *  Constructs the Breadcrumbs Snippet
     *
     *  @return  array
     */
    private function contentTypeBreadcrumbs()
    {
        $crumbs = $this->data->get('crumbs');
        
        if (!is_array($crumbs))
        {
            return;
        }

        $crumbsData = [];

        foreach ($crumbs as $key => $value)
        {
            $crumbsData[] = [
                '@type'    => 'ListItem',
                'position' => ($key + 1),
                'name'     => $value->name,
                'item'     => $value->link
            ];
        }

        return [
            '@type'           => 'BreadcrumbList',
            'itemListElement' => $crumbsData
        ];
    }

    /**
     *  Constructs the Website schema with the following info:
     * 
     *  Site Name: https://developers.google.com/structured-data/site-name
     *  Sitelinks Searchbox: https://developers.google.com/search/docs/data-types/sitelinks-searchbox
     *
     *  @return  array
     */
    private function contentTypeWebsite()
    {
        $content = [
            '@type' => 'WebSite',
            'url'   => $this->data->get('site_url')
        ];

        // Site Name
        if ($this->data->get('site_name_enabled'))
        {
            $content = array_merge($content, [
                'name'  => $this->data->get('site_name'),
                'alternateName' => $this->data->get('site_name_alt')
            ]);
        }

        // Sitelinks Search
        if ($this->data->get('site_links_search'))
        {
            $content = array_merge($content, [
                'potentialAction' => [
                    '@type'       => 'SearchAction',
                    'target'      => $this->data->get('site_links_search'),
                    'query-input' => 'required name=search_term'  
                ]
            ]);
        }

        return $content;
    }

    /**
     *  Constructs Site Logo Snippet
     *  https://developers.google.com/search/docs/data-types/logo
     *
     *  @return  array
     */
    private function contentTypeLogo()
    {
        return [
            '@type' => 'Organization',
            'url'   => $this->data->get('url'),
            'logo'  => $this->data->get('logo')
        ];
    }

	/**
	 *  Constructs the Article Content Type
	 *
	 *  @return  array
	 */
	private function contentTypeArticle()
	{
        $content = [
            '@type' => $this->data->get('type', 'Article'),
            'mainEntityOfPage' => [
                '@type' => 'WebPage',
                '@id'   => $this->data->get('url')
            ],
            'headline'    => $this->data->get('title'),
            'description' => $this->data->get('description'),
            'image' => [
                '@type'  => 'ImageObject',
                'url'    => $this->data->get('image')
            ]
        ];

		// Publisher
		if ($this->data->get('publisherName'))
		{
            $content = array_merge($content, [
                'publisher' => [
                    '@type' => 'Organization',
                    'name'  => $this->data->get('publisherName'),
                    'logo'  => [
                        '@type'  => 'ImageObject',
                        'url'    => $this->data->get('publisherLogo')
                    ]
                ]
            ]);  
		}

        // Add author
        $this->addAuthor($content);

        return $this->addDate($content);
	}

    /**
	 *  Constructs the Social Profiles Snippet
	 *  https://developers.google.com/search/docs/data-types/social-profile-links
	 *
	 *  @return  array
	 */
	private function contentTypeSocialProfiles()
	{
        return [
            '@type'  => $this->data->get('type'),
            'name'   => $this->data->get('sitename'),
            'url'    => $this->data->get('siteurl'),
            'sameAs' => array_values((array) $this->data->get('links'))
        ];
	}

    
    /**
     *  Constructs the Business Listing Content Type
     *  https://developers.google.com/search/docs/data-types/local-businesses
     *
     *  @return  array
     */
    private function contentTypeLocalBusiness()
    {
        $content = [
            '@type' => $this->data->get('type'),
            // Neither Rich Results Test or the deprecated tool, Google Structured Data Testing Tool doesn't throw a warning any more if the @id property is missing.            
            '@id'   => $this->data->get('id'),
            'name'  => $this->data->get('name'),
            'image' => $this->data->get('image'),
            'url' => $this->data->get('url'),
            'telephone' => $this->data->get('telephone'),
            'priceRange' => $this->data->get('priceRange'),
            'address' => $this->getPostalAddress()
        ];

        // Map coordinates
        $coords = $this->data->get('geo');
        if ($coords && !empty($coords) && count($coords) == 2)
        {
            $content['geo'] = [
                '@type'     => 'GeoCoordinates',
                'latitude'  => $coords[0],
                'longitude' => $coords[1]
            ];
        }
        
        // Opening Hours
        if ($this->data->get('openinghours'))
        {
            $openingHours = $this->getOpeningHours($this->data->get('openinghours'));
            $content = array_merge($content, $openingHours);
        }

        // Food-based business types
        $content['servesCuisine'] = $this->data->get('servesCuisine');
        $content['menu'] = $this->data->get('menu');

        // Add Review
        $this->addReview($content);

        // Aggregate Rating
        $this->addRating($content);

        return $content;
    }

    /**
     *  Constructs the Preson Content Type
     *
     *  @return  array
     */
    private function contentTypePerson()
    {
        $content = [
            '@type' => 'Person',
            '@id' => $this->data->get('id'),
            'url' => $this->data->get('url'),
            'name' => $this->data->get('title'),
            'description' => $this->data->get('description'),
            'honorificPrefix' => $this->data->get('honorificPrefix'),
            'honorificSuffix' => $this->data->get('honorificSuffix'),
            'alternateName' => $this->data->get('alternateName'),
            'additionalName' => $this->data->get('additionalName'),
            'givenName' => $this->data->get('givenName'),
            'familyName' => $this->data->get('familyName'),
            'address' => $this->getPostalAddress(),
            'nationality' => $this->data->get('nationality'),
            'email' => $this->data->get('email'),
            'telephone' => $this->data->get('telephone'),
            'gender' => $this->data->get('gender'),
            'birthDate' => $this->data->get('birthDate'),
            'memberOf' => $this->data->get('memberOf'),
            'image' => $this->data->get('image'),
            'jobTitle' => $this->data->get('jobTitle'),
            'affiliation' => $this->data->get('affiliation'),
            'alumniOf' => $this->data->get('alumniOf'),
            'award' => $this->data->get('award'),
            'knowsAbout' => $this->data->get('knowsAbout'),
            'hasCredential' => $this->data->get('hasCredential'),
            'hasOccupation' => [
                '@type' => 'Occupation',
                'name' => $this->data->get('occupationName'),
                'description' => $this->data->get('occupationDescription'),
                'educationRequirements' => $this->data->get('educationRequirements'),
                'experienceRequirements' => $this->data->get('experienceRequirements'),
                'occupationLocation' => [
                    '@type' => 'Country',
                    'name' => $this->data->get('addressCountry')
                ],
                'estimatedSalary' => [
                    '@type' => 'MonetaryAmountDistribution',
                    'name' => 'base',
                    'duration' => 'P1Y',
                    'minValue' => is_array($this->data->get('offerPrice')) ? $this->data->get('offerPrice')[0] : null,
                    'maxValue' => is_array($this->data->get('offerPrice')) ? $this->data->get('offerPrice')[1] : null,
                    'currency' => $this->data->get('currency'),
                ]
            ],
        ];

        if (empty(array_filter([
            $this->data->get('occupationName'),
            $this->data->get('occupationDescription'),
            $this->data->get('educationRequirements'),
            $this->data->get('experienceRequirements'),
            $this->data->get('offerPrice'),
            $this->data->get('currency')
        ])))
        {
            unset($content['hasOccupation']);
        }

        if ($this->data->get('type') !== 'Person')
        {
            $content['additionalType'] = $this->data->get('type');
        }

        if ($worksFor = $this->data->get('worksFor', ''))
        {
            $content['worksFor'] = [
                '@type' => 'Organization',
                'name' => $worksFor
            ];
        }

        if ($sameAs = (array) $this->data->get('sameAs', []))
        {
            $sameAs = array_column(array_values($sameAs), 'name');
            
            $content['sameAs'] = $sameAs;
        }

        return $content;
    }

    /**
     *  Constructs the Product Content Type
     *  https://developers.google.com/search/docs/data-types/products
     *
     *  @return  array
     */
    private function contentTypeProduct()
    {
        $content = [
            '@type'       => 'Product',
            'productID'   => $this->data->get('mpn'),
            'name'        => $this->data->get('title'),
            'image'       => $this->data->get('image'),
            'description' => $this->data->get('description'),
            'sku'         => $this->data->get('sku'),
            'mpn'         => $this->data->get('mpn'),
            'gtin'        => $this->data->get('gtin'),

            // @todo - We should replace "." with "_" in all properties to prevent issues with Registry namespace.
            // For instance, all matches of category.id should be renamed to category_id.
            'google_product_category' => $this->data->get('category_id')
        ];

        // Weight
        if ($this->data->get('weight'))
        {
            $content = array_merge($content, [
                'weight' => [
                    '@type' => 'QuantitativeValue',
                    'value' => $this->data->get('weight'),
                    'unitText' => $this->data->get('weightUnit')
                ]
            ]);
        }

        // Brand
        if ($this->data->get('brand'))
        {
            $content = array_merge($content, [
                'brand' => [
                    '@type' => 'Brand',
                    'name'  => $this->data->get('brand')
                ]
            ]);
        }

        // Offer / Pricing
        if ($price = $this->data->get('offerPrice'))
        {
            $offerCommon = [
                'priceCurrency'   => $this->data->get('currency'),
                'url'             => $this->data->get('url'),
                'itemCondition'   => $this->data->get('offerItemCondition'),
                'availability'    => $this->data->get('offerAvailability'),
                'priceValidUntil' => $this->data->get('priceValidUntil')
            ];

            if (is_array($price))
            {
                $offer = [
                    '@type' => 'AggregateOffer',
                    'offerCount' => $this->data->get('offerCount', 1),
                    'lowPrice' => $price[0],
                    'highPrice' => $price[1]
                ];
            } else 
            {
                $offer = [
                    '@type' => 'Offer',
                    'price' => $price
                ];
            }

            $content['offers'] = array_merge($offer, $offerCommon);
        }

        // Add Review
        $this->addReview($content);
        
        // Aggregate Rating
        $this->addRating($content);

        return $content;
    }

    /**
     * Adds review data to content
     * 
     * @param   array  $content
     * 
     * @return  void
     */
    private function addReview(&$content)
    {
        if (!$this->data->get('reviews') || !$this->data->get('reviewCount'))
        {
            return;
        }

        // Review
        $reviews     = $this->data->get('reviews');
        $bestRating  = $this->data->get('bestRating', 5);
        $worstRating = $this->data->get('worstRating', 0);

        $review_data = [];
        foreach ($reviews as $review)
        {
            $rating = $review['rating'];

            $review_data[] = [
                '@type' => 'Review',
                'author' => [
                    '@type' => 'Person',
                    'name'  => $review['author'],
                ],
                'datePublished' => $review['datePublished'],
                'description' => $review['description'],
                'reviewRating' => [
                    '@type' => 'Rating',
                    'bestRating'  => $bestRating,
                    'ratingValue' => $rating,
                    'worstRating' => $worstRating
                ]
            ];
        }

        $content = array_merge($content, [
            'review' => $review_data
        ]);
    }

	/**
	 *  Constructs the Book Content Type
	 *  https://developers.google.com/search/docs/appearance/structured-data/book
	 *
	 *  @return  array
	 */
	private function contentTypeBook()
	{
        $workExample = [
            '@type' => 'Book',
            '@id' => $this->data->get('id'),
            'bookFormat' => $this->data->get('bookFormat'),
            'inLanguage' => $this->data->get('inLanguage'),
            'isbn' => $this->data->get('isbn'),
            'url' => $this->data->get('url'),
            'bookEdition' => $this->data->get('edition'),
            'datePublished' => $this->data->get('datePublished'),
            'potentialAction' => [
                '@type' => $this->data->get('potentialAction'),
                'target' => [
                    '@type' => 'EntryPoint',
                    'urlTemplate' => $this->data->get('actionURL'),
                    'actionPlatform' => [
                        'http://schema.org/DesktopWebPlatform',
                        'http://schema.org/AndroidPlatform',
                        'http://schema.org/IOSPlatform'
                    ]
                ]
            ]
        ];
        
        // Add author to $workExample
        $this->addAuthor($workExample);
        
        $content = [
            '@type' => 'Book',
            '@id' => $this->data->get('id'),
            'url' => $this->data->get('url'),
            'name' => $this->data->get('title'),
            'image' => $this->data->get('image'),
            'sameAs' => $this->data->get('referenceURL'),
            'inLanguage' => $this->data->get('inLanguage'),
            'workExample' => $workExample
        ];

        // Add author
        $this->addAuthor($content);

        // Add identifier to workExample property
        $identifiers = [];
        if ($identifier_oclc_number = $this->data->get('identifier_oclc_number'))
        {
            $identifiers['OCLC_NUMBER'] = $identifier_oclc_number;
        }
        if ($identifier_lccn = $this->data->get('identifier_lccn'))
        {
            $identifiers['LCCN'] = $identifier_lccn;
        }
        if ($identifier_jp_e_code = $this->data->get('identifier_jp_e_code'))
        {
            $identifiers['JP_E-CODE'] = $identifier_jp_e_code;
        }
        if (count($identifiers))
        {
            $values = [];
            
            foreach ($identifiers as $key => $value)
            {
                $values[] = [
                    '@type' => 'PropertyValue',
                    'propertyID' => $key,
                    'value' => $value
                ];
            }

            if ($values)
            {
                $content['workExample'] = array_merge($content['workExample'], [
                    'identifier' => $values
                ]);
            }
        }

        return $content;
    }

	/**
	 *  Constructs the Event Content Type
	 *  https://developers.google.com/search/docs/data-types/events
	 *
	 *  @return  array
	 */
	private function contentTypeEvent()
	{
        $content = [
            '@type'       => 'Event',
            'name'        => $this->data->get('title'),
            'image'       => $this->data->get('image'),
            'description' => $this->data->get('description'),
            'url'         => $this->data->get('url'),
            'startDate'   => $this->data->get('startDate'),
            'endDate'     => $this->data->get('endDate'),
            'eventStatus' => 'https://schema.org/EventScheduled',
            'eventAttendanceMode' => $this->data->get('eventAttendanceMode', 'https://schema.org/OfflineEventAttendanceMode'),
            'offers' => [
                '@type'         => 'Offer',
                'url'           => $this->data->get('url'),
                'availability'  => $this->data->get('offerAvailability'),
                'validFrom'     => $this->data->get('startDateTime'),
                'price'         => $this->data->get('price'),
                'priceCurrency' => $this->data->get('offerCurrency'),
                'inventoryLevel' => [
                    '@context' => 'https://schema.org',
                    '@type'    => 'QuantitativeValue',
                    'value'    => $this->data->get('offerInventoryLevel'),
                    'unitText' => 'Tickets'
                ]
            ]
        ];

        // Psysical Location
        if ($this->data->get('locationName'))
        {
            $content['location'] = [
                '@type'   => 'Place',
                'name'    => $this->data->get('locationName'),
                'address' => $this->getPostalAddress()
            ];
        }

        // Online Event
        if ($online_event_url = $this->data->get('online_url'))
        {
            $online_event = [
                '@type' => 'VirtualLocation',
                'url'   => $online_event_url
            ];

            if (isset($content['location']))
            {
                $content['location'] = [$content['location'], $online_event];
            } else 
            {
                $content['location'] = $online_event;
            }
        }

        // Performer
        if ($this->data->get('performerName'))
        {
            $content = array_merge($content, [
                'performer' => [
                    '@type' => $this->data->get('performerType'),
                    'name'  => $this->data->get('performerName'),
                    'url'  => $this->data->get('performerURL')
                ],
            ]);
        }

        // Organizer
        if ($this->data->get('organizerName'))
        {
            $content = array_merge($content, [
                'organizer' => [
                    '@type' => $this->data->get('organizerType'),
                    'name'  => $this->data->get('organizerName'),
                    'url'  => $this->data->get('organizerURL')
                ],
            ]);
        }

		return $content;
	}

	/**
	 *  Constructs the Movie Content Type
	 *  https://developers.google.com/search/docs/data-types/movie
	 *
	 *  @return  array
	 */
	private function contentTypeMovie()
	{
        $content = [
            '@type'       => 'Movie',
            'url'         => $this->data->get('url'),
            'name'        => $this->data->get('title'),
            'description' => $this->data->get('description'),
            'image'       => $this->data->get('image'),
            'dateCreated' => $this->data->get('datePublished'),
        ];

        // Duration
        if ($duration = $this->data->get('duration'))
        {
            $content['duration'] = $duration;
        }

        // Genre
        if ($genre = $this->data->get('genre'))
        {
            $genreData = [];

            foreach ($genre as $key => $g)
            {
                if (empty($g->name))
                {
                    continue;
                }

                $genreData['genre'][] = trim($g->name);
            }

            $content = array_merge($content, $genreData);
        }

        // Creators
        if ($creators = $this->data->get('creators'))
        {
            $creatorsData = [];

            foreach ($creators as $key => $creator)
            {
                if (empty($creator->name))
                {
                    continue;
                }

                $creatorsData['creator'][] = [
                    '@type' => 'Person',
                    'name'  => trim($creator->name)
                ];
            }

            $content = array_merge($content, $creatorsData);
        }

        // Directors
        if ($directors = $this->data->get('directors'))
        {
            $directorsData = [];

            foreach ($directors as $key => $director)
            {
                if (empty($director->name))
                {
                    continue;
                }

                $directorsData['director'][] = [
                    '@type' => 'Person',
                    'name'  => trim($director->name)
                ];
            }

            $content = array_merge($content, $directorsData);
        }

        // Actors
        if ($actors = $this->data->get('actors'))
        {
            $actorsData = [];

            foreach ($actors as $key => $actor)
            {
                if (empty($actor->name))
                {
                    continue;
                }

                $actorsData['actor'][] = [
                    '@type' => 'Person',
                    'name'  => trim($actor->name)
                ];
            }

            $content = array_merge($content, $actorsData);
        }

        // Trailer
        $content['trailer'] = [
            '@type' => 'VideoObject',
            'embedUrl' => $this->data->get('trailerUrl'),
            'name' => $this->data->get('title'),
            'thumbnail' => [
                '@type' => 'ImageObject',
                'contentUrl' => $this->data->get('image')
            ],
            'thumbnailUrl' => $this->data->get('image'),
            'description' => $this->data->get('description'),
            'uploadDate' => $this->data->get('datePublished')
        ];

        // Add Aggregate Rating
        $this->addRating($content);
        
        $this->addReview($content);

		return $content;
	}

    /**
     * Gets the Opening Hours for the Business Listing Type
     * 
     * @param   array  $openingHours
     * 
     * @return  array
     */
    private function getOpeningHours($openingHours)
    {
        $content = [];

        // get the hours available
        // 0: No hours specified
        // 1: Always Open
        // 2: Specific Hours
        $hoursAvailable = (int) $openingHours->option;

        // return if no hours are specified
        if ($hoursAvailable == 0)
        {
            return $content;
        }

        unset($openingHours->option);
        $weekdays = array_map('ucfirst', array_keys((array) $openingHours));

        // Always Open
        if ($hoursAvailable == 1)
        {
            $content['openingHoursSpecification'] = [
                '@type'     =>  'OpeningHoursSpecification',
                'dayOfWeek' => $weekdays,
                'opens'     => '00:00',
                'closes'    => '23:59'
            ];

            return $content;
        }

        // Selected Dates
        $openingHoursData = [];

        foreach ($openingHours as $day_name => $day_options)
        {
            $day_name = ucfirst($day_name);

            if (!isset($day_options->enabled) || !$day_options->enabled)
            {
                continue;
            }

            // If no hours are set, assume open 24 hours
            if (empty($day_options->start) && empty($day_options->end))
            {
                $openingHoursData[] = [
                    '@type'       => 'OpeningHoursSpecification',
                    'dayOfWeek'   => $day_name,
                    'opens'       => '00:00',
                    'closes'      => '23:59'
                ];

                continue;
            }
            
            $openingHoursData[] = [
                '@type'       => 'OpeningHoursSpecification',
                'dayOfWeek'   => $day_name,
                'opens'       => $day_options->start,
                'closes'      => $day_options->end
            ];

            if (empty($day_options->start1) || empty($day_options->end1))
            {
                continue;
            }

            $openingHoursData[] = [
                '@type'       => 'OpeningHoursSpecification',
                'dayOfWeek'   => $day_name,
                'opens'       => $day_options->start1,
                'closes'      => $day_options->end1
            ];
        }

        if ($openingHoursData)
        {
            $content['openingHoursSpecification'] = $openingHoursData;             
        }

        return $content;
    }

	/**
	 *  Constructs the Recipe Content Type
	 *  https://developers.google.com/search/docs/data-types/recipes
	 *
	 *  @return  array
	 */
	private function contentTypeRecipe()
	{
        $content = [
            '@type'          => 'Recipe',
            'name'           => $this->data->get('title'),
            'image'          => $this->data->get('image'),
            'description'    => $this->data->get('description'),
            'prepTime'       => $this->data->get('prepTime'),
            'cookTime'       => $this->data->get('cookTime'),
            'totalTime'      => $this->data->get('totalTime'),
            'keywords'       => $this->data->get('keywords'),
            'recipeCuisine'  => $this->data->get('cuisine'),
            'recipeCategory' => $this->data->get('category'),
            'recipeYield'        => $this->data->get('yield'),
            'recipeIngredient'   => $this->data->get('ingredient'),
            'recipeInstructions' => $this->data->get('instructions')
        ];

        if ($this->data->get('calories'))
        {
            $content = array_merge($content, [
                'nutrition'    => [
                    '@type'    => 'NutritionInformation',
                    'calories' => $this->data->get('calories')
                ], 
            ]);
        }

        if ($this->data->get('video'))
        {
            $content = array_merge($content, [
                'video' => [
                    '@type'        => 'VideoObject',
                    'name'         => $this->data->get('title'),
                    'description'  => $this->data->get('description'),
                    'thumbnailUrl' => $this->data->get('image'),
                    'contentUrl'   => $this->data->get('video'),
                    'uploadDate'   => $this->data->get('datePublished')
                ]
            ]);
        }

        // Add author
        $this->addAuthor($content);

        $this->addRating($content);
        $this->addDate($content);

		return $content;
	}

    /**
     *  Constructs the Course Content Type
     *  https://developers.google.com/search/docs/data-types/courses
     *
     *  @return  array
     */
	private function contentTypeCourse()
	{
        $content = [
            '@type' => 'Course',
            'name'  => $this->data->get('title'),
            'description' => $this->data->get('description'),
            'courseCode' => $this->data->get('course_code'),
            'provider' => [
                '@type'  => 'Organization',
                'name'   => $this->data->get('sitename')
            ],
            'hasCourseInstance' => [
                '@type' => 'CourseInstance',
                'name'  => $this->data->get('title'),
                'description'  => $this->data->get('description'),
                'courseMode' => $this->data->get('course_mode'),
                'startDate' => $this->data->get('startDate'),
                'endDate' => $this->data->get('endDate'),
                'location' => [
                    '@type' => 'Place',
                    'name' => $this->data->get('place_name'),
                    'address' => $this->getPostalAddress()
                ],
                'image' => [
                    '@type' => 'ImageObject',
                    'url'   => $this->data->get('image')
                ],
                'performer' => [
                    '@type' => $this->data->get('performer_type'),
                    'name' => $this->data->get('performer_name')
                ],
                'courseWorkload' => $this->data->get('courseWorkload')
            ]            
        ];

        if ($price = $this->data->get('price'))
        {
            $content['offers'] = [
                [
                    '@type' => 'Offer',
                    'category' => $this->data->get('offerCategory', 'Free'),
                    'url' => $this->data->get('url'),
                    'availability' => $this->data->get('availability'),
                    'price' => $this->data->get('price'),
                    'priceCurrency' => $this->data->get('priceCurrency'),
                    'validFrom' => $this->data->get('validFrom')
                ]
            ];
        }
      
        $this->addRating($content);
        $this->addDate($content);

        return $content;
	}

    /**
     *  Constructs the Review Content Type
     *  https://developers.google.com/search/docs/data-types/reviews
     *
     *  @return  array
     */
    private function contentTypeReview()
    {
        $content = [
            '@type' => 'Review',
            'description' => $this->data->get('description'),
            'url' => $this->data->get('url'), 
            'datePublished' => $this->data->get('datePublished'),
            'publisher'  => [
                '@type'  => 'Organization',
                'name'   => $this->data->get('sitename'),
                'sameAs' => $this->data->get('siteurl')
            ],
            'inLanguage' => $this->data->get('language_code'),
            'itemReviewed' => [
                '@type' => $this->data->get('itemReviewedType'),
                'name' => $this->data->get('title'),
                'image' => $this->data->get('image'),
                'sameAs' => $this->data->get('itemReviewedURL')
            ]
        ];
        
        if ($this->data->get('itemReviewedType') == 'LocalBusiness') 
        {
            $content = array_merge_recursive($content, [
                'itemReviewed' => [
                    'address'    => $this->getPostalAddress(),
                    'priceRange' => $this->data->get('priceRange'),
                    'telephone'  => $this->data->get('telephone')
                ]
            ]);
        }

        if (in_array($this->data->get('itemReviewedType'), ['Movie', 'Book']))
        {
            $content = array_merge_recursive($content, [
                'itemReviewed' => [
                    'datePublished' => $this->data->get('itemReviewedPublishedDate')
                ]
            ]);
        }

        if ($this->data->get('itemReviewedType') == 'Movie')
        {
            $movie = [
                'itemReviewed' => [
                    'director' => [
                        '@type' => 'Person',
                        'name'  => $this->data->get('movie_director')
                    ]
                ]
            ]; 

            if ($actors = $this->data->get('actors'))
            {
                foreach ($actors as $key => $actor)
                {
                    if (empty($actor->name))
                    {
                        continue;
                    }

                    $movie['itemReviewed']['actor'][] = [
                        '@type' => 'Person',
                        'name'  => trim($actor->name)
                    ];
                }
            }

            $content = array_merge_recursive($content, $movie);
        }

        if ($this->data->get('itemReviewedType') == 'Book')
        {
            $content = array_merge_recursive($content, [
                'itemReviewed' => [
                    'isbn' => $this->data->get('book_isbn'),
                    'author' => [
                        '@type'  => 'Person',
                        'name'   => $this->data->get('book_author'),
                        'sameAs' => $this->data->get('book_author_url')
                    ]
                ]
            ]);
        }

        // Handle Product Type
        if ($this->data->get('itemReviewedType') == 'Product')
        {
            $content['itemReviewed']['sku'] = $this->data->get('product_sku');
            $content['itemReviewed']['mpn'] = $this->data->get('product_sku');
            $content['itemReviewed']['brand'] = [
                '@type' => 'Brand',
                'name'  => $this->data->get('product_brand')
            ];

            $content['itemReviewed']['description'] = $this->data->get('product_description');

            // Rating
            if ($this->data->get('ratingValue') && $this->data->get('reviewCount'))
            {
                $content['itemReviewed']['aggregateRating'] = [
                    '@type'       => 'AggregateRating',
                    'ratingValue' => $this->data->get('ratingValue'),
                    'reviewCount' => $this->data->get('reviewCount')
                ];
            }

            // offers
            if ($this->data->get('offerprice'))
            {
                $content['itemReviewed']['offers'] = [
                    '@type'           => 'Offer',
                    'priceCurrency'   => $this->data->get('currency', 'USD'),
                    'url'             => $this->data->get('itemReviewedURL'),
                    'itemCondition'   => $this->data->get('condition', 'http://schema.org/NewCondition'),
                    'availability'    => $this->data->get('availability', 'http://schema.org/InStock'),
                    'price'           => $this->data->get('offerprice'),
                    'priceValidUntil' => $this->data->get('pricevaliduntil')
                ];
            }
            
            // Review
            if ($reviews = $this->getReviews())
            {
                $content['itemReviewed']['review'] = $reviews;
            }
        }

        // Add author
        $this->addAuthor($content);

        if ($this->data->get('ratingValue'))
        {
            $content = array_merge($content, [
                'reviewRating' => [
                    '@type'       => 'Rating',
                    'ratingValue' => $this->data->get('ratingValue'),
                    'worstRating' => $this->data->get('worstRating', 0),
                    'bestRating'  => $this->data->get('bestRating', 5)
                ]
            ]);
        }

        return $content;
    }

    /**
     * Returns the reviews of the data
     * 
     * @return  array
     */
    private function getReviews()
    {
        if (!$this->data->get('review') || !$this->data->get('reviewCount'))
        {
            return [];
        }

        $bestRating = $this->data->get('bestRating', 5);
        $worstRating = $this->data->get('worstRating', 0);

        $review_data = [];
        foreach ($this->data->get('review') as $review)
        {
            $rating = $review['rating'];

            $review_data[] = [
                '@type' => 'Review',
                'author' => [
                    '@type' => 'Person',
                    'name'  => $review['author'],
                ],
                'datePublished' => $review['datePublished'],
                'description' => $review['description'],
                'reviewRating' => [
                    '@type' => 'Rating',
                    'bestRating'  => $bestRating,
                    'ratingValue' => $rating,
                    'worstRating' => $worstRating
                ]
            ];
        }

        return $review_data;
    }

    /**
     *  Constructs the Fact Check Content Type
     *  https://developers.google.com/search/docs/data-types/factcheck
     *
     *  @return  array
     */
    private function contentTypeFactCheck()
    {
        $content = [
            '@type' => 'ClaimReview',
            'url' => $this->data->get('factcheckURL'),
            'itemReviewed' => [
                '@type'  => 'CreativeWork',
                'author' => [
                    '@type'  => $this->data->get('claimAuthorType'),
                    'name'   => $this->data->get('claimAuthorName'),
                    'sameAs' => $this->data->get('claimURL')
                ],
                'datePublished' => $this->data->get('claimDatePublished')
            ],
            'claimReviewed' => $this->data->get('title'),
            'author' => [
                '@type' => 'Organization',
                'name'  => $this->data->get('sitename')
            ],
            'reviewRating' => [
                '@type'         => 'Rating',
                'ratingValue'   => $this->data->get('factcheckRating'),
                'bestRating'    => $this->data->get('bestFactcheckRating'),
                'worstRating'   => $this->data->get('worstFactcheckRating'),
                'alternateName' => $this->data->get('alternateName')
            ]
        ];

        return $this->addDate($content);
    }

    /**
     *  Constructs the Service Content Type
     *  https://schema.org/Service
     *
     *  @return  array
     */
    private function contentTypeService()
    {
        $content = [
            '@type' => 'Service',
            'name' => $this->data->get('title'),
            'serviceType' => $this->data->get('title'),
            'description' => $this->data->get('description'),
            'image' => $this->data->get('image'),
            'url' => $this->data->get('url'), 
            'provider' => [
                '@type'  => $this->data->get('provider_type'),
                'name' => $this->data->get('provider_name'),
                'image' => $this->data->get('provider_image'),
                'telephone' => $this->data->get('phone'),
                'address' => $this->getPostalAddress()
            ]
        ];

        // Offer / Pricing
        if ((float) $this->data->get('offerPrice') > 0)
        {
            $content = array_merge($content, [
                'offers' => [
                    '@type'         => 'Offer',
                    'priceCurrency' => $this->data->get('currency', 'USD'),
                    'price'         => $this->data->get('offerPrice')
                ]
            ]);
        }

        return $content;
    }

    /**
     *  Constructs the Video Content Type
     *  https://developers.google.com/search/docs/data-types/videos
     *
     *  @return  array
     */
    private function contentTypeVideo()
    {
        if (empty($this->data->get('contentUrl')) && empty($this->data->get('embedUrl')))
        {
            return;
        }
        
        return [
            '@type'        => 'VideoObject',
            'name'         => $this->data->get('name'),
            'description'  => $this->data->get('description'),
            'thumbnailUrl' => $this->data->get('thumbnailUrl'),
            'uploadDate'   => $this->data->get('uploadDate'),
            'contentUrl'   => $this->data->get('contentUrl'),
            'embedURL'     => $this->data->get('embedUrl'),
            'transcript'   => $this->data->get('transcript')
        ];
    }

    /**
     * Generates the Job Posting Content Type
     * https://developers.google.com/search/docs/data-types/job-posting
     *
     * @return void
     */
    private function contentTypeJobPosting()
    {
        $json = [
            '@type' => 'JobPosting',
            'title' => $this->data->get('title'),
            'description' => $this->data->get('description'),
            'datePosted' => $this->data->get('datePublished'),
            'educationRequirements' => $this->data->get('educationRequirements'),
            'employmentType' => $this->data->get('employmenttype'),
            'industry' => $this->data->get('industry'),
            'jobLocation' => [
                '@type' => 'Place',
                'address' => $this->getPostalAddress()
            ],
            'hiringOrganization' => [
                '@type' => 'Organization',
                'name' => $this->data->get('hiring_oprganization_name'),
                'sameAs' => $this->data->get('hiring_oprganization_url'),
                'logo' => $this->data->get('hiring_organization_logo')
            ],
            'validThrough' => $this->data->get('valid_through')
        ];

        $salary = $this->data->get('salary');

        if ($salary > 0)
        {
            if (is_array($salary) && count($salary) > 1)
            {
                $salary_value = [
                    'value' => trim($salary[0]),
                    'minValue' => trim($salary[0]),
                    'maxValue' => trim($salary[1])
                ];
            } else
            {
                $salary_value = ['value' => $salary];
            }

            $json = array_merge($json, [
                'baseSalary' => [
                    '@type' => 'MonetaryAmount',
                    'currency' => $this->data->get('currency'),
                    'value' => [
                        '@type' => 'QuantitativeValue',
                        'unitText' => $this->data->get('salary_unit')
                    ]
                ],
            ]);

            $json = array_merge_recursive($json, [
                'baseSalary' => [
                    'value' => $salary_value
                ]
            ]);
        }

        return $json;
    }

    /**
     *  Constructs the Custom Code Content Type
     *
     *  @return  string    The custom code entered by user
     */
    private function contentTypeCustom_Code()
    {
        return $this->data->get('custom_code', '');
    }
    
    
    /**
     *  Appends the aggregateRating property to object
     *
     *  @param  array  &$content
     */
    private function addRating(&$content)
    {
        if (!$this->data->get('ratingValue') || !$this->data->get('reviewCount'))
        {
            return;
        }

        return $content = array_merge($content, [
            'aggregateRating' => [
                '@type'       => 'AggregateRating',
                'ratingValue' => $this->data->get('ratingValue'),
                'reviewCount' => $this->data->get('reviewCount'),
                'worstRating' => $this->data->get('worstRating', 0),
                'bestRating'  => $this->data->get('bestRating', 5)
            ]
        ]);
    }

    /**
     * Returns the PostalAddress type used in most of the content types
     *
     * @return array
     */
    private function getPostalAddress()
    {
        return [
            '@type'           => 'PostalAddress',
            'streetAddress'   => $this->data->get('streetAddress'),
            'addressCountry'  => $this->data->get('addressCountry'),
            'addressLocality' => $this->data->get('addressLocality'),
            'addressRegion'   => $this->data->get('addressRegion'),
            'postalCode'      => $this->data->get('postalCode')
        ];
    }

    /**
     *  Appends date properties to object
     *
     *  @param  array  &$content
     */
    private function addDate(&$content)
    {
        return $content = array_merge($content, [
            'datePublished' => $this->data->get('datePublished'),
            'dateCreated'   => $this->data->get('dateCreated'),
            'dateModified'  => $this->data->get('dateModified')
        ]);
    }

    /**
     * Adds the author property to the content.
     * 
     * @param   array  &$content
     * 
     * @return  void
     */
    private function addAuthor(&$content)
    {
        if ($this->data->get('authorName'))
        {
            $content = array_merge($content, [
                'author' => [
                    '@type' => $this->data->get('authorType'),
                    'name'  => $this->data->get('authorName'),
                    'url'   => $this->data->get('authorUrl')
                ]
            ]);
        }
    }
}GSD/MappingOptions.php000064400000014020152345574300010646 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace GSD;

defined('_JEXEC') or die('Restricted Access');

use GSD\Helper;
use Joomla\Registry\Registry;
use NRFramework\SmartTags;
use Joomla\CMS\Factory;

/**
 *  Support SmartTags in the Structured Data Item properties
 */
class MappingOptions
{
    /**
     * List of available mapping options
     *
     * @var array
     */
    public static $options = [
        'GSD_INTEGRATION' => [
            'gsd.item.id'           => 'ID',
            'gsd.item.alias'        => 'Alias',
            'gsd.item.headline'     => 'NR_TITLE',
            'gsd.item.description'  => 'NR_TEXT',
            'gsd.item.introtext'    => 'GSD_INTROTEXT',
            'gsd.item.fulltext'     => 'GSD_FULLTEXT',
			'gsd.item.image'        => 'NR_IMAGE',
			'gsd.item.imagetext'    => 'GSD_IMAGE_FROM_TEXT',
			'gsd.item.weight'       => 'GSD_PRODUCT_WEIGHT',
			'gsd.item.weightUnit'   => 'GSD_PRODUCT_WEIGHT_UNIT',
            'url'                   => 'NR_URL',
			'user.id'			    => 'Author ID',
            'user.name'             => 'Author Name',
            'user.firstname'        => 'Author First Name',
            'user.lastname'         => 'Author Last Name',
            'user.login'            => 'Author Username',
            'user.email'            => 'Author Email',
            'gsd.item.created'      => 'Date Created',
            'gsd.item.publish_up'   => 'GSD_DATE_PUBLISH_UP',
            'gsd.item.publish_down' => 'GSD_DATE_PUBLISH_DOWN',
            'gsd.item.modified'     => 'GSD_DATE_MODIFIED',
            'gsd.item.ratingValue'  => 'Rating Value',
            'gsd.item.reviewCount'  => 'Review Count',
            'gsd.item.metakey'      => 'Meta Keywords',	
            'gsd.item.metadesc'     => 'Meta Description'
        ],
        'Page' => [
            'page.title'     => 'Page Title',
            'page.browsertitle' => 'Browser Page Title',
            'page.desc'      => 'Page Meta Description',
            'page.keywords'  => 'Page Meta Keywords',		
			'page.lang'      => 'Page Language',
			'page.generator' => 'Page Generator'
        ],
        'Site Info' => [
			'gsd.sitename'  => 'Site Name',
            'gsd.siteurl'   => 'Site URL',
			'gsd.sitelogo'  => 'Site Logo',
            'site.email'    => 'Site Email'
        ]
    ];

    public static function make($string)
    {
        if (empty($string))
        {
            return;
        }

        return '{' . $string . '}';
    }

    /**
     *  Replaces Smart Tags in a snippet.
     *
     *  @param   Registry  $snippet  The snippet data
     *  @param   Registry  $payload  The tags to use
     *
     *  @return  Registry
     */
    public static function replace($snippet, $payload)
    {
        $payload = $payload->toArray();

        // Null property must be converted to an empty string in order to replace the respective Smart Tag.
        foreach ($payload as $key => $value)
        {
            if (!is_null($value))
            {
                continue;
            }

            $payload[$key] = '';
        }

        // Initialize SmartTags class
        $SmartTags = new SmartTags([
            'technology_tags' => false,
            'user' => isset($payload['created_by']) ? $payload['created_by'] : null]
        );
        
        // Add payload to collection
        $SmartTags->add($payload, 'gsd.item.');

        // Add extension global settings to collection
        $settings = [
            'sitename' => Helper::getSiteName(),
            'siteurl'  => Helper::getSiteURL(),
            'sitelogo' => Helper::getSiteLogo()
        ];
        $SmartTags->add($settings, 'gsd.');

        // Replace Smart Tags now
        $data = $SmartTags->replace($snippet->toArray());

        return new Registry($data);   
    }

    public static function prepare(&$properties)
    {
        foreach ($properties as $key => $property)
        {
            if (!is_object($property) || !isset($property->option))
            {
                continue;
            }

            switch ($property->option)
            {
                
                case '_css_selector_':
                    if ($property->css_selector)
                    {
                        $crawler = new \NRFramework\DOMCrawler();
                        $value = $crawler->readCSSSelectorField($property->css_selector);
                    }
                    break;
                

                case 'fixed':
                    if (in_array($key, ['author', 'publisher_name']))
                    {
                        if ($user = Factory::getUser($property->fixed))
                        {
                            $property->fixed = $user->name;
                        }
                    }

                    $value = $property->fixed;

                    break;
                case '_custom_':
                    $value = $property->custom;
                    break;
                case '_disabled_':
                    $value = false; 
                    break;
                default:
                    $value = self::make($property->option);
                    break;
            }

            $properties->set($key, $value);
        }
    }

    /**
     * Add mapping options to the collection
     *
     * @param [type] $options
     * @param [type] $newoptions
     * @param string $group_name
     * @param string $prefix
     *
     * @return void
     */
    public static function add(&$options, $newoptions, $group_name = 'GSD_CUSTOM_FIELDS', $prefix = 'gsd.item.cf.')
    {
		foreach ($newoptions as $key => $newoption)
		{
            $new_key = $prefix . $key;
            $newoptions[$new_key] = $newoption;
            unset($newoptions[$key]);
        }

        $options = array_merge_recursive($options, [$group_name => $newoptions]);
    }
}GSD/Migrator.php000064400000022773152345574300007501 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace GSD;

defined('_JEXEC') or die('Restricted Access');

use GSD\Helper;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\Table\Table;
use Joomla\CMS\Language\Multilanguage;

/**
 * Google Structured Data Migrator Helper
 */
class Migrator
{
    /**
     * Indicates the current installed version of the extension
     *
     * @var  string
     */
    protected $installedVersion;

    /**
     * Shorthand of the Joomla Application Object
     *
     * @var  object
     */
    protected $app;

    /**
     * Class constructor
     *
     * @param string $installedVersion  The version of the extension
     */
    public function __construct($installedVersion)
    {
		BaseDatabaseModel::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_gsd/models');
		Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_gsd/tables');

        $this->installedVersion = $installedVersion;
        $this->app = Factory::getApplication();
    }

    /**
     * The main method to run migrations
     *
     * @return void
     */
    public function run()
    {
        try
        {
            $this->checkAndAddAppviewColumn();
            $this->moveGlobalLocalBusinessToItems();
        } catch (\Throwable $th)
        {
            $this->app->enqueueMessage($th->getMessage(), 'error');
        }
    }

    /**
     * Since v4.4.0, the Local Business Schema is available as an indepedent Schema Type. Given than update, the Local Business options
     * available in the extension's configuration page are no longer needed and they are migrated as a structured data item in the Items section.
     *
     * @return mixed Null if the migration doesn't run, True if it does run.
     */
    public function moveGlobalLocalBusinessToItems()
    {
        // Local Business Content Type introduced in v4.4.0
		if (version_compare($this->installedVersion, '4.4.0', '>'))
		{
            return;
        }

        $params = Helper::getParams();

        if (!$params->get('businesslisting_enabled'))
        {
            return;
        }

        // Enable Menu Manager Integration
        $menu_manager_plugin = \NRFramework\Extension::get('menus', 'plugin', 'gsd');
        if ($menu_manager_plugin && !$menu_manager_plugin['enabled'])
        {
            $table = Table::getInstance('Extension', 'Joomla\\CMS\\Table\\');
            $table->load($menu_manager_plugin['extension_id']);
            $table->enabled = 1;
            $table->store();
        }

        // Get homepage menu item
        $menu = $this->app->getMenu('site');
        $lang = Factory::getLanguage();
        $home = Multilanguage::isEnabled() ? $menu->getDefault($lang->getTag()) : $menu->getDefault();
        $homepage_menuitem = (int) $home->id;

        $item = [
            'title' => 'Website Local Business',
            'contenttype' => 'localbusiness',
            'plugin' => 'menus',
            'state' => $homepage_menuitem ? 1 : 0,
            'note' => 'Moved from extension configruation page',
            'localbusiness' => [
                'type' => $params->get('businesslisting_type'),
                'name' => [
                    'option' => 'gsd.sitename'
                ],
                'image' => [
                    'option' => 'gsd.sitelogo'
                ],
                'telephone' => [
                    'option' => '_custom_',
                    'custom' => $params->get('businesslisting_telephone')
                ],
                'priceRange' => [
                    'option' => '_custom_',
                    'custom' => $params->get('price_range')
                ],
                'openinghours' => [
                    'option' => 'fixed',
                    'fixed'  => [
                        'option' => $params->get('businesslisting_hours_available'),
                        'monday' => [
                            'enabled' => $params->get('businesslisting_monday'),
                            'start' => $params->get('businesslisting_monday_start'),
                            'end' => $params->get('businesslisting_monday_end')
                        ],
                        'tuesday' => [
                            'enabled' => $params->get('businesslisting_tuesday'),
                            'start' => $params->get('businesslisting_tuesday_start'),
                            'end' => $params->get('businesslisting_tuesday_end')
                        ],
                        'wednesday' => [
                            'enabled' => $params->get('businesslisting_wednesday'),
                            'start' => $params->get('businesslisting_wednesday_start'),
                            'end' => $params->get('businesslisting_wednesday_end')
                        ],
                        'thursday' => [
                            'enabled' => $params->get('businesslisting_thursday'),
                            'start' => $params->get('businesslisting_thursday_start'),
                            'end' => $params->get('businesslisting_thursday_end')
                        ],
                        'friday' => [
                            'enabled' => $params->get('businesslisting_friday'),
                            'start' => $params->get('businesslisting_friday_start'),
                            'end' => $params->get('businesslisting_friday_end')
                        ],
                        'saturday' => [
                            'enabled' => $params->get('businesslisting_saturday'),
                            'start' => $params->get('businesslisting_saturday_start'),
                            'end' => $params->get('businesslisting_saturday_end')
                        ],
                        'sunday' => [
                            'enabled' => $params->get('businesslisting_sunday'),
                            'start' => $params->get('businesslisting_sunday_start'),
                            'end' => $params->get('businesslisting_sunday_end')
                        ]
                    ],
                ],
                'addressCountry' => [
                    'option' => 'fixed',
                    'fixed' => $params->get('businesslisting_address_country')
                ],
                'addressLocality' => [
                    'option' => '_custom_',
                    'custom' => $params->get('businesslisting_address_locality')
                ],
                'streetAddress' => [
                    'option' => '_custom_',
                    'custom' => $params->get('businesslisting_street_address')
                ],
                'addressRegion' => [
                    'option' => '_custom_',
                    'custom' => $params->get('businesslisting_address_region')
                ],
                'postalCode' => [
                    'option' => '_custom_',
                    'custom' => $params->get('businesslisting_postal_code')
                ],
                'geo' => [
                    'option' => '_custom_',
                    'custom' => $params->get('businesslisting_latlng')
                ],  
                'servesCuisine' => [
                    'option' => '_custom_',
                    'custom' => $params->get('servesCuisine')
                ]
            ],
            'assignments' => [
                'menu' => [
                    'assignment_state' => 1,
                    'selection' => [$homepage_menuitem]
                ]
            ]
        ];

        if (!$this->createItem($item))
        {
            return;
        }
        
        $this->app->enqueueMessage('Your Local Business Listing options previously found in the extension configuration page has been migrated as a Structured Data Item in the Items section.', 'warning');
        
        // To ensure the migration runs once, disable the Local Business option in the configuration
		$table = Table::getInstance('Config', 'GSDTable');
        $table->load('config');
        
        $p_ = json_decode($table->params);
        $p_->businesslisting_enabled = false;

        $table->params = json_encode($p_);
	    $table->store();
        
        return true;
    }

    /**
     * Create a new structured data item
     *
     * @param  array $params
     *
     * @return boolean
     */
    private function createItem($params)
    {
        $model = BaseDatabaseModel::getInstance('Item', 'GSDModel');
        $item = $model->validate(null, $params);
        return $model->save($item);
    }

    /**
     * The "appview" column was introduced in 5.1.0 and due to
     * the fact that we did not include it in the main "gsd" table
     * SQL file right away, some users may be missing it.
     * 
     * We check whether this column exists and if not, add it, otherwise, abort.
     * 
     * @return  void
     */
    private function checkAndAddAppviewColumn()
    {
        $db = Factory::getDBO();
        $query = "SHOW COLUMNS FROM `#__gsd` LIKE 'appview'";
        $db->setQuery($query);

        // Column exists
        if ($res = $db->loadResult())
        {
            return;
        }

        // Add column
        $sql = "ALTER TABLE `#__gsd` ADD `appview` VARCHAR(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '*' AFTER `plugin`";
        $db->setQuery($sql);
        $db->execute();
    }
}GSD/PluginBase.php000064400000035120152345574300007734 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

namespace GSD;

defined('_JEXEC') or die('Restricted access');

use GSD\Json;
use GSD\Helper;
use GSD\MappingOptions;
use NRFramework\Cache;
use NRFramework\Assignments;
use Joomla\Registry\Registry;
use Joomla\String\StringHelper;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Multilanguage;

/**
 *  Google Structured Data helper class
 */
class PluginBase extends CMSPlugin
{
    /**
     *  Auto load the plugin language file
     *
     *  @var  boolean
     */
    protected $autoloadLanguage = true;

    /**
     *  Joomla Application Object
     *
     *  @var  object
     */
    protected $app;

    /**
     *  The selected app view.
     *
     *  @var  string
     */
    protected $appview;

    /**
     *  Joomla Database Object
     *
     *  @var  object
     */
    protected $db;

    /**
     *  Holds all available snippets for the current active page.
     *
     *  @var  array
     */
    protected $snippets;

    /**
     *  Indicates the query string parameter name that is used by the front-end component
     *
     *  @var  string
     */
    protected $thingRequestIDName = 'id';

    /**
     *  Indicates the request variable name used by plugin's assosiated component
     *
     *  @var  string
     */
    protected $thingRequestViewVar = 'view';

    /**
     *  Plugin constructor
     *
     *  @param  mixed   &$subject
     *  @param  array   $config
     */
    public function __construct(&$subject, $config = [])
    {
        // Load main language file
        Factory::getLanguage()->load('plg_system_gsd', JPATH_PLUGINS . '/system/gsd');

        // execute parent constructor
        parent::__construct($subject, $config);
    }

    /**
     * Return a list of all supported views. 
     * 
     * While in most Apps we support 1 view, in Apps like the J-Business Directory where we support 3 views. The App View dropdown helps us tell what 
     * snippets should be rendered per view without the need for Conditions. 
     * 
     * The App View information helps us improve performance on the front-end and UX on the back-end. In detail using the App View we can:
     * 
     * 1. [Front-end] Fetch only the snippets based on the active view. 
     * 2. [Back-end] Filter the Mapping Dropdown options. (Eg: When marking up a Product page we don't need mapping options related to an Event page.)
     * 3. [Back-end] Filter displayed Conditions per view. (Eg: When marking up a Product page, we don't need Conditions related to Event pages.)
     *
     * @return array
     */
    public function advertiseSupportedViews()
    {
        $methods = get_class_methods($this);
        $supportedViews = [];

        foreach ($methods as $method)
        {
            if (strpos($method, 'view') !== 0)
            {
                continue;
            }

            $viewName = strtolower(str_replace('view', '', $method));

            $supportedViews[$viewName] = Text::_('PLG_GSD_' . strtoupper($this->_name) . '_VIEW_' . strtoupper($viewName));
        }

        return $supportedViews;
    }

    /**
     *  Event triggered to gather all available plugins.
     *  Mostly used by the dropdowns in the backend.
     *
     *  @param   boolean  $mustBeInstalled  If enabled, the assosiated component must be installed
     *
     *  @return  array
     */
    public function onGSDGetType($mustBeInstalled = true)
    {
        if ($mustBeInstalled && !\NRFramework\Extension::isInstalled($this->_name))
        {
            return;
        }

        return [
            'name'  => Text::_('PLG_GSD_' . strtoupper($this->_name) . '_ALIAS'),
            'alias' => $this->_name
        ];
    }

     /**
     *  Prepare form.
     *
     *  @param   Form  $form  The form to be altered.
     *  @param   mixed  $data  The associated data for the form.
     *
     *  @return  boolean
     */
    public function onContentPrepareForm($form, $data)
    {
        // Make sure we are on the right context
        if ($this->app->isClient('site') || $form->getName() != 'com_gsd.item')
        {
            return;
        }

        // When item is not saved yet, the $data variable is type of Array.
        $tempData = (object) $data;

        if (!isset($tempData->plugin) || is_null($tempData->plugin) || $tempData->plugin != $this->_name)
        {
            return;
        }

        $view = isset($tempData->appview) ? $tempData->appview : '';

        $this->appview = $view;
        
        $viewXMLName = !empty($view) && $view !== '*' ? $view : 'assignments';

        // The assignments XML file base
        $assignmentsXMLFileBase = JPATH_PLUGINS . '/gsd/' . $this->_name . '/form/';

        $assignmentsXML = $assignmentsXMLFileBase . $viewXMLName . '.xml';

        /**
         * The XML file can be found in the following files:
         * 
         * - {VIEW}.xml
         *      Used individually for each view to provide different assignments.
         * - assignments.xml
         *      Used by single-view integrations or multi-view integrations that offer
         *      the same assignments per view (i.e. J2Store).
         */

        // Check view-based XML
        if (!is_file($assignmentsXML))
        {
            $assignmentsXML = $assignmentsXMLFileBase . 'assignments.xml';
        
            // Check generic XML
            if (!is_file($assignmentsXML))
            {
                return;
            }
        }

        $form->loadFile($assignmentsXML, false);
    }
    
    /**
     *  The event triggered before the JSON markup be appended to the document.
     *
     *  @param   array  &$data   The JSON snippets to be appended to the document
     *
     *  @return  void
     */
    public function onGSDBeforeRender(&$data)
    {
        // Quick filtering on component check
        if (!$this->passContext())
        {
            return;
        }

        // Let's check if the plugin supports the current component's view.
        if (!$payload = $this->getPayload())
        {
            return;
        }

        // Now, let's see if we have valid snippets for the active page. If not abort.
        if (!$this->snippets = $this->getSnippets())
        {
            $this->log('No valid items found');
            return;
        }

        // Prepare snippets
        foreach ($this->snippets as $snippet)
        {
            // Here, the payload must be merged with the snippet data
            $jsonData = $this->preparePayload($snippet, $payload);

            // Create JSON
            $jsonClass = new Json($jsonData);
            $json = $jsonClass->generate();

            // Add json back to main data object
            $data[] = $json;
        }
    }

    /**
     *  Validate context to decide whether the plugin should run or not.
     *
     *  @return   bool
     */
    protected function passContext()
    {
        return Helper::getComponentAlias() == $this->_name;
    }

    /**
     *  Get Item's ID
     *
     *  @return  string
     */
    protected function getThingID()
    {
        return $this->app->input->getInt($this->thingRequestIDName);
    }

    /**
     *  Get component's items and validate conditions
     *
     *  @return  Mixed   Null if no items found, The valid items array on success
     */
    protected function getSnippets()
    {
        BaseDatabaseModel::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_gsd/models');

        $model = BaseDatabaseModel::getInstance('Items', 'GSDModel', ['ignore_request' => true]);
        $model->setState('filter.plugin', $this->_name);

        // Since we did not code any migration script, pass asterisk to match old rows as well.
        $model->setState('filter.appview', [$this->getView(), '*']); 
        
        $model->setState('filter.state', 1);

        if (Multilanguage::isEnabled())
        {
            $model->setState('filter.language', [Factory::getLanguage()->getTag(), '*']);
        }

        if (!$rows = $model->getItems())
        {
            return;
        }

        // Check publishing assignments for each item
        foreach ($rows as $key => $row)
        {
            if (!isset($row->assignments) || !is_object($row->assignments))
            {
                continue;
            }

            // Prepare assignments
            $assignmentsFound = [];

            foreach ($row->assignments as $alias => $assignment)
            {
                if ($assignment->assignment_state == '0')
                {
                    continue;
                }

                // Remove unwanted assignments added by Free Pro code blocks
                if (strpos($alias, '@'))
                {
                    continue;
                }

                // If user hasn't made any selection, skip the assignment.
                if (!isset($assignment->selection))
                {
                    continue;
                }

                // Comply with the new conditions requirements
                $condition = (object) [
                    'alias'  => $alias,
                    'value'  => $assignment->selection,
                    'params' => isset($assignment->params) ? $assignment->params : [],
                    'assignment_state' => $assignment->assignment_state
                ];

                // Pass with 'AND' matching method. Hence the assignment to first [0] cell.
                $assignmentsFound[0][] = $condition;
            }

            // Validate assignments
            if (!$pass = (new Assignments())->passAll($assignmentsFound))
            {
                $this->log('Item #' . $row->id . ' does not pass the conditions check');
                unset($rows[$key]);
            }
        }

        $items = array_map(function($row)
        {
            $contentType = $row->contenttype;

            // After we have selected an Integration and a Content Type and we hit Save,
            // the item needs to be re-saved in order to access the Content Type options.
            //
            // We need to find a way to auto-populate the Content Type with default data during 1st save.
            //
            // A possible approach would be: Upon clicking on the New button, we display a popup modal where
            // the user can choose a Content Type, an Integration and a Title for the structured data item.
            // Then they will be redirected to the item editing page with these data prefilled.
            // 
            // UPDATE 04/05/2022: Since we have changed the way the structured data item is saved using the Joomla Loader (state) this may be no longer an issue. It needs a check.
            $contentTypeData = property_exists($row, $contentType) ? $row->{$contentType} : [];

            $s = new Registry($contentTypeData);
            $s->set('contentType', $contentType);
            $s->set('snippet_id', $row->id);

            // Help troubleshooting by logging item ID.
            $this->log('ID: ' . $row->id);

            return $s;
        }, $rows);

        return $items;
    }

    /**
     *  Asks for data from the child plugin based on the active view name
     *
     *  @return  Registry  The payload Registry
     */
    protected function getPayload()
    {
        $view   = $this->getView();
        $method = 'view' . ucfirst($view);

        if (!$view || !method_exists($this, $method))
        {
            $this->log('View ' . $view . ' is not supported');
            return;
        }

        // Yeah. Let's call the method. 
        $payload = $this->$method();

        // We need a valid array
        if (!is_array($payload))
        {  
            $this->log('Invalid Payload Array');
            return;
        }

        // If the payload contains any objects, convert them to an associative array
        $payload = json_decode(json_encode($payload), true);

        // Convert payload to Registry object and return it
        return new Registry($payload);
    }

    /**
     *  Prepares the payload to be used in the JSON class
     *
     *  @return  string
     */
    private function preparePayload($snippet, $payload)
    {   
        $schema = \GSD\Schemas\Helper::getInstance($snippet['contentType']);
        $schema->onPayloadPrepare($payload); // Temporary workaround. See comments in the Custom_Code class.

        MappingOptions::prepare($snippet);

        // Create a new combined object by merging the snippet data into the payload
        // Note: In order to produce a valid merged object, payload's array keys should match the field names
        // as declared in the form's XML file.
        $p = clone $payload;
        $s = $p->merge($snippet, false);

        // Replace Smart Tags - This can be implemented with a Plugin
        $s = MappingOptions::replace($s, $payload);

        $prepareContent = Helper::getParams()->get('preparecontent', false);

        // Content Preparation
        if ($prepareContent)
        {
            $s['headline']    = $this->prepareText($s['headline']);
            $s['description'] = $this->prepareText($s['description']);
        }

        return $schema->setData($s)->get();
    }

    /**
     *  Get View Name
     *
     *  @return  string  Return the current executed view in the front-end
     */
    protected function getView()
    {
        return $this->app->input->get($this->thingRequestViewVar);
    }

    /**
     * Prepare given text with Content and Field plugins
     *
     * @param  string $text
     *
     * @return string 
     */
    private function prepareText($text)
    {
        if (!$text)
        {
            return;
        }
        
        return \Joomla\CMS\HTML\HTMLHelper::_('content.prepare', $text);
    }

    /**
     *  Log messages
     *
     *  @param   string  $message  The message to log
     *
     *  @return  void
     */
    protected function log($message)
    {
        Helper::log(Text::_('PLG_GSD_' . $this->_name . '_ALIAS') . ' - ' . $message);
    }
}GSD/PluginBaseArticle.php000064400000002113152345574300011234 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

namespace GSD;

defined('_JEXEC') or die('Restricted access');

/**
 *  Google Structured Data Product Plugin Base
 */
class PluginBaseArticle extends \GSD\PluginBase
{
	
	/**
	 * Listening to the onAfterRender Joomla event
	 *
	 * @return void
	 */
	public function onAfterRender()
	{
        // Make sure we are on the right context
        if ($this->app->isClient('administrator') || !$this->passContext() || !$this->params->get('remove_default_schema', true))
		{
            return;
		}
		
		// Remove the most common article-based schemas
		$schemas = [
			'BlogPosting',
			'Article',
			'NewsArticle',
			'Blog',
			'AggregateRating',
			'Person'
		];

		\GSD\SchemaCleaner::remove($schemas, false);
	}
	
}GSD/PluginBaseEvent.php000064400000005467152345574300010751 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

namespace GSD;

defined('_JEXEC') or die('Restricted access');

use GSD\PluginBase;
use GSD\MappingOptions;

/**
 *  Google Structured Data Product Plugin Base
 */
class PluginBaseEvent extends PluginBase
{
 	/**
	 * The MapOptions Backend Event. Triggered by the mappingoptions fields to help each integration add its own map options.
	 *  
	 * @param	string	$plugin
	 * @param	array	$options
	 *
	 * @return	void
	 */
    public function onMapOptions($plugin, &$options)
    {
		if ($plugin != $this->_name)
        {
			return;
		}
		
		$remove_options = [
			'modified',
			'created',
			'ratingValue',
			'reviewCount'
		];
		
		// Remove unsupported mapping options
		foreach ($remove_options as $key => $option)
		{
			unset($options['GSD_INTEGRATION']['gsd.item.' . $option]);
		}

		// Add Event based options
		$new_options = [
			'startdate'  	      => 'GSD_EVENT_START_DATE',
			'enddate'    	      => 'GSD_EVENT_END_DATE',
			'offerprice'          => 'GSD_EVENT_OFFER_PRICE',
			'locationname'        => 'GSD_EVENT_LOCATION_NAME',
			'locationaddress'     => 'GSD_EVENT_STREET_ADDRESS',
			'addressCountry'      => 'GSD_BUSINESSLISTING_ADDRESS_COUNTRY',
			'addressLocality'     => 'GSD_BUSINESSLISTING_ADDRESS_LOCALITY',
			'addressRegion'       => 'GSD_BUSINESSLISTING_ADDRESS_REGION',
			'postalCode'	      => 'GSD_BUSINESSLISTING_POSTAL_CODE',
			'offercurrency'       => 'GSD_PRODUCT_OFFER_CURRENCY',
			'offerinventorylevel' => 'GSD_EVENT_INVENTORY_LEVEL',
			'offerstartdate'      => 'GSD_EVENT_AVAILABILITY_START_DATE',
			'organizerType'		  => 'GSD_EVENT_ORGANIZER_TYPE',
			'organizerName'		  => 'GSD_EVENT_ORGANIZER_NAME',
			'organizerURL'		  => 'GSD_EVENT_ORGANIZER_URL',
			'performerType'		  => 'GSD_EVENT_PERFORMER_TYPE',
			'performerName'		  => 'GSD_EVENT_PERFORMER_NAME',
			'performerURL'		  => 'GSD_EVENT_PERFORMER_URL'
		];

		MappingOptions::add($options, $new_options, 'GSD_INTEGRATION', 'gsd.item.');
	}

	/**
	 * Remove 3rd party structured data
	 *
	 * @return void
	 */
	public function onAfterRender()
	{
        // Make sure we are on the right context
        if ($this->app->isClient('Administrator') || !$this->passContext() || !$this->params->get('remove_default_schema', true))
		{
            return;
		}
		
		// Remove the most common event-based schemas
		$schemas = [
			'Event',
			'Place',
			'PostalAddress',
			'GeoCoordinates',
		];

		\GSD\SchemaCleaner::remove($schemas);
	}
}GSD/PluginBaseProduct.php000064400000003606152345574300011301 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

namespace GSD;

defined('_JEXEC') or die('Restricted access');

use GSD\PluginBase;
use GSD\MappingOptions;

/**
 *  Google Structured Data Product Plugin Base
 */
class PluginBaseProduct extends PluginBase
{
    /**
	 * The MapOptions Backend Event. Triggered by the mappingoptions fields to help each integration add its own map options.
	 *  
	 * @param	string	$plugin
	 * @param	array	$options
	 *
	 * @return	void
	 */
    public function onMapOptions($plugin, &$options)
    {
		if ($plugin != $this->_name)
        {
			return;
        }
        
		$new_options = [
			'sku'        => 'SKU',
			'mpn'        => 'MPN',
			'brand'      => 'GSD_PRODUCT_BRAND_NAME',
			'offerprice' => 'GSD_PRODUCT_OFFER_PRICE',
			'currency'   => 'GSD_PRODUCT_OFFER_CURRENCY',
			'offerAvailability' => 'GSD_PRODUCT_AVAILABILITY'
        ];

		MappingOptions::add($options, $new_options, 'GSD_INTEGRATION', 'gsd.item.');
	}
	
 	/**
     *  Asks for data from the child plugin based on the active view name
     *
     *  @return  Registry  The payload Registry
     */
    protected function getPayload()
    {
		if (!$payload = parent::getPayload())
		{
			return;
		}

		$schema_prefix = 'https://schema.org/';

		// Add offerAvailability property
		if (!$payload->offsetExists('offerAvailability'))
		{
			$available = method_exists($this, 'productIsAvailable') ? $this->productIsAvailable() : true;
			$payload->set('offerAvailability', $schema_prefix . ($available ? 'InStock' : 'OutOfStock'));
		}

		return $payload;
	}
}GSD/SchemaCleaner.php000064400000013410152345574300010373 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace GSD;

defined('_JEXEC') or die('Restricted Access');

use Joomla\String\StringHelper;
use Joomla\CMS\Factory;

class SchemaCleaner
{
    /**
	 * Remove JSON-LD Structured Data injected by the integration or 3rd party extensions.
	 * 
	 * Note: This method should be called either in the onAfterRender or in the onAfterInitialize Events when the document's buffer is available.
	 *
	 * @param	mixed	$schema_type		Search and remove only specific schema types
	 * @param	mixed	$remove_json		If true, the JSON-based markup will be removed from the page
	 * @param	mixed	$remove_microdata	If true, the microdata-based will be removed from the page
	 *
	 * @return	void
	 */
	public static function remove($schema_type, $remove_json = true, $remove_microdata = true)
	{
		if (empty($schema_type))
		{
			return;
		}

		$app = Factory::getApplication();

		// Get document buffer
        $body = $app->getBody();

		$replacements_count = 0;

        $schema_types = (array) $schema_type;
        
        // Search and remove JSON-LD scripts
		if ($remove_json)
		{
            foreach ($schema_types as $schema_type)
            {
                $replacements_count += self::removeJSONSchema($body, strtolower($schema_type));
            }
		}
		
		// Search and remove microdata
		if ($remove_microdata)
		{
            foreach ($schema_types as $schema_type)
            {

                $replacements_count += self::removeMicrodata($body, strtolower($schema_type));
            }
		}

        // If no replacements made, exit.
        if ($replacements_count == 0)
        {
            return;
		}
		
        // Set the new document body back.
		$app->setBody($body);
	}

	/**
	 * Remove microdata from a string
	 *
	 * @param	string	$text			The text to search for
	 * @param	mixed	$schema_type	Search and remove only specific schema types
	 *
	 * @return	integer	
	 */
	private static function removeMicrodata(&$text, $schema_type = null)
	{
        // Simple check to decide whether the plugin should procceed or not.
        if (StringHelper::strpos($text, 'itemtype') === false)
        {
            return;
		}

		// Base replacement pattern
		// We do not include itemprop property here as some components renders the element
		// like itemscope itemtype="http://schema.org/" or itemscope="" itemtype="http://schema.org/"
		$patterns = ['/(itemscope)? itemtype=(\'|")?http(s?):\/\/(www.)?schema.org\/' . $schema_type . '(\'|")?/msi'];

		if ($schema_type == 'all')
		{
			$patterns = [
				'/(itemscope)? itemtype=(\'|")?http(s?):\/\/(.*?)schema.org\/(.*?(\'|"))(\'|")?/msi',
				'/<meta(.*?)(itemscope|itemprop)(.*?)\/?>/',
				'/itemprop=("|\')(.*?)("|\')/'
			];
		}

        // Extra rules for the Event type
		if ($schema_type == 'event')
		{
			$extra_patterns = [
				'/<meta itemprop="(url|startDate|addressRegion|postalCode|latitude|longitude|streetAddress|addressLocality)"[^>]+>/'
			];

			$patterns = array_merge($patterns, $extra_patterns);
        }


        // Extra rules for the Article type
		if ($schema_type == 'article')
		{
			$extra_patterns = [
				'/itemprop="(url|name|author|headline|image|keywords|articleBody|datePublished)"/',
				'/<meta itemprop="(inLanguage|datePublished)"[^>]+>/'
			];

			$patterns = array_merge($patterns, $extra_patterns);
        }

        // Extra rules for the Product type
		if ($schema_type == 'product')
		{
			$extra_patterns = [
				'/<meta itemprop="(price|priceCurrency)"[^>]+>/',
				'/itemprop="(sku|description|offers|name)"/',
				'/<link itemprop="availability" href="http(s?):\/\/schema.org\/InStock" \/>/'
			];

			$patterns = array_merge($patterns, $extra_patterns);
        }

        // Extra rules for the Breadcrumbs type
        if ($schema_type == 'breadcrumblist')
		{
			$extra_patterns = [
				'/itemprop="(itemListElement|position|item)"/',
				'/itemscope itemtype="http(s?):\/\/schema.org\/ListItem"/'
			];

			$patterns = array_merge($patterns, $extra_patterns);
		}
		
        // Extra rules for the AggregateRating type
        if ($schema_type == 'aggregaterating')
		{
			$extra_patterns = [
				'/itemprop="(aggregateRating|ratingValue|bestRating)"/',
				'/<meta itemprop="(ratingCount|bestRating|worstRating)"[^>]+>/',
			];

			$patterns = array_merge($patterns, $extra_patterns);
		}

		// Do the replacements and return the number of replacements
		$text = preg_replace($patterns, '', $text, -1, $count);

		return $count;
	}
	
	 /**
	  * Remove JSON-LD scripts from a string
	  *
	  * @param	string	$text			The text to search for
	  * @param	mixed	$schema_type	Search and remove only specific schema types
	  *
	  * @return	integer	
	  */
    private static function removeJSONSchema(&$text, $schema_type = null)
	{
		// Simple check to decide whether we should procceed or not.
		if (StringHelper::strpos($text, '//schema.org/') === false)
		{
			return;
		}

		$re = '/<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/msi';

		preg_match_all($re, $text, $matches, PREG_SET_ORDER, 0);

		if (!$matches)
		{
			return;
		}

		$replacements_count = 0;

        foreach ($matches as $match)
        {
			// Ignore our scripts
			if (strpos($match[0], 'data-type="gsd"') !== false)
			{
				continue;
			}

			// If we are searching for a specific schema type, make sure it exists.
			if (!is_null($schema_type) && !preg_match('/"@type"\s*:\s*"' . $schema_type . '"/si', $match[1]))
			{
				continue;
			}

			$text = str_replace($match[0], '', $text);
			
			$replacements_count++;
		}
		
		return $replacements_count;
	}
}controllers/config.php000064400000001050152345574300011074 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\MVC\Controller\FormController;

/**
 * Config controller class
 */
class GSDControllerConfig extends FormController
{
	protected $text_prefix = 'GSD';
}
controllers/item.php000064400000001044152345574300010570 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\MVC\Controller\FormController;

/**
 * Item controller class
 */
class GSDControllerItem extends FormController
{
	protected $text_prefix = 'GSD';
}
controllers/items.php000064400000003244152345574300010757 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');
 
use Joomla\CMS\MVC\Controller\AdminController;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;

/**
 * Items controller class.
 */
class GSDControllerItems extends AdminController
{
	protected $text_prefix = 'GSD';

	/**
	 * Method to get a model object, loading it if required.
	 *
	 * @param   string  $name    The model name. Optional.
	 * @param   string  $prefix  The class prefix. Optional.
	 * @param   array   $config  Configuration array for model. Optional.
	 *
	 * @return  Joomla\CMS\MVC\Model\BaseDatabaseModel  The model.
	 *
	 * @since   1.6
	 */
	public function getModel($name = 'Item', $prefix = 'GSDModel', $config = array('ignore_request' => true))
	{
		return parent::getModel($name, $prefix, $config);
	}

	/**
	 *  Copy items specified by array cid and set Redirection to the list of items
	 *
	 *  @return  void
	 */
	public function duplicate()
	{
		$app = Factory::getApplication();
		$ids = $app->input->get('cid', array(), 'array');

		// Get the model.
		$model = $this->getModel('Item');

  		foreach ($ids as $id)
        {
            $model->copy($id);
        }

		$app->enqueueMessage(Text::sprintf('GSD_CAMPAIGN_N_ITEMS_COPIED', count($ids)));
        $app->redirect('index.php?option=com_gsd&view=items');
	}
}helpers/dashboard.php000064400000007165152345574300010667 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

defined('_JEXEC') or die('Restricted Access');

use GSD\Helper;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Filesystem\Folder;
use Joomla\CMS\Plugin\PluginHelper;

/**
 *  Google Structured Data Dashboard Helper Class
 */
class GSDDashboard
{
	private static $items;
	private static $integrations;

	public static function getStats()
	{
		BaseDatabaseModel::addIncludePath(JPATH_COMPONENT_ADMINISTRATOR . '/models/');

		$siteData     = self::getSiteData();
		$integrations = self::getIntegrations();

		return array(
			'items'               => self::getItemsPerContentType() ?: array(),
			'itemsCount'          => count(self::getItems()),
			'dynamicitemsCount'   => 0,
			'siteData'        	  => $siteData ?: array(),
			'siteDataEnabled' 	  => $siteData ? count(array_filter($siteData)) : 0,
			'integrations'        => $integrations,
			'integrationsEnabled' => $integrations ? count(array_filter($integrations)) : 0
		);
	}

	public static function getIntegrations()
	{
		if (is_array(self::$integrations))
		{
			return self::$integrations;
		}

		$integrations = Folder::folders(JPATH_PLUGINS . '/gsd');
		$integrations = is_array($integrations) ? array_flip($integrations) : array();

		foreach ($integrations as $key => $integration)
		{
			$integrations[$key] = PluginHelper::isEnabled('gsd', $key);
		}

		return (self::$integrations = $integrations);
	}

	public static function getSiteData()
	{
		$params = Helper::getParams();

		// Determine if Social Profiles snippet is enabled
		$socialProfileIsEnabled = false;
		$socialProfileFields = array(
			'facebook',
			'twitter',
			'googleplus',
			'instagram',
			'youtube',
			'linkedin',
			'pinterest',
			'soundcloud',
			'tumblr',
			'other'
		);

		foreach ($socialProfileFields as $key => $field)
		{
			if ($params->get('socialprofiles_' . $field, null))
			{
				$socialProfileIsEnabled = true;
				break;
			}
		}

		return array(
			'GSD_SITENAME_NAME'        => (bool) $params->get('sitename_enabled', 1),
			'GSD_BREADCRUMBS'          => (bool) $params->get('breadcrumbs_enabled', 1),
			'GSD_SITELINKS_NAME'       => (bool) $params->get('sitelinks_enabled', 1),
			'GSD_LOGO'                 => (bool) $params->get('logo_file', 0) ? 1 : 0,
			'GSD_SOCIALPROFILES'       => (bool) $socialProfileIsEnabled
		);
	}

	public static function getItems()
	{
		if (is_array(self::$items))
		{
			return self::$items;
		}

		$model = BaseDatabaseModel::getInstance('Items', 'GSDModel', array('ignore_request' => true));
		$model->setState('filter.state', 1);

		return (self::$items = $model->getItems());
	}

	public static function getItemsPerContentType()
	{
		$items = self::getItems();
		$overview = array();

		foreach ($items as $key => $item)
		{
			$type  = $item->contenttype;
			$count = !isset($overview[$type]) ? 1 : $overview[$type] + 1;
			$overview[$type] = $count;
		}

		$result = array();

		foreach (Helper::getContentTypes() as $value)
		{
			$count = 0;
			$share = 0;

			if (isset($overview[$value]))
			{
				$count = $overview[$value];
				$share = intval($overview[$value] / count($items) * 100);
			}

			$result[$value] = array(
				'count' => $count,
				'share' => $share
			);
		}
		
		return $result;
	}
}helpers/migrator.php000064400000027507152345574300010566 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

defined('_JEXEC') or die('Restricted Access');

use Joomla\Registry\Registry;
use Joomla\CMS\Factory;
use Joomla\Database\DatabaseDriver;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Filesystem\File;
use Joomla\CMS\Table\Table;

/**
 *  Google Structured Data Migrator
 *
 *  The purpose of this class is to migrate data and offer a non-breaking update from 2.1 to 3.0 version.
 *  Migration should be run on 3.0 versions and below only.
 */
class GSDMigrator
{
	/**
	 *  Log messages array
	 *
	 *  @var  array
	 */
	public $log;

	/**
	 *  If enabled, the main GSD table will be flushed before starting the migration
	 *
	 *  @var  boolean
	 */
	private $flushTable;

	/**
	 *  Override the database object for testing purposes and sharing data between different databases
	 *
	 *  @var  mixed
	 */
	private $db;

	/**
	 *  Supported migration component names
	 *
	 *  @var  array
	 */
	private $migrateComponents = array(
		'content',
		'k2'
	);

	/**
	 *  The name of the System Plugin
	 *
	 *  @var  string
	 */
	private $systemPluginName = 'plg_system_gsd';

	/**
	 *  Class Constructor
	 *
	 *  @param  boolean  $flushTable  Indicates wether the table will be flushed or not
	 *  @param  mixed    $db          Database options
	 */
	public function __construct($flushTable = false, $db = null)
	{
		$this->db = $db;
		$this->flushTable = $flushTable;
		$this->log = array('Migrator: Started');
	}

	/**
	 *  Returns a database instance
	 *
	 *  @return  JDatabase object
	 */
	private function getDB()
	{
		return is_array($this->db) ? DatabaseDriver::getInstance($this->db) : Factory::getDBo();
	}

	/**
	 *  Start the migration process
	 *
	 *  @return  void
	 */
	public function start()
	{
		// Flush the main GSD table
		if ($this->flushTable)
		{
			$this->flushTable();
		}

		// Start the migration process
		$this->migrateSnippets();
		$this->migrateParams();

		// Save log to a file
		$this->saveLog();
	}

	/**
	 *  Save log to a file
	 *
	 *  @return  void
	 */
	private function saveLog()
	{
		array_unshift(
			$this->log,
			str_repeat('-', 19),
			Factory::getDate(),
			str_repeat('-', 19)
		);

		$log = implode(PHP_EOL, $this->log) . PHP_EOL . PHP_EOL;

		try
		{
			$logFile = __DIR__ . '/migrator/migrator.log';

			File::write($logFile, $log);
		}
		catch (Exception $e)
		{
			$this->log[] = "Can't write log file" . $e->getMessage();
		}
	}

	/**
	 *  Move parameters from system plugin to component
	 *
	 *  @return  void
	 */
	private function migrateParams()
	{
		$this->log[] = 'Params: Migration Started.';

		$pluginParams = $this->getPluginParams();

		// Make sure we have valid parameters. Otherwise abort.
		if (empty($pluginParams) || strpos($pluginParams, 'sitename_enabled') === false)
		{
			$this->log[] = 'Params: Migration Skipped. Probably already migrated.';
			return;
		}

		// Store parameters to gsd_config
		Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_gsd/tables');

		$table = Table::getInstance('Config', 'GSDTable');
		$table->load('config');
		$table->params = $pluginParams;

		if (!$table->store())
		{
			$this->log[] = 'Params: Cannot store params to config table.';
			return;
		}

		// Backup parameters to a file
		$this->backup('params', $pluginParams);		

		// Remove parameters from plg_system_gsd
		if (!$this->removePluginParams())
		{
			$this->log[] = 'Params: Unable to remove params from system plugin.';
			return;
		}

		// Success!
		$this->log[] = "Params: Migration finished.";
		return true;
	}

	/**
	 *  Move snippet data from each component to com_gsd
	 *
	 *  Note: Items with the snippet property disabled are skipped.
	 *
	 *  @return  void
	 */
	private function migrateSnippets()
	{
		foreach ($this->migrateComponents as $component)
		{
			// Skip component if it's not installed.
			if (!\NRFramework\Functions::extensionInstalled($component))
			{
				$this->log[] = "Snippets - $component: Not installed. Skipped.";
				continue;
			}

			// Get component's snippets from database
			if (!$snippets = $this->getComponentSnippets($component))
			{
				$this->log[] = "Snippets - $component: No snippets found. Skipped.";
				continue;
			}

			// Backup Snippets
			$this->backup($component . '_snippets', json_encode($snippets, JSON_PRETTY_PRINT));

			// Loop through each snippet, validate it and save it to gsd table
			foreach ($snippets as $snippet)
			{
				$params = $this->validateSnippetParams($snippet->params);

				// Remove parameters from component's item
				$this->removeComponentSnippet($component, $snippet->id);
				
				if (is_null($params))
				{
					$this->log[] = "Snippets - $component: Item #$snippet->id skipped.";
					continue;
				}

				// Prepare data array
				$data = array(
					'thing'  => $snippet->id,
					'plugin' => $component,
					'params' => json_encode($params),
					'state'  => 1
				);

				// Load GSD model and save the data into the database
				BaseDatabaseModel::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_gsd/models');
        		Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_gsd/tables');

				$model  = BaseDatabaseModel::getInstance('Item', 'GSDModel');
				$item   = $model->validate(null, $data);

				$result = $model->save($item);
				
				$this->log[] = "Snippets - $component: Item #$snippet->id transfered.";
			}

			$this->log[] = "Snippets - $component: Migration finished.";
		}
	}

	/**
	 *  Validates snippets parameters and adjusts them to be valid with the new requirements
	 *
	 *  Checks:
	 *  
	 *  1. Removes gsd offset
	 *  2. Renames snippet property to contenttype
	 *  3. Alters rating property
	 *  4. Alters image property
	 *
	 *  @param   object  $data  The snippet params object
	 *
	 *  @return  array          The new snippet params array
	 */
	private function validateSnippetParams($data)
	{
		// Prepare Params
		$data = new Registry($data);

		// Abort if we don't have the required property
		if (!$data->offsetExists('gsd'))
		{
			return;
		}

		$p = $data->offsetGet('gsd');

		// Abort if there is no selected content type
		if (!isset($p->snippet) || $p->snippet == '0')
		{
			return;
		}

		$contentType = $p->snippet;

		//Shorthand for snippet data
		$s = $p->$contentType;

		/**
		 *  Rating Properties
		 */
		if ($contentType != 'review' && isset($s->ratingValue) && isset($s->reviewCount))
		{
			$ratingValue = (float) $s->ratingValue;
			$reviewCount = (int) $s->reviewCount;

			// If ratingValue and reviewCount are greater than 0 means that we have
			// a valid custom rating data. Otherwise we disable the rating.
			$ratingOption = ($ratingValue > 0 && $reviewCount > 0) ? 2 : 0;

			// Add rating data to object
			$s->rating = array(
				'option'      => $ratingOption,
				'ratingValue' => $ratingValue,
				'reviewCount' => $reviewCount
			);
		}

		// On Reviews Content Type we have a ratingValue property only
		if ($contentType == 'review' && isset($s->ratingValue) && (float) $s->ratingValue > 0)
		{
			$s->rating = array(
				'option'      => '2',
				'ratingValue' => $s->ratingValue
			);
		}

		// Unset old properties
		unset($s->ratingValue);
		unset($s->reviewCount);

		/**
		 *  Image Property
		 *
		 *  Since the image option was available only in the Reviews Content Type we are going to
		 *  check only that content type. All the other content types will be set to Inherit.
		 */
		if ($contentType == 'review' && isset($s->image))
		{
			$s->image = array(
				'option' => $s->image,
				'file'   => $s->imgupload,
				'url'    => $s->imgurl
			);

			// Unset old properties
			unset($s->imgupload);
			unset($s->imgurl);
		}

		$data = array(
			'contenttype' => $contentType,
			$contentType  => $s,
			'customcode'  => $p->customcode
		);

		return $data;
	}

	/**
	 *  Get snippets from database
	 *
	 *  @param   string  $component  The component's alias name
	 *
	 *  @return  object
	 */
	private function getComponentSnippets($component)
	{
		$db    = $this->getDB();
		$query = $db->getQuery(true);

		if ($component == 'k2')
		{
			$query
				->select('id, plugins as params')
				->from('#__k2_items')
				->where($db->quoteName('plugins') . ' LIKE ' . $db->quote('%gsd%'));
		}

		if ($component == 'content')
		{
			$query
		    	->select('id, attribs as params')
		    	->from('#__content')
		    	->where($db->quoteName('attribs') . ' LIKE ' . $db->quote('%gsd%'))
		    	->where($db->quoteName('state') . ' IN (0,1,2)');
		}

		$db->setQuery($query);

		return $db->loadObjectList();
	}

	/**
	 *  Remove snippet parameters from component's item
	 *
	 *  @param   integer  $id  The item's primary key
	 *
	 *  @return  bool
	 */
	public function removeComponentSnippet($component, $id)
	{
		if (!in_array($component, $this->migrateComponents))
		{
			return;
		}

		if ($component == 'content')
		{
			$column = 'attribs';
			$table  = 'content';
		}

		if ($component == 'k2')
		{
			$column = 'plugins';
			$table  = 'k2_items';
		}

		$db = $this->getDB();

        $params = $db->setQuery(
            $db->getQuery(true)
                ->select($db->quoteName($column))
                ->from($db->quoteName('#__' . $table))
                ->where($db->quoteName('id') . ' = ' . $id)
        )->loadResult();

        $params = json_decode($params);

        if (!isset($params->gsd))
        {
        	return;
        }

        unset($params->gsd);

        $db->setQuery(
        	$db->getQuery(true)
			->update($db->quoteName('#__' . $table))
			->set($db->quoteName($column) . ' = ' . $db->quote(json_encode($params)))
			->where($db->quoteName('id') . ' = ' . $id)
        );

        return $db->execute();
	}

	/**
	 *  Get GSD System Plugin Parameters
	 *
	 *  @return  mixed    Registry object on success, false on failure
	 */
	private function getPluginParams()
	{
		$db = $this->getDB();

        return $db->setQuery(
            $db->getQuery(true)
                ->select('params')
                ->from($db->quoteName('#__extensions'))
                ->where($db->quoteName('name') . ' = ' . $db->quote($this->systemPluginName))
        )->loadResult();
	}

	/**
	 *  Removes saved parameters from system plugin in order to avoid future re-migration.
	 *
	 *  @return  bool
	 */
	private function removePluginParams()
	{
		$db = $this->getDB();
		 
        $db->setQuery(
        	$db->getQuery(true)
				->update($db->quoteName('#__extensions'))
				->set(array($db->quoteName('params') . ' = ' . $db->quote('')))
				->where(array($db->quoteName('name') . ' = ' . $db->quote($this->systemPluginName)))
        );
		 
		return $db->execute();
	}

	/**
	 *  Remove all records from local table
	 *
	 *  @return  void
	 */
	private function flushTable()
	{
		$this->log[] = "Migrator: Flushing GSD main table.";

		$db = Factory::getDBo();
		$db->setQuery('TRUNCATE #__gsd');
		$db->execute();
	}

	/**
	 *  Writes data to a backup file
	 *
	 *  @param   string  $filename  The filename
	 *  @param   string  $data      The data to be written into the file
	 *
	 *  @return  void
	 */
	private function backup($filename, $data)
	{
		try
		{
			File::write(__DIR__ . '/migrator/backup/' . $filename . '.bak', $data);
		}
		catch (Exception $e)
		{
			$this->log[] = "Can't write backup file: $filename" . $e->getMessage();
		}
	}
}helpers/migrator4.php000064400000035462152345574300010651 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

defined('_JEXEC') or die('Restricted Access');

use Joomla\Registry\Registry;
use Joomla\CMS\Table\Table;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\Factory;
use Joomla\Database\DatabaseDriver;

/**
 *  Google Structured Data Migrator
 *
 *  The purpose of this class is to migrate data and offer a non-breaking update from v3.1.9 and v3.2.0 (dev) to v4.0.0.
 * 	
 * 	Changes
 *  - Adds 'title' column to gsd table
 *  - Adds 'contenttype' column to gsd table
 *  - Adds 'note' column to gsd table
 *  - Removes 'thing' column from gsd table
 *  - Removes Dynamic Items completely
 *  - Adds missing override_property introduced after 3.2.0 came out
 *  - Fixes brand property
 *  - Fixes new custom code path
 *  - Renames 'categories' assignment property name to 'category'
 */
class GSDMigrator4
{
	/**
	 *  Override the database object for testing purposes and sharing data between different databases
	 *
	 *  @var  mixed
	 */
	private $db;

	/**
	 * The extension's version before the update
	 *
	 * @var string
	 */
	private $version;

	 /**
	  * Class constructor
	  *
	  * @param string $version      The extension's version before update
	  * @param mixed $db			Database options
	  */
	public function __construct($version = null, $db = null)
	{
		// Load GSD model
		BaseDatabaseModel::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_gsd/models');
		Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_gsd/tables');

		$this->version = $version;
		$this->db = $db;
	}

	/**
	 *  Returns a database instance
	 *
	 *  @return  JDatabase object
	 */
	private function getDB()
	{
		return is_array($this->db) ? DatabaseDriver::getInstance($this->db) : Factory::getDBo();
	}

	/**
	 *  Start the migration process
	 *
	 *  @return  void
	 */
	public function start()
	{
		$app = Factory::getApplication();

		// Migrate Items
		$total_items = $this->migrateItems();

		if ($total_items > 0)
		{
			$app->enqueueMessage(
				$total_items . ' Structured Data Items have been updated to comply with the new extension requirements. '
			, 'notice');
		}

		// Migrate Dynamic Items
		$total_dynamic_items = $this->migrateDynamicItems();
		if ($total_dynamic_items > 0)
		{
			$app->enqueueMessage(
				'The Dynamic Items section introduced in the v3.2.0 developer release is no longer available. 
				To make things simpler and the structured data process faster, we decided to merge the Dynamic Items section
				with the old Items Section and manage all our structured in one place. 
				Therefore, your <b>' . $total_dynamic_items . '</b> Dynamic Items are now available in the Items section.'
			,'warning');
		}

		// Migrate Auto-Mode
		if ($automode_plugins = $this->migrateAutoMode())
		{
			foreach ($automode_plugins as $key => $plugin)
			{
				$plugin = ucfirst($plugin);
				$app->enqueueMessage('The <b>' . $plugin . ' Auto Mode</b> option has been removed. To keep the Product Structured Data generated on <b>all ' . $plugin . ' product pages</b>, we have automatically created the respective Item in the Items section.', 'warning');
			}
		}

		// Migrate Mapping Options
		$migrate_mapping_options = $this->migrateMappingOptions();
	}

	/**
	 * Transform content type properites in the database based on the new MappingOptions field.
	 *
	 * @return void
	 */
	public function migrateMappingOptions()
	{
		$db = $this->getDB();
		$query = $db->getQuery(true);
		$query
			->select('*')
			->from('#__gsd')
			->where('state >= 0');
		$db->setQuery($query);

		if (!$items = $db->loadObjectList())
		{
			return;
		}
		
		$total_items_migrated = 0;

		foreach ($items as $key => $item)
		{
			$type = $item->contenttype;

			if (in_array($type, ['custom_code', 'jobposting']))
			{
				continue;
			}

			$params = json_decode($item->params);
			
			// If params type offset doesn't exist, the item is probably not saved after the content type selection.
			if (!isset($params->$type))
			{
				continue;
			}

			// If headline.option does exist then the item is already migrated.
			if (isset($params->$type->headline->option))
			{
				continue;
			}

			$properties = $params->$type;
			$properties_new = [];

			foreach ($properties as $key => $property)
			{
				$skip_properties = [
					'multiple', // Fact Check
				];

				if (in_array($key, $skip_properties))
				{
					$properties_new[$key] = $property;
					continue;
				}

				$option_ = null;

				if (is_object($property))
				{
					switch ($key)
					{
						case 'author':
							switch ($property->option)
							{
								case '0':
									$option_ = 'user.name';
									break;
								case '1':
									$option_ = 'fixed';
									$fixed_  = $property->user;
									break;
								case '2':
									$option_ = '_custom_';
									$custom_ = $property->custom;
									break;
							}
							break;
						case 'image':
							switch ($property->option)
							{
								case '1': // Inherit
									$option_ = 'gsd.item.image';
									break;
								case '2': // Upload
									$option_ = 'fixed';
									$fixed_  = $property->file;
									break;
								case '3': // Custom
									$option_ = '_custom_';
									$custom_ = $property->url;
									break;
							}
							break;
						case 'rating':
							switch ($property->option)
							{
								case '0': // Disabled
									$properties_new['rating_value'] = [
										'option' => '_custom_',
										'custom' => '0',
									];
									break;
								case '1': // Inherit
									$properties_new['rating_value'] = [
										'option' => 'gsd.item.ratingValue',
									];
									$properties_new['review_count'] = [
										'option' => 'gsd.item.reviewCount',
									];
									continue;
									break;
								case '2': // Custom
									$properties_new['rating_value'] = [
										'option' => '_custom_',
										'custom' => $property->ratingValue,
									];

									if (isset($property->reviewCount))
									{
										$properties_new['review_count'] = [
											'option' => '_custom_',
											'custom' => $property->reviewCount,
										];
									}

									break;
							}
							break;
					}
				}

				if (is_string($property))
				{
					$fixed_  = '';
					$property = trim($property);

					// Some values however belong to fixed values.
					$fixed_property_values = [
						'offerAvailability',
						'offerItemCondition',
						'currency',
						'offerCurrency',
						'claimAuthorType',
						'factcheckRating',
						'performerType'
					];

					if (in_array($key, $fixed_property_values) && !empty($property))
					{
						$option_ = 'fixed';
						$fixed_  = $property;
					} 
					else 
					{
						if (empty($property) || $property == '0000-00-00 00:00:00')
						{
							$option_ = 'gsd.item.' . $key;
							$custom_ = '';
						} else
						{
							$option_ = '_custom_';
							$custom_ = $property;	
						}
					}
				}

				if (!is_null($option_))
				{
					$properties_new[$key] = [
						'option' => $option_,
						'fixed'  => $fixed_,
						'custom' => $custom_
					];
				}
			}

			$params->$type = $properties_new;
			 
			$table = Table::getInstance('Item', 'GSDTable');
			$table->load($item->id);
			$table->params = json_encode($params);

			if ($table->store()){
				$total_items_migrated++;
			}
		}

		return $total_items_migrated;
	}

	/**
	 *
	 *  @return  void
	 */
	private function migrateItems()
	{
		if (!$snippets = $this->getSnippets())
		{
			return;
		}

		$total_items_migrated = 0;

		// Loop through each snippet, validate it and save it to gsd table
		foreach ($snippets as $snippet)
		{
			$id = $snippet->id;

			$this->validateSnippet($snippet);

			$model = BaseDatabaseModel::getInstance('Item', 'GSDModel');
			$item = $model->validate(null, $snippet);

			if ($result = $model->save($item))
			{
				$total_items_migrated++;
			}
		}

		return $total_items_migrated;
	}

	/**
	 * Move Dynamic Items to Items and renames 'dynamic' table to avoid transfering the same times again.
	 *
	 * @return void
	 */
	private function migrateDynamicItems()
	{
		if (!$items = $this->getDynamicSnippets())
		{
			return;
		}

		$total_transfered = 0;

		foreach ($items as $key => $item)
		{
			$params = json_decode($item->params);
			
			// Unset id in order to force model to create a new row in the db.
			unset($item->id);
			
			// Rename component property to plugin
			$item->plugin = $item->component;
			unset($item->component);

			// Rename 'categories' assignment to 'category'
			if (isset($params->assignments) && isset($params->assignments->categories))
			{
				$params->assignments->category = $params->assignments->categories;
				unset($params->assignments->categories);
			}

			$item->note = 'Moved Dynamic Item';

			$item->params = json_encode($params);
			
			$model = BaseDatabaseModel::getInstance('Item', 'GSDModel');
			$data = $model->validate(null, $item);

			if ($result = $model->save($data))
			{
				$total_transfered++;
			}
		}

		// Finally rename the dynamic table if all items have been successfully transfered.
		if (count($items) == $total_transfered)
		{
			$db = $this->getDB();
			$db->setQuery('RENAME TABLE ' . $db->quoteName('#__gsd_dynamic') .' to ' . $db->quoteName('#__gsd_dynamic_bck' . rand(1,100000)));
			$db->execute();
		}

		return $total_transfered;
	}

	/**
	 *  Validates snippets parameters and adjusts them to be valid with the new requirements
	 *
	 *  @param   object  $item  The snippet params object
	 *
	 *  @return  array          The new snippet params array
	 */
	private function validateSnippet(&$item)
	{
		$params = json_decode($item->params);

		// Set Title
		$item->title = $this->getItemTitle($item->plugin, $item->thing);

		// Migrate Note
		$item->note = $params->note;
		unset($params->note);

		// Content Type
		$item->contenttype = $params->contenttype;
		unset($params->contenttype);

		// Migrate Thing ID to Single Publishing Rules
		$integrationSingleRuleName = [
			'content' 		=> 'article',
			'k2' 	  		=> 'k2_items',
			'easyblog'	 	=> 'easyblogsingle',
			'eshop' 		=> 'eshopsingle',
			'eventbooking'  => 'eventbookingsingle',
			'hikashop' 		=> 'hikashopsingle',
			'j2store'	    => 'j2storesingle',
			'jshopping' 	=> 'jshoppingsingle',
			'menus' 		=> 'menu',
			'rsblog'		=> 'rsblogsingle',
			'sppagebuilder' => 'sppagebuildersingle',
			'virtuemart' 	=> 'virtuemartsingle',
			'zoo'			=> 'zoosingle'
		];

		$singleRuleName = $integrationSingleRuleName[$item->plugin];

		$params->assignments = [
			$singleRuleName => [
				'assignment_state' => 1,
				'selection'        => (array) $item->thing
			]
		];

		// Fix Brand Property
		// The brand property should have been declared as 'brand' and not as 'brandName' in the product XML file. 
		// Let's rename it.
		if ($item->contenttype == 'product' && isset($params->product) && isset($params->product->brandName) && !empty($params->product->brandName))
		{
			$params->product->brand = $params->product->brandName;
			unset($params->product->brandName);
		}

		// Fix new Custom Code path only on versions below 3.2 (dev)
		if (version_compare($this->version, '3.2', '<'))
		{
			if ($item->contenttype == 'custom_code' && !isset($item->custom_code) && !isset($item->custom_code->custom_code))
			{
				$params->custom_code = [
					'custom_code' => $params->customcode
				];
			}
		}

		// Unset obsolete properties
		unset($item->thing);

		$item->params = json_encode($params);
	}

	/**
	 *  Get snippets from database
	 *
	 *  @return  object
	 */
	public function getSnippets()
	{
		$db = $this->getDB();
		$query = $db->getQuery(true);
		$query
			->select('*')
			->from('#__gsd')
			->where('params LIKE ' . $db->quote("%contenttype%"));
		$db->setQuery($query);

		return $db->loadObjectList();
	}

	private function getDynamicSnippets()
	{
		if (!$this->dynamicTableExists())
		{
			return;
		}

		$db = $this->getDB();
		$query = $db->getQuery(true);
		$query
			->select('*')
			->from('#__gsd_dynamic');

		$db->setQuery($query);

		return $db->loadObjectList();
	}

	private function dynamicTableExists()
	{
		$db = $this->getDB();
		$db->setQuery('SHOW TABLES LIKE ' . $db->quote('%gsd_dynamic'));
		$db->execute();

		return $db->loadResult();
	}

	private function getItemTitle($plugin, $id)
	{
		$method = 'get' . ucfirst($plugin) . 'TitleQuery';

		if (!method_exists($this, $method) || !$id)
		{
			return ucfirst($plugin) . ' Item #'  . $id;
		}

		$query = $this->$method($id);

		$db = Factory::getDbo();
		$db->setQuery($query);
		$db->execute();

		return $db->loadResult() . ' #' . $id;
	}

	private function getEasyBlogTitleQuery($id)
	{
		return 'SELECT title from #__easyblog_post where id = ' . $id;
	}

	private function getRSBlogTitleQuery($id)
	{
		return 'SELECT title from #__rsblog_posts where id = ' . $id;
	}

	private function getContentTitleQuery($id)
	{
		return 'SELECT title from #__content where id = ' . $id;
	}

	private function getK2TitleQuery($id)
	{
		return 'SELECT title from #__k2_items where id = ' . $id;
	}

	public function migrateAutoMode()
	{
		$db = $this->getDB();
		$query = $db->getQuery(true);
		$query
			->select('*')
			->from('#__extensions')
			->where('folder = ' . $db->quote("gsd"))
			->where('params LIKE ' . $db->quote("%automode%"))
			->where('enabled = 1');

		$db->setQuery($query);

		$plugins = $db->loadObjectList();

		$plugins_migrated = [];

		foreach ($plugins as $key => $plugin)
		{
			// Make sure the component is installed
			if (!NRFramework\Extension::isInstalled($plugin->element))
			{
				continue;
			}

			$params = json_decode($plugin->params);

			if (!isset($params->automode) || (int) $params->automode == 0)
			{
				continue;
			}

			$model = BaseDatabaseModel::getInstance('Item', 'GSDModel');

			$data = [
				'title'       => '[Auto Mode] ' . ucfirst($plugin->element),
				'contenttype' => 'product',
				'plugin'      => $plugin->element,
				'state'		  => true,
				'note' 		  => 'Previously known as the Auto-Mode option',
				'colorgroup'  => '#46a546'
			];

			$item = $model->validate(null, $data);

			if ($result = $model->save($item))
			{
				$plugins_migrated[] = $plugin->element;
			}
		}

		return $plugins_migrated;
	}
}language/en-GB/en-GB.com_gsd.ini000064400000000735152345574300012237 0ustar00;; Language File
;;
;; @package         Google Structured Data
;; @version         5.6.5 Pro
;;
;; @author          Tassos Marinos <info@tassos.gr>
;; @link            http://www.tassos.gr
;; @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
;; @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later

GSD="Google Structured Data"
COM_GSD="Google Structured Data"
GSD_DESC="Enhance the appearance of your website in Google Search results"language/en-GB/en-GB.com_gsd.sys.ini000064400000001044152345574300013046 0ustar00;; Language File
;;
;; @package         Google Structured Data
;; @version         5.6.5 Pro
;;
;; @author          Tassos Marinos <info@tassos.gr>
;; @link            http://www.tassos.gr
;; @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
;; @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later

GSD="Google Structured Data"
COM_GSD="Google Structured Data"
GSD_DESC="Enhance the appearance of your website in Google Search results"
GSD_ITEMS="Items"
GSD_CONFIG="Configuration"
GSD_DASHBOARD="Dashboard"language/es-ES/es-ES.com_gsd.ini000064400000000772152345574300012310 0ustar00;; Language File
;;
;; @package         Google Structured Data
;; @version         5.6.5 Pro
;;
;; @author          Tassos Marinos <info@tassos.gr>
;; @link            http://www.tassos.gr
;; @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
;; @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later

GSD="Datos Estructurados de Google"
COM_GSD="Datos Estructurados de Google"
GSD_DESC="Mejore la apariencia de su sitio web en los resultados de búsqueda de Google"
language/es-ES/es-ES.com_gsd.sys.ini000064400000001102152345574300013111 0ustar00;; Language File
;;
;; @package         Google Structured Data
;; @version         5.6.5 Pro
;;
;; @author          Tassos Marinos <info@tassos.gr>
;; @link            http://www.tassos.gr
;; @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
;; @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later

GSD="Datos estructurados de Google"
COM_GSD="Datos estructurados de Google"
GSD_DESC="Mejore la apariencia de su sitio web en los resultados de búsqueda de Google"
GSD_ITEMS="Elementos"
GSD_CONFIG="Configuración"
GSD_DASHBOARD="Panel"
layouts/addons.php000064400000014071152345574300010240 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Language\Text;
use Joomla\CMS\HTML\HTMLHelper;

$isJ4 = defined('nrJ4');

if (!$isJ4)
{
    HTMLHelper::_('behavior.modal'); 
}

extract($displayData);

$isPro = GSD\Helper::isPro();
?>
<div class="nr-app-addons" data-base="<?php echo Uri::base() ?>">
    <table class="table nrTable">
    	<?php foreach ($items as $key => $item) { 
            $docsURL  = 'https://www.tassos.gr/joomla-extensions/google-structured-data-markup/docs/' . $item['docalias'];
            $btnClass = defined('nrJ4') ? 'btn-outline-secondary btn-sm' : 'btn-secondary';
    	?>
        <tr data-id="<?php echo $item['id']; ?>">
            <td class="addonImg">
                <img alt="<?php echo $item["label"]; ?>" src="<?php echo $item["image"]; ?>"/>
            </td>
            <td>
                <div class="addonTitle"><?php echo Text::_($item["label"]); ?></div>
                <div class="addonDesc"><?php echo Text::_($item["description"]); ?></div>
            </td>
            <td class="addonButtons">
                <?php if ($item['comingsoon']) { ?><?php echo Text::_('NR_ROADMAP'); ?><?php } ?>
                
                <?php if (!$item['comingsoon']) { ?>
                    <?php if ($item['id']) { ?>
        				<a class="btn <?php echo $btnClass ?> pluginState" href="#" title="<?php echo Text::_('GSD_INTEGRATION_TOGGLE') ?>">
        					<span class="icon-<?php echo $item['isEnabled'] ? "publish" : "unpublish" ?>"></span>
        				</a>

                        <?php 
                            $optionsURL = Uri::base(true) . '/index.php?option=com_plugins&view=plugin&tmpl=component&layout=modal&extension_id=' . $item['id'];
                            $modalName = 'gsdPluginModal-' . $item['id'];
                        ?>

              			<a class="btn <?php echo $btnClass ?>"
                            data-bs-toggle="modal"
                            data-toggle="modal"
                            href="#<?php echo $modalName ?>"
                            role="button"
                            title="<?php echo Text::_("JOPTIONS") ?>">
                        	<span class="icon-options"></span>
                        </a>

                        <?php
                            $options = [
                                'title'       => Text::_('GSD_INTEGRATION_EDIT'),
                                'url'         => $optionsURL,
                                'height'      => '400px',
                                'width'       => '800px',
                                'backdrop'    => 'static',
                                'bodyHeight'  => '70',
                                'modalWidth'  => '70',
                                'footer'      => '<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-dismiss="modal" aria-hidden="true">'
                                        . Text::_('JLIB_HTML_BEHAVIOR_CLOSE') . '</button>                                      
                                        <button type="button" class="btn btn-primary"
                                            onclick="document.querySelector(\'#' . $modalName . ' iframe\').contentDocument.querySelector(\'' . (version_compare(JVERSION, '5.0', 'ge') ? 'button.button-apply' : '#applyBtn') . '\').click();' . (defined('nrJ4') ? ' window.parent.Joomla.Modal.getCurrent().close();' : '') . '">'
                                        . Text::_('JSAVE') . '</button>
                                        <button type="button" class="btn btn-success"
                                        onclick="document.querySelector(\'#' . $modalName . ' iframe\').contentDocument.querySelector(\'' . (version_compare(JVERSION, '5.0', 'ge') ? 'button.button-apply' : '#applyBtn') . '\').click();">'
                                        . Text::_('JAPPLY') . '</button>'
                            ];

                            echo HTMLHelper::_('bootstrap.renderModal', $modalName, $options);
                        ?>

                    <?php } ?>
                    
                    <a class="btn <?php echo $btnClass ?>" href="<?php echo $docsURL; ?>" target="_blank" title="<?php echo Text::_("NR_DOCUMENTATION") ?>">
                        <span class="icon-info"></span>
                    </a>
                    <?php if (!$isPro && isset($item['image'])) { ?>
                        <a class="btn <?php echo $btnClass ?>" target="_blank" href="<?php echo $item['image']; ?>" title="<?php echo Text::_('NR_SAMPLE') ?>">
                            <span class="icon-image"></span>
                        </a>
                    <?php } ?>
                <?php } ?>
            </td>
        </tr>
    	<?php } ?>
		<tr>
			<td class="addonImg">
                <a target="_blank" target="_blank" href="https://www.tassos.gr/contact?extension=Google Structured Data&topic=Feature Request">
                    <img width="60px" alt="<?php echo $item["description"]; ?>" src="//static.tassos.gr/images/integrations/addon.png"/>
                </a>
            </td>
            <td>
                <div class="addonTitle"><?php echo Text::_("GSD_INTEGRATIONS_MISSING") ?></div>
                <?php echo Text::_("GSD_INTEGRATIONS_MISSING_DESC") ?>
            </div>
            <td class="addonButtons" colspan="2">
                <a class="btn btn-secondary btn-sm" target="_blank" href="https://www.tassos.gr/contact?extension=Google Structured Data&topic=Feature Request">
                    <span class="icon-mail"></span>
                	<?php echo Text::_("NR_CONTACT_US")?>
                </a>
            </td>
		</tr>
	</table>
</div>layouts/debug.php000064400000002164152345574300010056 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

?>

<div style="background-color:#fff; padding:30px; border:solid 1px #ccc; margin:20px; position:relative; grid-column: full-start/full-end; z-index:9999999999;">
    <div style="font-size:20px; font-weight:bold; margin-bottom:10px;">Google Structured Data Runtime Logs</div>
    <div>This log can help troubleshoot issues with your structured data. It's visible to Super Users only.</div>

    <div style="margin:20px 0;">
        <pre>
            <?php 
                highlight_string("<?php\n\$data =\n" . var_export($displayData, true) . ";\n?>");
            ?>
        </pre>
    </div>

    <div>To disable runtime logs, go to Components » Google Structured Data » Configuration » Advancecd and turn the <b>Debug</b> option off.</div>
</div>
layouts/fastedit.php000064400000003654152345574300010600 0ustar00

<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;

HTMLHelper::_('jquery.framework');
HTMLHelper::_('bootstrap.popover');

extract($displayData);

?>

<table class="table">
    <tbody>
        <?php foreach ($items as $key => $item) { ?>
            <tr data-pk="<?php echo $item->id ?>">
                <td width="1%"><span class="icon-<?php echo $item->state == 1 ? 'publish' : 'unpublish' ?>"></span></td>
                <td><?php echo $item->title; ?></td>
                <td width="5%" class="nowrap"><?php echo Text::_('GSD_' . $item->contenttype); ?></td>
                <td width="1%" class="gsdID nowrap">#<?php echo $item->id ?></td>
                <td width="15%" class="gsd-btn-toolbar nowrap text-right">
                    <a href="#gsdModal"
                        data-src="<?php echo Route::_('index.php?option=com_gsd&view=item&tmpl=component&layout=modal&id='. $item->id) ?>" 
                        class="btn btn-secondary"
                        data-bs-toggle="modal"
                        data-toggle="modal"
                        title="<?php echo Text::_('GSD_EDIT_SNIPPET'); ?>">
                        <span class="icon-edit"></span>
                    </a>
                    <a href="#" class="btn  btn-secondary gsdRemove" title="<?php echo Text::_('GSD_DELETE_SNIPPET'); ?>">
                        <span class="icon-trash"></span>
                    </a>
                </td>
            </tr>
        <?php } ?>
    </tbody>
</table>layouts/sidebar.php000064400000002351152345574300010377 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\Language\Text;

extract($displayData);

?>
<div class="nr-app-sidebar">
	<div class="nav">
		<ul>
			<?php foreach ($items as $key => $item) { 

				$class = '';

				if (isset($item['view']))
				{
					if (in_array($view, explode(",", $item['view'])))
					{
						$class = 'active';
					}
				}

				$target  = isset($item['target']) ? '_' . $item['target'] : "_self";
				$isModal = $item['url'] == '#proOnly';

				?>
			<li class="<?php echo $class; ?>">
				<a href="<?php echo $item['url']; ?>" target="<?php echo $target; ?>" <?php echo $isModal ? 'data-pro-only="' . Text::_($item['label']) . '"' : '' ?>>
					<span class="icon icon-<?php echo $item['icon'] ?>"></span>
					<span class="nav-label"><?php echo Text::_($item['label']); ?></span>
				</a>
			</li>
			<?php } ?>
		</ul>
	</div>
</div>models/forms/contenttypes/article.xml000064400000005214152345574300014073 0ustar00<?xml version="1.0" encoding="utf-8"?>
<form>
    <fieldset name="contenttype">
        <fields name="article">
            <field name="type" type="list"
                label="GSD_ARTICLE_SCHEMA"
                description="GSD_ARTICLE_SCHEMA_DESC"
                default="Article"
                required="true">
                <option value="Article">Article</option>
                <option value="NewsArticle">NewsArticle</option>
                <option value="BlogPosting">BlogPosting</option>
            </field>
            <field name="headline" type="map"
                label="NR_TITLE"
                description="GSD_ARTICLE_HEADLINE_DESC"
                hint="Article headline"
                default="gsd.item.headline"
                custom_class="input-xlarge"
                required="true"
            />
            <field name="description" type="map"
                label="GSD_ITEM_DESC"
                description="GSD_ITEM_DESC_DESC"
                default="gsd.item.description"
                custom_class="input-xlarge"
                required="true"
            />
            <field name="author" type="mapuser"
                label="GSD_AUTHOR_NAME"
                description="GSD_AUTHOR_DESC"
                required="true"
                default="user.name"
            />
            <field name="authorUrl" type="map"
                label="GSD_AUTHOR_URL"
                description="GSD_AUTHOR_URL_DESC"
                required="true"
                default="url"
            />
            <field name="image" type="mapimage"
                label="NR_IMAGE"
                description="GSD_IMAGE_OPTION_DESC"
                default="gsd.item.image"
                required="true"
            />
            <field name="publisher_name" type="mapuser"
                label="GSD_PUBLISHER_NAME"
                description="GSD_PUBLISHER_NAME_DESC"
                default="gsd.sitename"
            />
            <field name="publisher_logo" type="mapimage"
                label="GSD_PUBLISHER_LOGO"
                description="GSD_PUBLISHER_LOGO_DESC"
                default="gsd.sitelogo"
            />
            <field name="publish_up" type="mapdate"
                label="GSD_DATE_PUBLISHED"
                description="GSD_DATE_PUBLISHED_DESC"
                default="gsd.item.publish_up"
            />
            <field name="modified" type="mapdate"
                label="GSD_DATE_MODIFIED"
                description="GSD_DATE_MODIFIED_DESC"
                default="gsd.item.modified"
            />
        </fields>
    </fieldset>
</form>models/forms/contenttypes/book.xml000064400000011513152345574300013401 0ustar00<?xml version="1.0" encoding="utf-8"?>
<form>
    <fieldset name="contenttype">
        <fields name="book">
            <field name="headline" type="map"
                label="NR_TITLE"
                description="GSD_BOOK_HEADLINE_DESC"
                hint="The Book Title"
                default="gsd.item.headline"
                custom_class="input-xlarge"
                required="true"
            />
            <field name="author" type="mapuser"
                label="GSD_AUTHOR_NAME"
                description="GSD_AUTHOR_DESC"
                required="true"
                default="user.name"
            />
            <field name="authorUrl" type="map"
                label="GSD_AUTHOR_URL"
                description="GSD_AUTHOR_URL_DESC"
                required="true"
                default="url"
            />
            <field name="image" type="mapimage"
                label="NR_IMAGE"
                description="GSD_BOOK_IMAGE_DESC"
                default="gsd.item.image"
            />
            <field name="referenceURL" type="map"
                label="GSD_SAME_AS"
                description="GSD_BOOK_SAME_AS_DESC"
            />
            <field name="bookFormat" type="map"
                label="GSD_BOOK_FORMAT"
                description="GSD_BOOK_FORMAT_DESC"
                required="true">
                <subform>
                    <field name="fixed" type="list"
                        hiddenLabel="true"
                        showon="option:fixed">
                        <option value="AudiobookFormat">GSD_BOOK_AUDIOBOOK_FORMAT</option>
                        <option value="EBook">GSD_BOOK_EBOOK</option>
                        <option value="Hardcover">GSD_BOOK_HARDCOVER</option>
                        <option value="Paperback">GSD_BOOK_PAPERBACK</option>
                    </field>
                </subform>
            </field>
            <field name="edition" type="map"
                label="GSD_BOOK_EDITION"
                description="GSD_BOOK_EDITION_DESC"
                hint="2nd Edition"
            />
            <field name="inLanguage" type="map"
                label="GSD_BOOK_MAIN_LANGUAGE"
                description="GSD_BOOK_MAIN_LANGUAGE_DESC"
                hint="en"
                required="true">
                <subform>
                    <field name="fixed" type="nr_geo"
                        showon="option:fixed"
                        showselect="false"
                    />
                </subform>
            </field>
            <field name="isbn" type="map"
                label="GSD_ISBN"
                description="GSD_BOOK_ISBN_DESC"
                required="true"
            />
            <field name="publish_up" type="mapdate"
                label="GSD_DATE_PUBLISHED"
                description="GSD_DATE_PUBLISHED_DESC"
                default="gsd.item.publish_up"
            />
            <field name="identifier_oclc_number" type="map"
                label="GSD_OCLC_NUMBER"
                description="GSD_BOOK_OCLC_NUMBER_DESC"
            />
            <field name="identifier_lccn" type="map"
                label="GSD_LCCN"
                description="GSD_BOOK_LCCN_DESC"
            />
            <field name="identifier_jp_e_code" type="map"
                label="GSD_JP_E_CODE"
                description="GSD_BOOK_JP_E_CODE_DESC"
            />
            <field name="potentialAction" type="map"
                label="GSD_ACTION"
                description="GSD_BOOK_POTENTIAL_ACTION_DESC"
                required="true">
                <subform>
                    <field name="fixed" type="list"
                        hiddenLabel="true"
                        showon="option:fixed">
                        <option value="ReadAction">Read</option>
                        <option value="BuyAction">Buy</option>
                        <option value="SellAction">Sell</option>
                        <option value="OrderAction">Order</option>
                        <option value="PreOrderAction">Pre Order</option>
                        <option value="RentAction">Rent</option>
                        <option value="ReviewAction">Review</option>
                        <option value="DownloadAction">Download</option>
                        <option value="TipAction">Tip</option>
                        <option value="DonateAction">Donate</option>
                        <option value="BorrowAction">Borrow</option>
                    </field>
                </subform>
            </field>
            <field name="actionURL" type="map"
                label="GSD_ACTION_URL"
                description="GSD_BOOK_ACTION_URL_DESC"
                required="true"
            />
        </fields>
    </fieldset>
</form>models/forms/contenttypes/course.xml000064400000022544152345574300013755 0ustar00<?xml version="1.0" encoding="utf-8"?>
<form>
    <fieldset name="contenttype">
        <fields name="course">
            <field name="headline" type="map"
                label="NR_TITLE"
                description="GSD_COURSE_TITLE_DESC"
                hint="Mathematics 101"
                default="gsd.item.headline"
                custom_class="input-xlarge"
                required="true"
            />
            <field name="description" type="map"
                label="GSD_ITEM_DESC"
                description="GSD_ITEM_DESC_DESC"
                custom_class="input-xlarge"
                default="gsd.item.description"
                required="true"
            />
            <field name="image" type="mapimage"
                label="NR_IMAGE"
                description="GSD_IMAGE_OPTION_DESC"
                default="gsd.item.image"
            />
            <field name="start_date" type="mapdate"
                label="GSD_EVENT_START_DATE"
                description="GSD_COURSE_START_DATE_DESC"
            />
            <field name="end_date" type="mapdate"
                label="GSD_EVENT_END_DATE"
                description="GSD_COURSE_END_DATE_DESC"
            />  
            <field name="course_mode" type="map"
                label="GSD_COURSE_MODE"
                description="GSD_COURSE_MODE_DESC"
                required="true">
                <subform>
                    <field name="fixed" type="list"
                        hiddenLabel="true"
                        showon="option:fixed"
                        multiple="true">
                        <option value="online">Online</option>
                        <option value="onsite">Onsite</option>
                        <option value="blended">Blended</option>
                        <option value="synchronous">Synchronous</option>
                        <option value="asynchronous">Asynchronous</option>
                        <option value="full-time">Full-time</option>
                        <option value="part-time">Part-time</option>
                    </field>
                </subform>
            </field>
            <field name="course_code" type="map"
                label="GSD_COURSE_CODE"
                description="GSD_COURSE_CODE_DESC"
            />
            <field name="courseWorkload" type="map"
                label="GSD_COURSE_WORKLOAD"
                description="GSD_COURSE_WORKLOAD_DESC"
                hint="5"
                required="true"
            />
            <field type="spacer" name="label3" label="Place / Address" />
            <field name="place_name" type="map"
                label="GSD_COURSE_PLACE_NAME"
                description="GSD_COURSE_PLACE_NAME_DESC"
            />
            <field name="country" type="map"
                label="GSD_JOBPOSTING_COUNTRY"
                description="GSD_COURSE_COUNTRY_DESC"
                hint="US">
                <subform>
                    <field name="fixed" type="nr_geo"
                        showon="option:fixed"
                        showselect="false"
                    />
                </subform>
            </field>
            <field name="address" type="map"
                label="GSD_JOBPOSTING_ADDRESS"
                description="GSD_COURSE_ADDRESS_DESC"
                hint="555 Clancy St"
            />
            <field name="locality" type="map"
                label="GSD_JOBPOSTING_LOCALITY"
                description="GSD_COURSE_LOCALITY_DESC"
                hint="Mountain View"
            />
            <field name="region" type="map"
                label="GSD_BUSINESSLISTING_ADDRESS_REGION"
                description="GSD_COURSE_REGION_DESC"
                hint="MI"
            />
            <field name="postal_code" type="map"
                label="GSD_BUSINESSLISTING_POSTAL_CODE"
                description="GSD_COURSE_POSTA_CODE_DESC"
                hint="48201"
            />
            <field type="spacer" name="label1" label="Price" />
            <field name="price" type="map"
                label="GSD_PRODUCT_OFFER_PRICE"
                description="GSD_PRODUCT_OFFER_PRICE_DESC"
                hint="29.50"
            />
            <field name="priceCurrency" type="map"
                label="GSD_PRODUCT_OFFER_CURRENCY"
                description="GSD_PRODUCT_OFFER_CURRENCY_DESC"
                hint="USD">
                <subform>
                    <field name="fixed" type="nr_currencies"
                        showon="option:fixed"
                        showselect="false"
                    />
                </subform>
            </field>
            <field name="offerCategory" type="map"
                label="GSD_OFFER_CATEGORY"
                description="GSD_OFFER_CATEGORY_DESC"
                required="true">
                <subform>
                    <field name="fixed" type="list"
                        hiddenLabel="true"
                        showon="option:fixed"
                        default="Free">
                        <option value="Free">GSD_FREE</option>
                        <option value="Partially Free">GSD_PARTIALLY_FREE</option>
                        <option value="Subscription">GSD_SUBSCRIPTION</option>
                        <option value="Paid">GSD_PAID</option>
                    </field>
                </subform>
            </field>
            <field name="availability" type="map"
                label="GSD_PRODUCT_AVAILABILITY"
                description="GSD_PRODUCT_AVAILABILITY_DESC">
                <subform>
                    <field name="fixed" type="list"
                        hiddenLabel="true"
                        showon="option:fixed"
                        default="http://schema.org/InStock">
                        <option value="http://schema.org/Discontinued">GSD_AVAILABILITY_DISCONTINUED</option>
                        <option value="http://schema.org/InStock">GSD_AVAILABILITY_IN_STOCK</option>
                        <option value="http://schema.org/InStoreOnly">GSD_AVAILABILITY_INSTORE_ONLY</option>
                        <option value="http://schema.org/LimitedAvailability">GSD_AVAILABILITY_LIMITED_AVAILABILITY</option>
                        <option value="http://schema.org/OnlineOnly">GSD_AVAILABILITY_ONLINE_ONLY</option>
                        <option value="http://schema.org/OutOfStock">GSD_AVAILABILITY_OUT_OF_STOCK</option>
                        <option value="http://schema.org/PreOrder">GSD_AVAILABILITY_PRE_ORDER</option>
                        <option value="http://schema.org/PreSale">GSD_AVAILABILITY_PRE_SALE</option>
                        <option value="http://schema.org/SoldOut">GSD_AVAILABILITY_SOLDOUT</option>
                    </field>
                </subform>
            </field>
            <field name="validFrom" type="mapdate"
                label="GSD_PRODUCT_PRICE_VALID_DATE_FROM"
                description="GSD_PRODUCT_PRICE_VALID_DATE_FROM_DESC"
            />
            <field type="spacer" name="performer" label="Performer" />
            <field name="performer_type" type="map"
                label="GSD_COURSE_PERFORMER_TYPE"
                description="GSD_COURSE_PERFORMER_TYPE_DESC">
                <subform>
                    <field name="fixed" type="list"
                        hiddenLabel="true"
                        showon="option:fixed"
                        default="PerformingGroup">
                        <option value="PerformingGroup">GSD_EVENT_PERFORMING_GROUP</option>
                        <option value="Organization">GSD_ORGANIZATION</option>
                        <option value="Person">GSD_PERSON</option>
                    </field>
                </subform>
            </field>
            <field name="performer_name" type="map"
                label="GSD_COURSE_PERFORMER_NAME"
                description="GSD_COURSE_PERFORMER_NAME_DESC"
                hint="John Doe"
            />
            <field type="spacer" name="rating" label="Rating" />
            <field name="rating_value" type="map"
                label="GSD_RATE_VALUE"
                description="GSD_RATE_VALUE_DESC"
                default="gsd.item.ratingValue">
                <subform>
                    <field name="custom" type="nr_rate"/>
                </subform>
            </field>
            <field name="review_count" type="map"
                label="GSD_REVIEW_COUNT"
                description="GSD_REVIEW_COUNT_DESC"
                default="gsd.item.reviewCount"
                hint="5"
            />
            <field type="spacer" name="other" label="Other" />
            <field name="publish_up" type="mapdate"
                label="GSD_DATE_PUBLISHED"
                description="GSD_DATE_PUBLISHED_DESC"
                default="gsd.item.publish_up"
            />
            <field name="created" type="mapdate"
                label="GSD_DATE_CREATED"
                description="GSD_DATE_PUBLISHED_DESC"
                default="gsd.item.created"
            />
            <field name="modified" type="mapdate"
                label="GSD_DATE_MODIFIED"
                description="GSD_DATE_MODIFIED_DESC"
                default="gsd.item.modified"
            />
        </fields>
    </fieldset>
</form>models/forms/contenttypes/custom_code.xml000064400000001244152345574300014753 0ustar00<?xml version="1.0" encoding="utf-8"?>
<form>
    <fieldset name="contenttype">
        <fields name="custom_code">
            <field name="custom_code" type="textarea"
                label="GSD_CUSTOM_CODE"
                description="GSD_CUSTOM_CODE_DESC"
                class="span12"
                rows="16"
                filter="raw"
                hint="&lt;script type='application/ld+json'&gt;&lt;/script&gt;"
            />
        </fields>
        <field name="customcode_disclaimer" type="note" 
            class="alert alert-error full-width"
            description="GSD_CUSTOM_CODE_DISCLAIMER"
        />     
    </fieldset>
</form>models/forms/contenttypes/event.xml000064400000021144152345574300013571 0ustar00<?xml version="1.0" encoding="utf-8"?>
<form>
    <fieldset name="contenttype">
        <fields name="event">
            <field name="headline" type="map"
                label="NR_TITLE"
                description="GSD_EVENT_TITLE_DESC"
                hint="Jan Lieberman Concert Series: Journey in Jazz"
                custom_class="input-xlarge"
                default="gsd.item.headline"
                required="true"
            />
            <field name="description" type="map"
                label="GSD_ITEM_DESC"
                description="GSD_ITEM_DESC_DESC"
                custom_class="input-xlarge"
                rows="5"
                default="gsd.item.description"
                required="true"
            />
            <field name="image" type="mapimage"
                label="NR_IMAGE"
                description="GSD_IMAGE_OPTION_DESC"
                default="gsd.item.image"
            />
            <field name="startDate" type="mapdate"
                label="GSD_EVENT_START_DATE"
                description="GSD_EVENT_START_DATE_DESC"
                required="true"
                default="gsd.item.startdate"
            />
            <field name="endDate" type="mapdate"
                label="GSD_EVENT_END_DATE"
                description="GSD_EVENT_END_DATE_DESC"
                default="gsd.item.enddate"
            />
            <field type="spacer" name="label" label="GSD_LOCATION" />
            <field name="locationName" type="map"
                label="GSD_EVENT_LOCATION_NAME"
                description="GSD_EVENT_LOCATION_NAME_DESC"
                hint="Warner Theatre"
                default="gsd.item.locationname"
                required="true"
            />
            <field name="locationAddress" type="map"
                label="GSD_EVENT_STREET_ADDRESS"
                description="GSD_EVENT_STREET_ADDRESS_DESC"
                hint="New York, NY, 12345, USA"
                default="gsd.item.locationaddress"
            />
            <field name="addressCountry" type="map"
                label="GSD_BUSINESSLISTING_ADDRESS_COUNTRY"
                description="GSD_EVENT_ADDRESS_COUNTRY_DESC"
                default="gsd.item.addressCountry"
                hint="US">
                <subform>
                    <field name="fixed" type="NR_Geo"
                        showon="option:fixed"
                    />
                </subform>
            </field>
            <field name="addressLocality" type="map"
                label="GSD_BUSINESSLISTING_ADDRESS_LOCALITY"
                description="GSD_EVENT_ADDRESS_LOCALITY_DESC"
                hint="GSD_EVENT_ADDRESS_LOCALITY_HINT"
                default="gsd.item.addressLocality"
            />
            <field name="addressRegion" type="map"
                label="GSD_BUSINESSLISTING_ADDRESS_REGION"
                description="GSD_EVENT_ADDRESS_REGION_DESC"
                hint="GSD_EVENT_ADDRESS_REGION_HINT"
                default="gsd.item.addressRegion"
            />
            <field name="postalCode" type="map"
                label="GSD_BUSINESSLISTING_POSTAL_CODE"
                description="GSD_EVENT_POSTAL_CODE_DESC"
                hint="GSD_EVENT_POSTAL_CODE_HINT"
                default="gsd.item.postalCode"
            />
            <field name="online_url" type="map"
                label="GSD_EVENT_ONLINE_EVENT_URL"
                description="GSD_EVENT_ONLINE_EVENT_URL_DESC"
                hint="https://"
            />
            <field type="spacer" name="label2" label="GSD_PERFORMER" />
            <field name="performerType" type="map"
                label="GSD_EVENT_PERFORMER_TYPE"
                description="GSD_EVENT_PERFORMER_TYPE_DESC">
                <subform>
                    <field name="fixed" type="list"
                        hiddenLabel="true"
                        showon="option:fixed">
                        <option value="Person">GSD_PERSON</option> 
                        <option value="PerformingGroup">GSD_EVENT_PERFORMING_GROUP</option>
                        <option value="DanceGroup">GSD_EVENT_DANCE_GROUP</option>
                        <option value="MusicGroup">GSD_EVENT_MUSIC_GROUP</option>
                        <option value="TheaterGroup">GSD_EVENT_THEATER_GROUP</option>
                    </field>
                </subform>
            </field>
            <field name="performerName" type="map"
                label="GSD_EVENT_PERFORMER_NAME"
                description="GSD_EVENT_PERFORMER_NAME_DESC"
                hint="John Doe"
            />
            <field name="performerURL" type="map"
                label="GSD_EVENT_PERFORMER_URL"
                description="GSD_EVENT_PERFORMER_URL_DESC"
                hint="https://"
            />
            <field type="spacer" name="label4" label="GSD_EVENT_ORGANIZER" />
            <field name="organizerType" type="map"
                label="GSD_EVENT_ORGANIZER_TYPE"
                description="GSD_EVENT_ORGANIZER_TYPE_DESC">
                <subform>
                    <field name="fixed" type="list"
                        hiddenLabel="true"
                        showon="option:fixed">
                        <option value="Organization">GSD_ORGANIZATION</option>
                        <option value="Person">GSD_PERSON</option> 
                    </field>
                </subform>
            </field>
            <field name="organizerName" type="map"
                label="GSD_EVENT_ORGANIZER_NAME"
                description="GSD_EVENT_ORGANIZER_NAME_DESC"
                hint="John Doe"
            />
            <field name="organizerURL" type="map"
                label="GSD_EVENT_ORGANIZER_URL"
                description="GSD_EVENT_ORGANIZER_URL_DESC"
                hint="https://"
            />
            <field type="spacer" name="label3" label="GSD_JOBPOSTING_OTHER" />
            <field name="offerAvailability" type="map"
                label="GSD_EVENT_AVAILABILITY"
                description="GSD_EVENT_AVAILABILITY_DESC">
                <subform>
                    <field name="fixed" type="list"
                        hiddenLabel="true"
                        showon="option:fixed">
                        <option value="http://schema.org/InStock">GSD_AVAILABILITY_IN_STOCK</option>
                        <option value="http://schema.org/PreOrder">GSD_AVAILABILITY_PRE_ORDER</option>
                        <option value="http://schema.org/SoldOut">GSD_AVAILABILITY_SOLDOUT</option>
                    </field>
                </subform>
            </field>
            <field name="eventAttendanceMode" type="map"
                label="GSD_EVENT_ATTENDANCE_MODE"
                description="GSD_EVENT_ATTENDANCE_MODE_DESC">
                <subform>
                    <field name="fixed" type="list"
                        hiddenLabel="true"
                        showon="option:fixed">
                        <option value="https://schema.org/OnlineEventAttendanceMode">GSD_ONLINE</option>
                        <option value="https://schema.org/OfflineEventAttendanceMode">GSD_OFFLINE</option>
                        <option value="https://schema.org/MixedEventAttendanceMode">GSD_MIXED</option>
                    </field>
                </subform>
            </field>
            <field name="offerStartDate" type="mapdate"
                label="GSD_EVENT_AVAILABILITY_START_DATE"
                description="GSD_EVENT_AVAILABILITY_START_DATE_DESC"
                default="gsd.item.offerstartdate"
            />  
            <field name="offerPrice" type="map"
                label="GSD_EVENT_OFFER_PRICE"
                description="GSD_EVENT_OFFER_PRICE_DESC"
                default="gsd.item.offerprice"
            />
            <field name="offerCurrency" type="map"
                label="GSD_PRODUCT_OFFER_CURRENCY"
                description="GSD_PRODUCT_OFFER_CURRENCY_DESC"
                hint="EUR"
                default="gsd.item.offercurrency">
                <subform>
                    <field name="fixed" type="nr_currencies"
                        showon="option:fixed"
                        showselect="false"
                    />
                </subform>
            </field>
            <field name="offerInventoryLevel" type="map"
                label="GSD_EVENT_INVENTORY_LEVEL"
                description="GSD_EVENT_INVENTORY_LEVEL_DESC"
                default="gsd.item.offerinventorylevel"
            />
        </fields>
    </fieldset>
</form>models/forms/contenttypes/factcheck.xml000064400000006325152345574300014367 0ustar00<?xml version="1.0" encoding="utf-8"?>
<form>
    <fieldset name="contenttype">
        <fields name="factcheck">
            <field name="headline" type="map"
                label="NR_TITLE"
                description="GSD_FACTCHECK_CLAIMTITLE_DESC"
                hint="The world is flat"
                custom_class="input-xlarge"
                default="gsd.item.headline"
                required="true"
            />
            <field name="claimDatePublished" type="mapdate"
                label="GSD_FACTCHECK_CLAIMDATEPUBLISHED"
                description="GSD_FACTCHECK_CLAIMDATEPUBLISHED_DESC"
                default="gsd.item.publish_up"
                required="true"
            />
            <field name="claimAuthorName" type="map"
                label="GSD_FACTCHECK_CLAIMAUTHORNAME"
                description="GSD_FACTCHECK_CLAIMAUTHORNAME_DESC"
                hint="Flat World Society"
                required="true"
            />
            <field name="claimAuthorType" type="map"
                label="GSD_FACTCHECK_CLAIMAUTHORTYPE" 
                description="GSD_FACTCHECK_CLAIMAUTHORTYPE_DESC"
                required="true">
                <subform>
                    <field name="fixed" type="list"
                        hiddenLabel="true"
                        showon="option:fixed">
                        <option value="Organization">GSD_ORGANIZATION</option>
                        <option value="Person">GSD_PERSON</option>
                    </field>
               </subform>
            </field>
            <field name="factcheckRating" type="map"
                label="GSD_FACTCHECK_RATING" 
                description="GSD_FACTCHECK_RATING_DESC"
                required="true">
                <subform>
                    <field name="fixed" type="list"
                        hiddenLabel="true"
                        showon="option:fixed">
                        <option value="-1">GSD_FACTCHECK_UNCATEGORIZED</option>
                        <option value="1">JFALSE</option>
                        <option value="2">GSD_FACTCHECK_MOSTLY_FALSE</option>
                        <option value="3">GSD_FACTCHECK_HALF_TRUE</option>
                        <option value="4">GSD_FACTCHECK_MOSTLY_TRUE</option>
                        <option value="5">JTRUE</option>
                    </field>
               </subform>
            </field>
            <field name="claimURL" type="map"
                label="GSD_FACTCHECK_CLAIMURL"
                description="GSD_FACTCHECK_CLAIMURL_DESC"
                custom_class="input-xlarge"
                hint="https://example.flatworlders.com/we-know-that-the-world-is-flat"
            />
            <field name="multiple" type="nrtoggle"
                label="GSD_FACTCHECK_MULTIPLE"
                description="GSD_FACTCHECK_MULTIPLE_DESC"
            />
            <field name="anchorName" type="map"
                label="GSD_FACTCHECK_ANCHORNAME"
                description="GSD_FACTCHECK_ANCHORNAME_DESC"
                hint="#fact1"
                showon="multiple:1"
                custom_class="input-xlarge"
            />
        </fields>
    </fieldset>
</form>models/forms/contenttypes/faq.xml000064400000004241152345574300013216 0ustar00<?xml version="1.0" encoding="utf-8"?>
<form>
    <fieldset name="contenttype">
        <fields name="faq">
            <field name="mode" type="radio" 
		        label="GSD_FAQ_MODE"
				description="GSD_FAQ_MODE_DESC"
				default="auto"
                class="btn-group btn-group-yesno">
				<option value="auto">GSD_FAQ_MODE_AUTO</option>
				<option value="manual">GSD_FAQ_MODE_MANUAL</option>
			</field>
            <field name="faq_repeater_fields" type="subform"
                label="NR_LIST"
                description="GSD_FAQ_REPEATER_DESC"
                showon="mode:manual"
                layout="joomla.form.field.subform.repeatable-table"
                multiple="true"
                default='{"faq_repeater_fields":{}}'>
                <form>
                    <field name="question" type="textarea"
                        label="GSD_FAQ_QUESTION"
                        hint="GSD_FAQ_QUESTION_HINT"
                        class="span12 full-width w-100"
                        rows="4"
                    />
                    <field name="answer" type="textarea"
                        label="GSD_FAQ_ANSWER"
                        hint="GSD_FAQ_ANSWER_HINT"
                        class="span12 full-width w-100"
                        rows="4"
                    />
                </form>
            </field>
            <fieldset>
                <field name="question_selector" type="text"
                    label="GSD_FAQ_QUESTION_SELECTOR"
                    description="GSD_FAQ_QUESTION_SELECTOR_DESC"
                    showon="mode:auto"
                    default=".question"
                    hint="h3.faq-question"
                    class="input-xlarge"
                />
                <field name="answer_selector" type="text"
                    label="GSD_FAQ_ANSWER_SELECTOR"
                    description="GSD_FAQ_ANSWER_SELECTOR_DESC"
                    showon="mode:auto"
                    default=".answer"
                    hint="div.faq-answer"
                    class="input-xlarge"
                />
            </fieldset>
        </fields>
    </fieldset>
</form>models/forms/contenttypes/howto.xml000064400000011426152345574300013612 0ustar00<?xml version="1.0" encoding="utf-8"?>
<form>
    <fieldset name="contenttype">
        <fields name="howto">
            <field name="name" type="map"
                label="NR_TITLE"
                description="GSD_HOWTO_NAME_DESC"
                hint="How to tie a tie"
                default="gsd.item.headline"
                required="true"
            />
            <field name="image" type="mapimage"
                label="NR_IMAGE"
                description="GSD_HOWTO_IMAGE_DESC"
                default="gsd.item.image"
            />
            <field name="estimatedCost" type="map"
                label="GSD_HOWTO_ESTIMATE_COST"
                description="GSD_HOWTO_ESTIMATE_COST_DESC"
            />
            <field name="estimatedCostCurrency" type="map"
                label="GSD_HOWTO_ESTIMATE_COST_CURRENCY"
                description="GSD_HOWTO_ESTIMATE_COST_CURRENCY_DESC"
                hint="USD">
                <subform>
                    <field name="fixed" type="nr_currencies"
                        showon="option:fixed"
                        showselect="false"
                    />
                </subform>
            </field>
            <field name="supply" type="map"
                label="GSD_HOWTO_SUPPLY"
                description="GSD_HOWTO_SUPPLY_DESC"
                custom_value_multiple="true"
            />
            <field name="tool" type="map"
                label="GSD_HOWTO_TOOL"
                description="GSD_HOWTO_TOOL_DESC"
                custom_value_multiple="true"
            />
            <field name="totalTime" type="map"
                label="GSD_HOWTO_TOTALTIME"
                description="GSD_HOWTO_TOTALTIME_DESC"
                default="gsd.item.offerprice"
            />
            <field name="mode" type="radio" 
		        label="GSD_HOWTO_STEPS"
				description="GSD_HOWTO_STEPS_TYPE"
				default="auto"
                class="btn-group btn-group-yesno">
				<option value="auto">GSD_USE_CSS_SELECTORS</option>
				<option value="manual">GSD_MANUAL_ENTRY</option>
			</field>
            <field name="howto_repeater" type="subform"
                label="GSD_HOWTO_STEPS"
                description="GSD_HOWTO_STEPS_DESC"
                showon="mode:manual"
                layout="joomla.form.field.subform.repeatable-table"
                class="howToRepeater"
                multiple="true"
                default='{"howto_repeater":{}}'>
                <form>
                    <field name="name" type="textarea"
                        label="GSD_HOWTO_STEP_NAME"
                        description="GSD_HOWTO_STEP_NAME_DESC"
                        rows="4"
                    />
                    <field name="text" type="textarea"
                        label="GSD_HOWTO_STEP_TEXT"
                        description="GSD_HOWTO_STEP_TEXT_DESC"
                        rows="4"
                    />
                    <field name="image" type="media"
                        label="GSD_HOWTO_STEP_IMAGE"
                        description="GSD_HOWTO_STEP_IMAGE_DESC"
                        preview="false"
                    />
                    <field name="url" type="url"
                        label="GSD_HOWTO_STEP_URL"
                        description="GSD_HOWTO_STEP_URL_DESC"
                    />
                </form>
            </field>
            <field name="name_selector" type="cssselector"
                label="GSD_HOWTO_STEP_NAME"
                description="GSD_HOWTO_STEP_NAME_DESC"
                showon="mode:auto"
                default='{"selector":".howToStep .name","task":"text","attr":""}'
                hint=".howToStep .name"
            />
            <field name="text_selector" type="cssselector"
                label="GSD_HOWTO_STEP_TEXT"
                description="GSD_HOWTO_STEP_TEXT_DESC"
                showon="mode:auto"
                default='{"selector":".howToStep .text","task":"innerhtml","attr":""}'
                hint=".howToStep .text"
            />
            <field name="image_selector" type="cssselector"
                label="GSD_HOWTO_STEP_IMAGE"
                description="GSD_HOWTO_STEP_IMAGE_DESC"
                showon="mode:auto"
                default='{"selector":".howToStep .image","task":"attr","attr":"src"}'
                hint=".howToStep .image"
            />
            <field name="url_selector" type="cssselector"
                label="GSD_HOWTO_STEP_URL"
                description="GSD_HOWTO_STEP_URL_DESC"
                showon="mode:auto"
                default='{"selector":".howToStep .url","task":"attr","attr":"href"}'
                hint=".howToStep .url"
            />
        </fields>
    </fieldset>
</form>models/forms/contenttypes/jobposting.xml000064400000015062152345574300014630 0ustar00<?xml version="1.0" encoding="utf-8"?>
<form>
    <fieldset name="contenttype">
        <fields name="jobposting">
            <field name="headline" type="map"
                label="GSD_JOBPOSTING_TITLE"
                description="GSD_JOBPOSTING_TITLE_DESC"
                default="gsd.item.headline"
                hint="Software Engineer"
                custom_class="input-xlarge"
                required="true"
            />
            <field name="description" type="map"
                label="GSD_JOBPOSTING_DESCRIPTION"
                description="GSD_JOBPOSTING_DESCRIPTION_DESC"
                default="gsd.item.description"
                hint="ABC Company Inc. seeks a full-time software engineer to develop in-house tools."
                custom_class="input-xlarge"
                required="true"
            />
            <field name="publish_up" type="mapdate"
                label="GSD_JOBPOSTING_POSTED"
                description="GSD_JOBPOSTING_POSTED_DESC"
                default="gsd.item.publish_up"
            />
            <field name="educationRequirements" type="map"
                label="GSD_JOBPOSTING_EDUCATION"
                description="GSD_JOBPOSTING_EDUCATION_DESC"
                hint="Bachelor's Degree in Computer Science"
            />
            <field name="industry" type="map"
                label="GSD_JOBPOSTING_INDUSTRY"
                description="GSD_JOBPOSTING_INDUSTRY_DESC"
                hint="Computer Software"
            />
            <field name="valid_through" type="mapdate"
                label="GSD_JOBPOSTING_VALID_THROUGH"
                description="GSD_JOBPOSTING_VALID_THROUGH_DESC"
            />
            <field name="employmenttype" type="map"
                label="GSD_JOBPOSTING_TYPE"
                description="GSD_JOBPOSTING_TYPE_DESC">
                <subform>
                    <field name="fixed" type="list"
                        hiddenLabel="true"
                        multiple="true"
                        showon="option:fixed">
                        <option value="FULL_TIME">GSD_JOBPOSTING_FULLTIME</option>
                        <option value="PART_TIME">GSD_JOBPOSTING_PARTTIME</option>
                        <option value="CONTRACTOR">GSD_JOBPOSTING_CONTRACTOR</option>
                        <option value="TEMPORARY">GSD_JOBPOSTING_TEMPORARY</option>
                        <option value="INTERN">GSD_JOBPOSTING_INTERN</option>
                        <option value="VOLUNTEER">GSD_JOBPOSTING_VOLUNTEER</option>
                        <option value="PER_DIEM">GSD_JOBPOSTING_PERDIEM</option>
                        <option value="OTHER">GSD_JOBPOSTING_OTHER</option>
                    </field>
                </subform>
            </field>
            <field type="spacer" name="label3" label="GSD_JOBPOSTING_HIRING_ORGANIZATION" />
            <field name="hiring_oprganization_name" type="map"
                label="GSD_JOBPOSTING_ORG_NAME"
                description="GSD_JOBPOSTING_ORG_NAME_DESC"
                hint="Wheel Company"
                default="gsd.sitename"
                required="true"
            />
            <field name="hiring_oprganization_url" type="map"
                label="GSD_JOBPOSTING_ORG_URL"
                description="GSD_JOBPOSTING_ORG_URL_DESC"
                hint="http://"
                default="gsd.siteurl"
                required="true"
            />
            <field name="hiring_organization_logo" type="mapimage"
                label="GSD_JOBPOSTING_ORG_LOGO"
                description="GSD_JOBPOSTING_ORG_LOGO_DESC"
                default="gsd.sitelogo"
            />
            <field type="spacer" name="label1" label="GSD_JOBPOSTING_SALARY" />
            <field name="salary" type="map"
                label="GSD_JOBPOSTING_SALARY"
                description="GSD_JOBPOSTING_SALARY_DESC"
                hint="50"
            />
            <field name="salary_unit" type="map"
                label="GSD_JOBPOSTING_SALARY_UNIT"
                description="GSD_JOBPOSTING_SALARY_UNIT_DESC">
                <subform>
                    <field name="fixed" type="list"
                        hiddenLabel="true"
                        showon="option:fixed">
                        <option value="HOUR">GSD_HOUR</option>
                        <option value="DAY">GSD_DAY</option>
                        <option value="WEEK">GSD_WEEK</option>
                        <option value="MONTH">GSD_MONTH</option>
                        <option value="YEAR">GSD_YEAR</option>
                    </field>
                </subform>
            </field>
            <field name="currency" type="map"
                label="GSD_JOBPOSTING_SALARY_CURRENCY"
                description="GSD_JOBPOSTING_SALARY_CURRENCY_DESC"
                hint="USD">
                <subform>
                    <field name="fixed" type="nr_currencies"
                        showon="option:fixed"
                        showselect="false"
                    />
                </subform>
            </field>
            <field type="spacer" name="label2" label="GSD_JOBPOSTING_LOCATION" />
            <field name="addressCountry" type="map"
                label="GSD_JOBPOSTING_COUNTRY"
                description="GSD_JOBPOSTING_COUNTRY_DESC"
                hint="US"
                required="true">
                <subform>
                    <field name="fixed" type="nr_geo"
                        showon="option:fixed"
                        showselect="false"
                    />
                </subform>
            </field>
            <field name="streetAddress" type="map"
                label="GSD_JOBPOSTING_ADDRESS"
                description="GSD_JOBPOSTING_ADDRESS_DESC"
                hint="555 Clancy St"
                required="true"
            />
            <field name="locality" type="map"
                label="GSD_JOBPOSTING_LOCALITY"
                description="GSD_JOBPOSTING_LOCALITY_DESC"
                hint="Mountain View"
            />
            <field name="region" type="map"
                label="GSD_BUSINESSLISTING_ADDRESS_REGION"
                description="GSD_JOBPOSTING_REGION_DESC"
                hint="MI"
            />
            <field name="postal_code" type="map"
                label="GSD_BUSINESSLISTING_POSTAL_CODE"
                description="GSD_BUSINESSLISTING_POSTAL_CODE_DESC"
                hint="48201"
            />
        </fields>
    </fieldset>
</form>models/forms/contenttypes/localbusiness.xml000064400000012216152345574300015316 0ustar00<?xml version="1.0" encoding="utf-8"?>
<form>
    <fieldset name="contenttype">
        <fields name="localbusiness">
            <field name="type" type="businesstypes" 
                default="LocalBusiness" 
                description="GSD_LOCALBUSINESS_TYPE_DESC"
                label="GSD_BUSINESSLISTING_TYPE"
                multiple="true"
			    layout="joomla.form.field.list-fancy-select"
                required="true">
            </field>
            <field name="name" type="map"
                label="GSD_LOCALBUSINESS_NAME"
                description="GSD_LOCALBUSINESS_NAME_DESC"
                hint="GSD_LOCALBUSINESS_NAME_HINT"
                default="gsd.item.headline"
                required="true"
            />
            <field name="image" type="mapimage"
                label="NR_IMAGE"
                description="GSD_IMAGE_OPTION_DESC"
                default="gsd.item.image"
                required="true"
            />
            <field name="telephone" type="map"
                label="GSD_LOCALBUSINESS_PHONE"
                description="GSD_LOCALBUSINESS_PHONE_DESC"
                hint="GSD_BUSINESSLISTING_TELEPHONE_HINT"
            />
            <field name="priceRange" type="map"
                label="GSD_PRICE_RANGE"
                description="GSD_PRICE_RANGE_DESC"
                hint="GSD_PRICE_RANGE_HINT"
            />
           <field name="openinghours" type="map"
                label="GSD_LOCALBUSINESS_OPENING_HOURS"
                description="GSD_LOCALBUSINESS_OPENING_HOURS_DESC"
                hidecustomvalue="true">
                <subform>
                    <field name="fixed" type="openinghours"
                        hiddenLabel="true"
                        showon="option:fixed"
                    />
                </subform>
            </field>
            <field name="servesCuisine" type="map"
                label="GSD_RECIPE_CUISINE"
                description="GSD_LOCALBUSINESS_CUISINE_DESC"
                hint="Mediterranean"
                showon="type:Bakery,BarOrPub,Brewery,CafeOrCoffeeShop,FastFoodRestaurant,IceCreamShop,Restaurant,Winery"
            />
            <field name="menu" type="map"
                label="GSD_LOCALBUSINESS_MENU"
                description="GSD_LOCALBUSINESS_MENU_DESC"
                hint="http://"
                showon="type:Bakery,BarOrPub,Brewery,CafeOrCoffeeShop,FastFoodRestaurant,IceCreamShop,Restaurant,Winery"
            />
            <field type="spacer" name="label" label="GSD_LOCALBUSINESS_ADDRESS" />
            <field name="addressCountry" type="map"
                label="GSD_JOBPOSTING_COUNTRY"
                description="GSD_LOCALBUSINESS_COUNTRY_DESC"
                hint="US">
                <subform>
                    <field name="fixed" type="nr_geo"
                        showon="option:fixed"
                        showselect="false"
                    />
                </subform>
            </field>
            <field name="addressLocality" type="map"
                label="GSD_BUSINESSLISTING_ADDRESS_LOCALITY"
                description="GSD_LOCALBUSINESS_CITY_DESC"
                hint="GSD_BUSINESSLISTING_ADDRESS_LOCALITY_HINT"
            />
            <field name="streetAddress" type="map"
                label="GSD_BUSINESSLISTING_STREET_ADDRESS"
                description="GSD_LOCALBUSINESS_STREET_ADDRESS_DESC"
                hint="GSD_BUSINESSLISTING_STREET_ADDRESS_HINT"
            />
            <field name="addressRegion" type="map"
                label="GSD_BUSINESSLISTING_ADDRESS_REGION"
                description="GSD_LOCALBUSINESS_REGION_DESC"
                hint="GSD_BUSINESSLISTING_ADDRESS_REGION_HINT"
            />
            <field name="postalCode" type="map"
                label="GSD_BUSINESSLISTING_POSTAL_CODE"
                description="GSD_LOCALBUSINESS_POSTALCODE_DESC"
                hint="GSD_BUSINESSLISTING_POSTAL_CODE_HINT"
            />
            <field name="geo" type="map"
                label="GSD_BUSINESSLISTING_COORDINATES"
                description="GSD_LOCALBUSINESS_COORDINATES_DESC"
                hint="GSD_BUSINESSLISTING_COORDINATES_HINT">
                <subform>
                    <field name="custom" type="nr_gmap"
                        showon="option:_custom_"
                        showselect="false"
                    />
                </subform>
            </field>
            <field type="spacer" name="label2" label="GSD_BUSINESSLISTING_RATING" />
            <field name="rating_value" type="map"
                label="GSD_RATE_VALUE"
                description="GSD_RATE_VALUE_DESC"
                default="gsd.item.ratingValue">
                <subform>
                    <field name="custom" type="nr_rate"/>
                </subform>
            </field>
            <field name="review_count" type="map"
                label="GSD_REVIEW_COUNT"
                description="GSD_REVIEW_COUNT_DESC"
                default="gsd.item.reviewCount"
                hint="5"
            />
        </fields>
    </fieldset>
</form>models/forms/contenttypes/movie.xml000064400000012157152345574300013573 0ustar00<?xml version="1.0" encoding="utf-8"?>
<form>
    <fieldset name="contenttype">
        <fields name="movie">
            <field name="title" type="map"
                label="NR_NAME"
                description="GSD_MOVIE_TITLE_DESC"
                hint="Lord of the Rings"
                default="gsd.item.headline"
                custom_class="input-xlarge"
                required="true"
            />
            <field name="description" type="map"
                label="GSD_ITEM_DESC"
                description="GSD_MOVIE_ITEM_DESC"
                default="gsd.item.description"
                custom_class="input-xlarge"
            />
            <field name="image" type="mapimage"
                label="NR_IMAGE"
                description="GSD_MOVIE_IMAGE_DESC"
                default="gsd.item.image"
                required="true"
            />
            <field name="duration" type="map"
                label="GSD_MOVIE_DURATION"
                description="GSD_MOVIE_DURATION_DESC"
            />
            <field name="genre" type="map"
                label="GSD_MOVIE_GENRE"
                description="GSD_MOVIE_GENRE_DESC">
                <subform>
                    <field name="custom" type="subform"
                        layout="joomla.form.field.subform.repeatable-table"
                        multiple="true"
                        default='{"name":""}'>
                        <form>
                            <field name="name" type="text"
                                hint="GSD_MOVIE_GENRE_HINT"
                                label="GSD_MOVIE_GENRE_NAME"
                            />
                        </form>
                    </field>
                </subform>
            </field>
            <field name="creators" type="map"
                label="GSD_MOVIE_CREATORS"
                description="GSD_MOVIE_CREATORS_DESC">
                <subform>
                    <field name="custom" type="subform"
                        layout="joomla.form.field.subform.repeatable-table"
                        multiple="true"
                        default='{"name":""}'>
                        <form>
                            <field name="name" type="text"
                                hint="GSD_MOVIE_CREATOR_NAME_HINT"
                                label="GSD_MOVIE_CREATOR_NAME"
                            />
                        </form>
                    </field>
                </subform>
            </field>
            <field name="directors" type="map"
                label="GSD_MOVIE_DIRECTORS"
                description="GSD_MOVIE_DIRECTORS_DESC">
                <subform>
                    <field name="custom" type="subform"
                        layout="joomla.form.field.subform.repeatable-table"
                        multiple="true"
                        default='{"name":""}'>
                        <form>
                            <field name="name" type="text"
                                hint="GSD_MOVIE_DIRECTOR_HINT"
                                label="GSD_MOVIE_DIRECTOR_NAME"
                            />
                        </form>
                    </field>
                </subform>
            </field>
            <field name="actors" type="map"
                label="GSD_ACTORS"
                description="GSD_ACTORS_DESC">
                <subform>
                    <field name="custom" type="subform"
                        layout="joomla.form.field.subform.repeatable-table"
                        multiple="true"
                        default='{"name":""}'>
                        <form>
                            <field name="name" type="text"
                                hint="GSD_MOVIE_ACTOR_HINT"
                                label="GSD_ACTOR_NAME"
                            />
                        </form>
                    </field>
                </subform>
            </field>

            <field name="trailerUrl" type="map"
                label="GSD_MOVIE_TRAILER_TRAILERURL"
                description="GSD_VIDEO_CONTENTURL_DESC"
                custom_class="input-xlarge"
                hint="https://www.example.com/movie_trailer.mp4"
            />
            <field name="publish_up" type="mapdate"
                label="GSD_MOVIE_RELEASE_DATE"
                description="GSD_MOVIE_RELEASE_DATE_DESC"
            />
            <field type="spacer" name="label2" label="GSD_BUSINESSLISTING_RATING" />
            <field name="rating_value" type="map"
                label="GSD_RATE_VALUE"
                description="GSD_RATE_VALUE_DESC"
                default="gsd.item.ratingValue">
                <subform>
                    <field name="custom" type="nr_rate"/>
                </subform>
            </field>
            <field name="review_count" type="map"
                label="GSD_REVIEW_COUNT"
                description="GSD_REVIEW_COUNT_DESC"
                default="gsd.item.reviewCount"
                hint="5"
            />
        </fields>
    </fieldset>
</form>models/forms/contenttypes/person.xml000064400000023367152345574300013767 0ustar00<?xml version="1.0" encoding="utf-8"?>
<form>
    <fieldset name="contenttype">
        <fields name="person">
            <field name="type" type="persontypes" 
                default="Person" 
                label="NR_TYPE"
                description="GSD_PERSON_TYPE_DESC"
			    layout="joomla.form.field.list-fancy-select"
                required="true">
            </field>
            <field type="spacer" name="label" label="NR_NAME" />
            <field name="headline" type="map"
                label="NR_NAME"
                description="GSD_PERSON_NAME_DESC"
                hint="John Doe"
                custom_class="input-xlarge"
                default="gsd.item.headline"
            />
            <field name="honorificPrefix" type="map"
                label="GSD_PERSON_HONORIFICPREFIX"
                description="GSD_PERSON_HONORIFICPREFIX_DESC"
                hint="Dr."
                custom_class="input-xlarge"
            />
            <field name="honorificSuffix" type="map"
                label="GSD_PERSON_HONORIFICSUFFIX"
                description="GSD_PERSON_HONORIFICSUFFIX_DESC"
                hint="PhD"
                custom_class="input-xlarge"
            />
            <field name="alternateName" type="map"
                label="GSD_ALTERNATE_NAME"
                description="GSD_ALTERNATE_NAME_DESC"
                custom_class="input-xlarge"
                hint="Johny"
            />
            <field name="additionalName" type="map"
                label="GSD_ADDITIONAL_NAME"
                description="GSD_PERSON_ADDITIONAL_NAME"
                custom_class="input-xlarge"
                hint="George"
            />
            <field name="givenName" type="map"
                label="GSD_GIVEN_NAME"
                description="GSD_PERSON_GIVEN_NAME"
                hint="John"
                custom_class="input-xlarge"
            />
            <field name="familyName" type="map"
                label="GSD_FAMILY_NAME"
                hint="Doe"
                description="GSD_PERSON_FAMILY_NAME"
                custom_class="input-xlarge"
            />
            <field name="nationality" type="map"
                label="GSD_NATIONALITY"
                description="GSD_PERSON_NATIONALITY_DESC"
                hint="US">
                <subform>
                    <field name="fixed" type="nr_geo"
                        showon="option:fixed"
                        showselect="false"
                    />
                </subform>
            </field>
            <field type="spacer" name="label2" label="NR_ADDRESS" />
            <field name="addressCountry" type="map"
                label="NR_COUNTRY"
                description="GSD_PERSON_COUNTRY_DESC"
                hint="US">
                <subform>
                    <field name="fixed" type="nr_geo"
                        showon="option:fixed"
                        showselect="false"
                    />
                </subform>
            </field>
            <field name="addressLocality" type="map"
                label="NR_CITY"
                description="GSD_PERSON_CITY_DESC"
                hint="Mountain View"
            />
            <field name="streetAddress" type="map"
                label="GSD_BUSINESSLISTING_STREET_ADDRESS"
                description="GSD_PERSON_STREET_ADDRESS_DESC"
                hint="GSD_BUSINESSLISTING_STREET_ADDRESS_HINT"
            />
            <field name="addressRegion" type="map"
                label="GSD_BUSINESSLISTING_ADDRESS_REGION"
                description="GSD_PERSON_REGION_DESC"
                hint="GSD_BUSINESSLISTING_ADDRESS_REGION_HINT"
            />
            <field name="postalCode" type="map"
                label="NR_POSTAL_CODE"
                description="GSD_PERSON_POSTALCODE_DESC"
                hint="GSD_BUSINESSLISTING_POSTAL_CODE_HINT"
            />
            <field type="spacer" name="label3" label="GSD_PERSON" />
            <field name="description" type="map"
                label="GSD_ITEM_DESC"
                description="GSD_PERSON_TEXT_DESC"
                default="gsd.item.description"
                custom_class="input-xlarge"
            />
            <field name="email" type="map"
                label="NR_EMAIL"
                description="GSD_PERSON_EMAIL_DESC"
                custom_class="input-xlarge"
            />
            <field name="telephone" type="map"
                label="GSD_LOCALBUSINESS_PHONE"
                description="GSD_PERSON_PHONE_DESC"
                hint="GSD_BUSINESSLISTING_TELEPHONE_HINT"
            />
            <field name="gender" type="map"
                label="GSD_GENDER"
                description="GSD_PERSON_GENDER_DESC">
                <subform>
                    <field name="fixed" type="list"
                        hiddenLabel="true"
                        showon="option:fixed">
                        <option value="https://schema.org/Male">GSD_MALE</option>
                        <option value="https://schema.org/Female">GSD_FEMALE</option>
                    </field>
                </subform>
            </field>
            <field name="birthDate" type="mapdate"
                label="GSD_BIRTH_DATE"
                description="GSD_PERSON_BIRTH_DATE_DESC"
                format="%Y-%m-%d"
            />
            <field name="memberOf" type="map"
                label="GSD_MEMBER_OF"
                description="GSD_PERSON_MEMBER_OF_DESC"
                custom_value_multiple="true"
            />
            <field name="image" type="mapimage"
                label="NR_IMAGE"
                description="GSD_PERSON_IMAGE_DESC"
                default="gsd.item.image"
            />
            <field name="jobTitle" type="map"
                label="GSD_JOBPOSTING_TITLE"
                description="GSD_PERSON_JOB_TITLE_DESC"
                hint="Software Engineer"
            />
            <field name="worksFor" type="map"
                label="GSD_WORKS_FOR"
                description="GSD_PERSON_WORKS_FOR_DESC"
                hint="Apple"
            />
            <field name="sameAs" type="map"
                label="GSD_SAME_AS"
                description="GSD_PERSON_SAME_AS_DESC"
                subform_class="hide-custom-repeater-label subform-fullwidth">
                <subform>
                    <field name="custom" type="subform"
                        layout="joomla.form.field.subform.repeatable-table"
                        multiple="true"
                        default='{"name":""}'>
                        <form>
                            <field name="name" type="text"
                                hint="https://"
                                label="NR_URL"
                                class="input-xlarge"
                            />
                        </form>
                    </field>
                </subform>
            </field>
            <field name="affiliation" type="map"
                label="GSD_AFFILIATION"
                description="GSD_PERSON_AFFILIATION_DESC"
                custom_value_multiple="true"
            />
            <field name="alumniOf" type="map"
                label="GSD_ALUMNI_OF"
                description="GSD_PERSON_ALUMNI_OF_DESC"
                custom_value_multiple="true"
            />
            <field name="award" type="map"
                label="GSD_AWARD"
                description="GSD_PERSON_AWARD_DESC"
                custom_value_multiple="true"
            />
            <field name="knowsAbout" type="map"
                label="GSD_KNOWS_ABOUT"
                description="GSD_PERSON_KNOWS_ABOUT_DESC"
                custom_value_multiple="true"
            />
            <field name="hasCredential" type="map"
                label="GSD_HAS_CREDENTIAL"
                description="GSD_PERSON_HAS_CREDENTIAL_DESC"
                custom_value_multiple="true"
            />
            <field type="spacer" name="label4" label="GSD_OCCUPATION" />
            <field name="occupationName" type="map"
                label="NR_NAME"
                description="GSD_PERSON_OCCUPATION_NAME_DESC"
                custom_class="input-xlarge"
            />
            <field name="occupationDescription" type="map"
                label="GSD_ITEM_DESC"
                description="GSD_OCCUPATION_DESCRIPTION_DESC"
                custom_class="input-xlarge"
            />
            <field name="educationRequirements" type="map"
                label="GSD_EDUCATIONAL_REQUIREMENTS"
                description="GSD_PERSON_EDUCATIONAL_REQUIREMENTS_DESC"
                custom_class="input-xlarge"
            />
            <field name="experienceRequirements" type="map"
                label="GSD_EXPERIENCE_REQUIREMENTS"
                description="GSD_EXPERIENCE_REQUIREMENTS_DESC"
                custom_class="input-xlarge"
            />
            <field name="offerPrice" type="map"
                real_type="pricerange"
                label="GSD_SALARY_AMOUNT"
                description="GSD_PERSON_SALARY_DESC"
                default="gsd.item.offerprice"
            />
            <field name="currency" type="map"
                label="NR_CURRENCY"
                description="GSD_PERSON_CURRENCY_DESC"
                hint="USD"
                default="gsd.item.currency">
                <subform>
                    <field name="fixed" type="nr_currencies"
                        showon="option:fixed"
                        showselect="false"
                    />
                </subform>
            </field>
        </fields>
    </fieldset>
</form>models/forms/contenttypes/product.xml000064400000014761152345574300014137 0ustar00<?xml version="1.0" encoding="utf-8"?>
<form>
    <fieldset name="contenttype">
        <fields name="product">
            <field name="headline" type="map"
                label="NR_TITLE"
                description="GSD_PRODUCT_HEADLINE_DESC"
                hint="Apple iPhone 8 Plus"
                default="gsd.item.headline"
                custom_class="input-xlarge"
                required="true"
            />
            <field name="description" type="map"
                label="GSD_ITEM_DESC"
                description="GSD_PRODUCT_TEXT_DESC"
                default="gsd.item.description"
                custom_class="input-xlarge"
                required="true"
            />
            <field name="image" type="mapimage"
                label="NR_IMAGE"
                description="GSD_PRODUCT_IMAGE_DESC"
                default="gsd.item.image"
            />
            <field name="rating_value" type="map"
                label="GSD_RATE_VALUE"
                description="GSD_RATE_VALUE_DESC"
                default="gsd.item.ratingValue">
                <subform>
                    <field name="custom" type="nr_rate"/>
                </subform>
            </field>
            <field name="review_count" type="map"
                label="GSD_REVIEW_COUNT"
                description="GSD_REVIEW_COUNT_DESC"
                default="gsd.item.reviewCount"
                hint="5"
            />
            <field name="sku" type="map"
                label="SKU"
                description="GSD_PRODUCT_SKU_DESC"
                hint="ABC123"
                default="gsd.item.sku"
            />
            <field name="mpn" type="map"
                label="MPN"
                description="GSD_PRODUCT_MPN_DESC"
                hint="ABC123"
                default="gsd.item.mpn"
            />
            <field name="gtin" type="map"
                label="GTIN"
                description="GSD_PRODUCT_GTIN_DESC"
                hint="3234567890126"
                default="gsd.item.gtin"
            />
            <field name="weight" type="map"
                label="GSD_PRODUCT_WEIGHT"
                description="GSD_PRODUCT_WEIGHT_DESC"
                default="gsd.item.weight"
            />
            <field name="weightUnit" type="map"
                label="GSD_PRODUCT_WEIGHT_UNIT"
                description="GSD_PRODUCT_WEIGHT_UNIT_DESC"
                default="gsd.item.weightUnit">
                <subform>
                    <field name="fixed" type="list"
                        hiddenLabel="true"
                        showon="option:fixed">
                        <option value="kg">GSD_KILOGRAM</option>
                        <option value="g">GSD_GRAM</option>
                        <option value="mg">GSD_MILLIGRAM</option>
                        <option value="lb">GSD_POUNDS</option>
                        <option value="oz">GSD_OUNCE</option>
                    </field>
                </subform>
            </field>
            <field name="brand" type="map"
                label="GSD_PRODUCT_BRAND_NAME"
                description="GSD_PRODUCT_BRAND_NAME_DESC"
                hint="Apple"
                default="gsd.item.brand"
            />
            <field name="offerPrice" type="map"
                label="GSD_PRODUCT_OFFER_PRICE"
                description="GSD_PRODUCT_OFFER_PRICE_DESC"
                hint="29.50"
                default="gsd.item.offerprice"
            />
            <field name="currency" type="map"
                label="GSD_PRODUCT_OFFER_CURRENCY"
                description="GSD_PRODUCT_OFFER_CURRENCY_DESC"
                hint="USD"
                default="gsd.item.currency">
                <subform>
                    <field name="fixed" type="nr_currencies"
                        showon="option:fixed"
                        showselect="false"
                    />
                </subform>
            </field>
            <field name="offerItemCondition" type="map"
                label="GSD_PRODUCT_CONDITION"
                description="GSD_PRODUCT_CONDITION_DESC">
                <subform>
                    <field name="fixed" type="list"
                        hiddenLabel="true"
                        showon="option:fixed">
                        <option value="http://schema.org/NewCondition">GSD_PRODUCT_CONDITION_NEW</option>
                        <option value="http://schema.org/UsedCondition">GSD_PRODUCT_CONDITION_USED</option>
                        <option value="http://schema.org/DamagedCondition">GSD_PRODUCT_CONDITION_DAMAGED</option>
                        <option value="http://schema.org/RefurbishedCondition">GSD_PRODUCT_CONDITION_REFURBISHED</option>
                    </field>
                </subform>
            </field>
            <field name="offerAvailability" type="map"
                label="GSD_PRODUCT_AVAILABILITY"
                description="GSD_PRODUCT_AVAILABILITY_DESC">
                <subform>
                    <field name="fixed" type="list"
                        hiddenLabel="true"
                        showon="option:fixed"
                        default="http://schema.org/InStock">
                        <option value="http://schema.org/Discontinued">GSD_AVAILABILITY_DISCONTINUED</option>
                        <option value="http://schema.org/InStock">GSD_AVAILABILITY_IN_STOCK</option>
                        <option value="http://schema.org/InStoreOnly">GSD_AVAILABILITY_INSTORE_ONLY</option>
                        <option value="http://schema.org/LimitedAvailability">GSD_AVAILABILITY_LIMITED_AVAILABILITY</option>
                        <option value="http://schema.org/OnlineOnly">GSD_AVAILABILITY_ONLINE_ONLY</option>
                        <option value="http://schema.org/OutOfStock">GSD_AVAILABILITY_OUT_OF_STOCK</option>
                        <option value="http://schema.org/PreOrder">GSD_AVAILABILITY_PRE_ORDER</option>
                        <option value="http://schema.org/PreSale">GSD_AVAILABILITY_PRE_SALE</option>
                        <option value="http://schema.org/SoldOut">GSD_AVAILABILITY_SOLDOUT</option>
                    </field>
                </subform>
            </field>
            <field name="priceValidUntil" type="mapdate"
                label="GSD_PRODUCT_PRICE_VALID_DATE"
                description="GSD_PRODUCT_PRICE_VALID_DATE_DESC"
            />
        </fields>
    </fieldset>
</form>models/forms/contenttypes/recipe.xml000064400000012053152345574300013716 0ustar00<?xml version="1.0" encoding="utf-8"?>
<form>
    <fieldset name="contenttype">
         <fields name="recipe">
            <field name="headline" type="map"
                label="NR_TITLE"
                description="GSD_RECIPE_TITLE_DESC"
                hint="Chicken Soup"
                default="gsd.item.headline"
                custom_class="input-xlarge"
                required="true"
            />
            <field name="description" type="map"
                label="GSD_ITEM_DESC"
                description="GSD_ITEM_DESC_DESC"
                default="gsd.item.description"
                custom_class="input-xlarge"
                required="true"
            />
            <field name="image" type="mapimage"
                label="NR_IMAGE"
                description="GSD_IMAGE_OPTION_DESC"
                default="gsd.item.image"
                required="true"
            />
            <field name="author" type="mapuser"
                label="GSD_AUTHOR_NAME"
                description="GSD_AUTHOR_DESC"
                required="true"
                default="user.name"
            />
            <field name="authorUrl" type="map"
                label="GSD_AUTHOR_URL"
                description="GSD_AUTHOR_URL_DESC"
                required="true"
                default="url"
            />
            <field name="rating_value" type="map"
                label="GSD_RATE_VALUE"
                description="GSD_RATE_VALUE_DESC"
                default="gsd.item.ratingValue">
                <subform>
                    <field name="custom" type="nr_rate"/>
                </subform>
            </field>
            <field name="review_count" type="map"
                label="GSD_REVIEW_COUNT"
                description="GSD_REVIEW_COUNT_DESC"
                default="gsd.item.reviewCount"
                hint="5"
            />
            <field name="category" type="map"
                label="GSD_CATEGORY"
                description="GSD_RECIPE_CATEGORY_DESC"
                hint="Dessert"
            />
            <field name="cuisine" type="map"
                label="GSD_RECIPE_CUISINE"
                description="GSD_RECIPE_CUISINE_DESC"
                hint="Mediterranean"
            />
            <field name="prepTime" type="map"
                label="GSD_RECIPE_PREPTIME"
                description="GSD_RECIPE_PREPTIME_DESC"
            />
            <field name="cookTime" type="map"
                label="GSD_RECIPE_COOKTIME"
                description="GSD_RECIPE_COOKTIME_DESC"
            />
            <field name="totalTime" type="map"
                label="GSD_RECIPE_TOTALTIME"
                description="GSD_RECIPE_TOTALTIME_DESC"
            />
            <field name="calories" type="map"
                label="GSD_RECIPE_CALORIES"
                description="GSD_RECIPE_CALORIES_DESC"
            />
            <field name="yield" type="map"
                label="GSD_RECIPE_YIELD"
                description="GSD_RECIPE_YIELD_DESC"
                hint="4 servings"
            />
            <field name="ingredient" type="map"
                label="GSD_RECIPE_INGREDIENT"
                description="GSD_RECIPE_INGREDIENT_DESC">
                <subform>
                    <field name="custom" type="textarea"
                        hint="GSD_RECIPE_INGREDIENT_DESC"
                        class="input-xxlarge"
                        rows="3"
                        filter="raw"
                    />
                </subform>
            </field>
            <field name="instructions" type="map"
                label="GSD_RECIPE_INSTRUCTIONS"
                description="GSD_RECIPE_INSTRUCTIONS_DESC">
                <subform>
                    <field name="custom" type="textarea"
                        hint="GSD_RECIPE_INSTRUCTIONS_DESC"
                        class="input-xxlarge"
                        rows="3"
                        filter="raw"
                    />
                </subform>
            </field>
            <field name="video" type="map"
                label="GSD_VIDEO_CONTENTURL"
                description="GSD_RECIPE_VIDEO_DESC"
                hint="https://www.example.com/chicken_soup.mp4"
                custom_class="input-xxlarge"
            />
            <field name="keywords" type="map"
                label="GSD_KEYWORDS"
                description="GSD_RECIPE_KEYWORDS_DESC"
                hint="winter apple pie, nutmeg crust"
                custom_class="input-xxlarge"
            />
            <field name="publish_up" type="mapdate"
                label="GSD_DATE_PUBLISHED"
                description="GSD_DATE_PUBLISHED_DESC"
                default="gsd.item.publish_up"
            />
            <field name="modified" type="mapdate"
                label="GSD_DATE_MODIFIED"
                description="GSD_DATE_MODIFIED_DESC"
                default="gsd.item.modified"
            />
        </fields>
    </fieldset>
</form>models/forms/contenttypes/review.xml000064400000031262152345574300013753 0ustar00<?xml version="1.0" encoding="utf-8"?>
<form>
    <fieldset name="contenttype">
        <fields name="review">
            <field name="itemReviewedType" type="list"
                label="GSD_REVIEW_ITEM_TYPE"
                description="GSD_REVIEW_ITEM_TYPE_DESC"
                required="true">
                <option value="">GSD_PLEASE_SELECT</option>
                <option value="CreativeWorkSeries">GSD_REVIEW_ITEM_CREATIVEWORKSERIES</option>
                <option value="LocalBusiness">GSD_REVIEW_ITEM_LOCALBUSINESS</option>
                <option value="Movie">GSD_REVIEW_ITEM_MOVIE</option>
                <option value="Book">GSD_REVIEW_ITEM_BOOK</option>
                <option value="Product">GSD_REVIEW_ITEM_PRODUCT</option>
                <option value="Game">Game</option>
            </field>
            <field name="headline" type="map"
                label="GSD_REVIEW_ITEM_NAME"
                description="GSD_REVIEW_NAME_TYPE_DESC"
                custom_class="input-xlarge"
                hint="iPhone 6s"
                default="gsd.item.headline"
                required="true"
            />
            <field name="description" type="map"
                label="GSD_REVIEW_TEXT"
                description="GSD_REVIEW_TEXT_DESC"
                hint="GSD_ITEM_DESC_DESC"
                custom_class="input-xxlarge"
                default="gsd.item.description"
                required="true"
            />
            <field name="author" type="mapuser"
                label="GSD_AUTHOR_NAME"
                description="GSD_AUTHOR_DESC"
                required="true"
                default="user.name"
            />
            <field name="authorUrl" type="map"
                label="GSD_AUTHOR_URL"
                description="GSD_AUTHOR_URL_DESC"
                required="true"
                default="url"
            />
            <field name="image" type="mapimage"
                label="NR_IMAGE"
                description="GSD_IMAGE_OPTION_DESC"
                default="gsd.item.image"
            />
            <field name="rating_value" type="map"
                label="GSD_RATE_VALUE"
                description="GSD_RATE_VALUE_DESC"
                default="gsd.item.ratingValue">
                <subform>
                    <field name="custom" type="nr_rate"/>
                </subform>
            </field>
            <field name="review_count" type="map"
                showon="itemReviewedType:Product"
                label="GSD_REVIEW_COUNT"
                description="GSD_REVIEW_COUNT_DESC"
                default="gsd.item.reviewCount"
                hint="5"
            />
            <field name="publish_up" type="mapdate"
                label="GSD_DATE_PUBLISHED"
                description="GSD_DATE_PUBLISHED_DESC"
                default="gsd.item.publish_up"
            />
            <field type="spacer" name="label0" label="GSD_REVIEW_ITEM_PRODUCT" showon="itemReviewedType:Product"/>
            <!-- Product -->
            <field name="item_reviewed_product_description" type="map"
                label="GSD_ITEM_DESC"
                description="GSD_PRODUCT_TEXT_DESC"
                showon="itemReviewedType:Product"
                required="false">
                <subform>
                    <field name="custom" type="textarea"/>
                </subform>
            </field>
            <field name="item_reviewed_product_sku" type="map"
                label="SKU"
                description="GSD_PRODUCT_SKU_DESC"
                showon="itemReviewedType:Product"
                required="false"
            />
            <field name="item_reviewed_product_brand" type="map"
                label="GSD_PRODUCT_BRAND_NAME"
                description="GSD_PRODUCT_BRAND_NAME_DESC"
                showon="itemReviewedType:Product"
                required="false"
            />
            <field name="item_reviewed_product_offerprice" type="map"
                showon="itemReviewedType:Product"
                label="GSD_PRODUCT_OFFER_PRICE"
                description="GSD_PRODUCT_OFFER_PRICE_DESC"
                hint="29.50"
                default="gsd.item.offerprice"
            />
            <field name="item_reviewed_product_currency" type="map"
                showon="itemReviewedType:Product"
                label="GSD_PRODUCT_OFFER_CURRENCY"
                description="GSD_PRODUCT_OFFER_CURRENCY_DESC"
                hint="USD"
                default="gsd.item.currency">
                <subform>
                    <field name="fixed" type="nr_currencies"
                        showon="option:fixed"
                        showselect="false"
                    />
                </subform>
            </field>
            <field name="item_reviewed_product_offeritemcondition" type="map"
                showon="itemReviewedType:Product"
                label="GSD_PRODUCT_CONDITION"
                description="GSD_PRODUCT_CONDITION_DESC">
                <subform>
                    <field name="fixed" type="list"
                        hiddenLabel="true"
                        showon="option:fixed">
                        <option value="http://schema.org/NewCondition">GSD_PRODUCT_CONDITION_NEW</option>
                        <option value="http://schema.org/UsedCondition">GSD_PRODUCT_CONDITION_USED</option>
                        <option value="http://schema.org/DamagedCondition">GSD_PRODUCT_CONDITION_DAMAGED</option>
                        <option value="http://schema.org/RefurbishedCondition">GSD_PRODUCT_CONDITION_REFURBISHED</option>
                    </field>
                </subform>
            </field>
            <field name="item_reviewed_product_offeravailability" type="map"
                showon="itemReviewedType:Product"
                label="GSD_PRODUCT_AVAILABILITY"
                description="GSD_PRODUCT_AVAILABILITY_DESC">
                <subform>
                    <field name="fixed" type="list"
                        hiddenLabel="true"
                        showon="option:fixed"
                        default="http://schema.org/InStock">
                        <option value="http://schema.org/Discontinued">GSD_AVAILABILITY_DISCONTINUED</option>
                        <option value="http://schema.org/InStock">GSD_AVAILABILITY_IN_STOCK</option>
                        <option value="http://schema.org/InStoreOnly">GSD_AVAILABILITY_INSTORE_ONLY</option>
                        <option value="http://schema.org/LimitedAvailability">GSD_AVAILABILITY_LIMITED_AVAILABILITY</option>
                        <option value="http://schema.org/OnlineOnly">GSD_AVAILABILITY_ONLINE_ONLY</option>
                        <option value="http://schema.org/OutOfStock">GSD_AVAILABILITY_OUT_OF_STOCK</option>
                        <option value="http://schema.org/PreOrder">GSD_AVAILABILITY_PRE_ORDER</option>
                        <option value="http://schema.org/PreSale">GSD_AVAILABILITY_PRE_SALE</option>
                        <option value="http://schema.org/SoldOut">GSD_AVAILABILITY_SOLDOUT</option>
                    </field>
                </subform>
            </field>
            <field name="item_reviewed_product_pricevaliduntil" type="mapdate"
                showon="itemReviewedType:Product"
                label="GSD_PRODUCT_PRICE_VALID_DATE"
                description="GSD_PRODUCT_PRICE_VALID_DATE_DESC"
            />
            <field type="spacer" name="label1" label="GSD_REVIEW_ITEM_BOOK" showon="itemReviewedType:Book"/>
            <!-- Book -->
            <field name="item_reviewed_book_author" type="map"
                label="GSD_REVIEW_BOOK_AUTHOR"
                description="GSD_REVIEW_BOOK_AUTHOR_DESC"
                hint="Paulo Coelho"
                showon="itemReviewedType:Book"
                required="false"
            />
            <field name="item_reviewed_book_author_url" type="map"
                label="GSD_REVIEW_BOOK_AUTHOR_URL"
                description="GSD_REVIEW_BOOK_AUTHOR_URL_DESC"
                showon="itemReviewedType:Book"
                hint="http://"
                required="false"
            />
            <field name="item_reviewed_book_isbn" type="map"
                label="ISBN"
                description="GSD_REVIEW_BOOK_ISBN_DESC"
                hint="0760351899"
                showon="itemReviewedType:Book"
                required="false"
            />
            <!-- Movie -->
            <field type="spacer" name="label2" label="GSD_REVIEW_ITEM_MOVIE" showon="itemReviewedType:Movie"/>
            <field name="item_reviewed_movie_director" type="map"
                label="GSD_REVIEW_MOVIE_DIRECTOR"
                description="GSD_REVIEW_MOVIE_DIRECTOR_DESC"
                hint="Rob Marshall"
                showon="itemReviewedType:Movie"
                required="false"
            />
            <field name="actors" type="map"
                label="GSD_ACTORS"
                description="GSD_ACTORS_DESC"
                showon="itemReviewedType:Movie">
                <subform>
                    <field name="custom" type="subform"
                        layout="joomla.form.field.subform.repeatable-table"
                        multiple="true">
                        <form>
                            <field name="name" type="text"
                                label="GSD_ACTOR_NAME"
                            />
                        </form>
                    </field>
                </subform>
            </field>
            <!-- Local Business -->
            <field type="spacer" name="label3" label="GSD_REVIEW_ITEM_LOCALBUSINESS" showon="itemReviewedType:LocalBusiness"/>
            <field name="priceRange" type="map"
                label="GSD_REVIEW_PRICERANGE"
                description="GSD_REVIEW_PRICERANGE_DESC"
                hint="$10 - $1000"
                showon="itemReviewedType:LocalBusiness"
                required="false"
            />
            <field name="telephone" type="map"
                label="GSD_REVIEW_TELEPHONE"
                description="GSD_REVIEW_TELEPHONE_DESC"
                hint="+47 73904011"
                showon="itemReviewedType:LocalBusiness"
                required="false"
            />
            <field name="item_reviewed_published_date" type="mapdate"
                label="GSD_DATE_PUBLISHED"
                description="GSD_REVIEW_DATE_PUBLISHED_DESC"
                showon="itemReviewedType:Movie,Book"
                required="false"
            />
            <field name="itemReviewedURL" type="map"
                label="NR_URL"
                description="GSD_REVIEW_ITEM_URL_DESC"
                showon="itemReviewedType:LocalBusiness,Movie,Book,Product"
                hint="http://"
                required="false"
            />
            <field type="spacer" name="label_address" label="GSD_REVIEW_ADDRESS" showon="itemReviewedType:LocalBusiness"/>
            <field name="address" type="map"
                label="GSD_REVIEW_ADDRESS"
                description="GSD_REVIEW_ADDRESS_DESC"
                showon="itemReviewedType:LocalBusiness"
                required="false"
            />
            <field name="addressCountry" type="map"
                label="GSD_JOBPOSTING_COUNTRY"
                description="GSD_LOCALBUSINESS_COUNTRY_DESC"
                hint="US"
                showon="itemReviewedType:LocalBusiness">
                <subform>
                    <field name="fixed" type="nr_geo"
                        showon="option:fixed"
                        showselect="false"
                    />
                </subform>
            </field>
            <field name="addressLocality" type="map"
                label="GSD_BUSINESSLISTING_ADDRESS_LOCALITY"
                description="GSD_LOCALBUSINESS_CITY_DESC"
                hint="GSD_BUSINESSLISTING_ADDRESS_LOCALITY_HINT"
                showon="itemReviewedType:LocalBusiness"
            />
            <field name="addressRegion" type="map"
                label="GSD_BUSINESSLISTING_ADDRESS_REGION"
                description="GSD_LOCALBUSINESS_REGION_DESC"
                hint="GSD_BUSINESSLISTING_ADDRESS_REGION_HINT"
                showon="itemReviewedType:LocalBusiness"
            />
            <field name="postalCode" type="map"
                label="GSD_BUSINESSLISTING_POSTAL_CODE"
                description="GSD_LOCALBUSINESS_POSTALCODE_DESC"
                hint="GSD_BUSINESSLISTING_POSTAL_CODE_HINT"
                showon="itemReviewedType:LocalBusiness"
            />
        </fields>
    </fieldset>
</form>models/forms/contenttypes/service.xml000064400000010142152345574300014104 0ustar00<?xml version="1.0" encoding="utf-8"?>
<form>
    <fieldset name="contenttype">
        <fields name="service">
            <field name="headline" type="map"
                label="NR_TITLE"
                description="GSD_SERVICE_TITLE_DESC"
                hint="Service title"
                default="gsd.item.headline"
                custom_class="input-xlarge"
                required="true"
            />
            <field name="description" type="map"
                label="GSD_ITEM_DESC"
                description="GSD_SERVICE_DESCRIPTION"
                default="gsd.item.description"
                custom_class="input-xlarge"
            />
            <field name="image" type="mapimage"
                label="NR_IMAGE"
                description="GSD_SERVICE_IMAGE_DESC"
                default="gsd.item.image"
            />
            <field type="spacer" name="label" label="GSD_SERVICE_PRICE_HEADING" />
            <field name="offerPrice" type="map"
                label="GSD_PRODUCT_OFFER_PRICE"
                description="GSD_SERVICE_OFFER_PRICE_DESC"
                hint="29.50"
                default="gsd.item.offerprice"
            />
            <field name="currency" type="map"
                label="GSD_PRODUCT_OFFER_CURRENCY"
                description="GSD_PRODUCT_OFFER_CURRENCY_DESC"
                hint="USD"
                default="gsd.item.currency">
                <subform>
                    <field name="fixed" type="nr_currencies"
                        showon="option:fixed"
                        showselect="false"
                    />
                </subform>
            </field>
            <field type="spacer" name="label2" label="GSD_SERVICE_PROVIDER_HEADING" />
            <field name="provider_type" type="list" 
                label="GSD_TYPE" 
                description="GSD_SERVICE_PROVIDER_TYPE_DESC"
                default="Organization">
                    <option value="Organization">GSD_ORGANIZATION</option>
                    <option value="Person">GSD_PERSON</option>
            </field>
            <field name="provider_name" type="map"
                label="NR_NAME"
                description="GSD_SERVICE_PROVIDER_NAME_DESC"
                hint=""
                custom_class="input-xlarge"
            />
            <field name="provider_image" type="mapimage"
                label="NR_IMAGE"
                description="GSD_SERVICE_PROVIDER_IMAGE_DESC"
            />
            <field name="provider_phone" type="map"
                label="GSD_PHONE"
                description="GSD_SERVICE_PHONE_DESC"
                hint="GSD_BUSINESSLISTING_TELEPHONE_HINT"
            />
            <field name="provider_country" type="map"
                label="NR_ASSIGN_COUNTRIES"
                description="GSD_SERVICE_COUNTRY_DESC"
                hint="US">
                <subform>
                    <field name="fixed" type="nr_geo"
                        showon="option:fixed"
                        showselect="false"
                    />
                </subform>
            </field>
            <field name="provider_city" type="map"
                label="NR_CITY"
                description="GSD_SERVICE_CITY_DESC"
                hint="Athens"
            />
            <field name="provider_streetAddress" type="map"
                label="GSD_BUSINESSLISTING_STREET_ADDRESS"
                description="GSD_SERVICE_STREET_ADDRESS_DESC"
                hint="GSD_BUSINESSLISTING_STREET_ADDRESS_HINT"
            />
            <field name="provider_addressRegion" type="map"
                label="GSD_BUSINESSLISTING_ADDRESS_REGION"
                description="GSD_SERVICE_REGION_DESC"
                hint="GSD_BUSINESSLISTING_ADDRESS_REGION_HINT"
            />
            <field name="provider_postalCode" type="map"
                label="GSD_BUSINESSLISTING_POSTAL_CODE"
                description="GSD_SERVICE_POSTALCODE_DESC"
                hint="GSD_BUSINESSLISTING_POSTAL_CODE_HINT"
            />
        </fields>
    </fieldset>
</form>models/forms/contenttypes/video.xml000064400000004221152345574300013553 0ustar00<?xml version="1.0" encoding="utf-8"?>
<form>
    <fieldset name="contenttype">
        <fields name="video">
            <field name="headline" type="map"
                label="NR_TITLE"
                description="GSD_VIDEO_TITLE_DESC"
                hint="How to create a smoothie in 5 mins"
                default="gsd.item.headline"
                custom_class="input-xlarge"
                required="true"
            />
            <field name="description" type="map"
                label="GSD_ITEM_DESC"
                description="GSD_ITEM_DESC_DESC"
                default="gsd.item.description"
                custom_class="input-xlarge"
                required="true"
            />
            <field name="image" type="mapimage"
                label="NR_IMAGE"
                description="GSD_IMAGE_OPTION_DESC"
                default="gsd.item.image"
                required="true"
            />
            <field name="contentUrl" type="map"
                label="GSD_VIDEO_CONTENTURL"
                description="GSD_VIDEO_CONTENTURL_DESC"
                custom_class="input-xlarge"
                hint="https://www.example.com/video123.flv"
            />
            <field name="embedUrl" type="map"
                label="GSD_VIDEO_EMBED_URL"
                description="GSD_VIDEO_EMBED_URL_DESC"
                custom_class="input-xlarge"
                hint="https://www.youtube.com/embed/abcdefg"
            />
            <field name="transcript" type="map"
                label="GSD_VIDEO_TRANSCRIPT"
                description="GSD_VIDEO_TRANSCRIPT_DESC"
                custom_class="input-xlarge">
                <subform>
                    <field name="custom" type="textarea"
                        class="input-xlarge"
                        rows="5"
                    />
                </subform>
            </field>
            <field name="publish_up" type="mapdate"
                label="GSD_DATE_PUBLISHED"
                description="GSD_DATE_PUBLISHED_DESC"
                default="gsd.item.publish_up"
            />
        </fields>
    </fieldset>
</form>models/forms/fields/addons.php000064400000006505152345574300012422 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\Registry\Registry;

require_once JPATH_PLUGINS . '/system/nrframework/helpers/field.php';

class JFormFieldAddons extends NRFormField
{
    /**
     *  Indicates if the field is being referred to plugins
     *
     *  @var  bool
     */
    private $pluginmode;

    /**
     *  Disable input's label
     *
     *  @return  null
     */
    protected function getLabel()
    {
        return;
    }

    /**
     * Method to get a list of options for a list input.
     *
     * @return   string
     */
    protected function getInput()
    {
        $this->pluginmode = $this->get('pluginmode', true) === true;

        if ($this->pluginmode)
        {
            HTMLHelper::_('script', 'com_gsd/addons.js', array('relative' => true, 'version' => 'auto'));
        }

        return LayoutHelper::render('addons', [ 'items' => $this->getItems()]);
    }

    /**
     *  Get addons data from the xml file
     *
     *  @return  array
     */
    private function getItems()
    {
        // Load XML file
        $xmlfile = JPATH_COMPONENT_ADMINISTRATOR . '/models/forms/' . $this->get('xmlfile') . '.xml';

        if (!$xmlItems = simplexml_load_file($xmlfile))
        {
            return;
        }

        $items = array();
        $itemsComingSoon = array();

        foreach ($xmlItems as $key => $item)
        {
            $item = (array) $item;
            $item = new Registry($item["@attributes"]);

            $enabled = false;
            $id      = null;
            $name    = $item->get('name');

            if ($this->pluginmode && ($plugin = NRFramework\Extension::get($name, 'plugin', 'gsd')))
            {
                $enabled = $plugin['enabled'] == 1 ? true : false;
                $id      = $plugin['state'] > -1 ? $plugin['extension_id'] : $id;
            }

            $obj = array(
                'id'          => $id,
                'name'        => $name,
                'isEnabled'   => $enabled,
                'label'       => $item->get('label'),
                'image'       => !empty($item->get('icon')) ? 'https://www.tassos.gr/images/icons/' . $item->get('icon') . '.svg' : 'https://www.tassos.gr/images/integrations/gsd/' . $name . '.png',
                'description' => $item->get('description'),
                'docalias'    => $item->get('docalias'),
                'proonly'     => $item->get('proonly', true),
                'comingsoon'  => $item->get('comingsoon', false)
            );

            if ($obj['comingsoon'])
            {
                $itemsComingSoon[$name] = $obj;
            } else 
            {
                $items[$name] = $obj;
            }
        }

        // Sort by key value both arrays
        ksort($items);
        ksort($itemsComingSoon);

        return array_merge($items, $itemsComingSoon);
    }
}models/forms/fields/businesstypes.php000064400000010357152345574300014072 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */


defined('_JEXEC') or die;

use Joomla\CMS\Form\Field\ListField;
use Joomla\CMS\Language\Text;

class JFormFieldBusinessTypes extends ListField
{
	/**
	 *  Dropdown options array
	 *
	 *  @var  array
	 */
	private $options;

	/**
	 *  Schema.org Local Business Types
	 *
	 *  https://schema.org/docs/full.html#LocalBusiness
	 *
	 *  @var  array
	 */
	private $businessTypes = array(
		'AnimalShelter',
		'AutomotiveBusiness' => array(
			'AutoBodyShop',
			'AutoDealer',
			'AutoPartsStore',
			'AutoRental',
			'AutoRepair',
			'AutoWash',
			'GasStation',
			'MotorcycleDealer',
			'MotorcycleRepair'
		),
		'ChildCare',
		'DryCleaningOrLaundry',
		'EducationalOrganization' => [
			'CollegeOrUniversity',
			'ElementarySchool',
			'HighSchool',
			'MiddleSchool',
			'Preschool',
			'School'
		],
		'EmergencyService' => array(
			'FireStation',
			'Hospital',
			'PoliceStation'
		),
		'EmploymentAgency',
		'EntertainmentBusiness' => array(
			'AdultEntertainment',
			'AmusementPark',
			'ArtGallery',
			'Casino',
			'ComedyClub',
			'MovieTheater',
			'NightClub'
		),
		'FinancialService' => array(
			'AccountingService',
			'AutomatedTeller',
			'BankOrCreditUnion',
			'InsuranceAgency'
		),
		'FoodEstablishment' => [
			'Bakery',
			'BarOrPub',
			'Brewery',
			'CafeOrCoffeeShop',
			'FastFoodRestaurant',
			'IceCreamShop',
			'Restaurant',
			'Winery'
		],
		'GovernmentOffice' => array(
			'PostOffice'
		),
		'HealthAndBeautyBusiness' => array(
			'BeautySalon',
			'DaySpa',
			'HairSalon',
			'HealthClub',
			'NailSalon',
			'TattooParlor'
		),
		'HomeAndConstructionBusiness' => array(
			'Electrician',
			'GeneralContractor',
			'HVACBusiness',
			'HousePainter',
			'Locksmith',
			'MovingCompany',
			'Plumber',
			'RoofingContractor'
		),
		'InternetCafe',
		'LegalService' => array(
			'Attorney',
			'Notary'
		),
		'Library',
		'LocalBusiness',
		'LodgingBusiness'=> array(
			'BedAndBreakfast',
			'Campground',
			'Hostel',
			'Hotel',
			'Motel',
			'Resort'
		),
		'MedicalBusiness' => array(
			'Dentist',
			'Pharmacy',
			'Physician',
			'Optician'
		),
		'ProfessionalService',
		'RadioStation',
		'RealEstateAgent',
		'RecyclingCenter',
		'SelfStorage',
		'ShoppingCenter',
		'SportsActivityLocation' => array(
			'BowlingAlley',
			'ExerciseGym',
			'GolfCourse',
			'PublicSwimmingPool',
			'SkiResort',
			'SportsClub',
			'StadiumOrArena',
			'TennisComplex'
		),
		'Store' => array(
			'BikeStore',
			'BookStore',
			'ClothingStore',
			'ComputerStore',
			'ConvenienceStore',
			'DepartmentStore',
			'ElectronicsStore',
			'Florist',
			'FurnitureStore',
			'GardenStore',
			'GroceryStore',
			'HardwareStore',
			'HobbyShop',
			'HomeGoodsStore',
			'JewelryStore',
			'LiquorStore',
			'MedicalClinic',
			'MensClothingStore',
			'MobilePhoneStore',
			'MovieRentalStore',
			'MusicStore',
			'OfficeEquipmentStore',
			'OutletStore',
			'PawnShop',
			'PetStore',
			'ShoeStore',
			'SportingGoodsStore',
			'TireShop',
			'ToyStore',
			'WholesaleStore'
		),
		'TelevisionStation', 
		'TouristInformationCenter', 
		'TravelAgency'
	);

	/**
	 *  Returns all options to dropdown field
	 *
	 *  @return  array
	 */
	protected function getOptions()
	{
		return array_merge(parent::getOptions(), $this->buildTree($this->businessTypes));
	}

	/**
	 *  Recursive traversal of the businessTypes array tree
	 *
	 *  @param   Array    $types   The business types
	 *  @param   integer  $level   The array level
	 *
	 *  @return  array
	 */
	private function buildTree($types, $level = 0)
	{
		foreach ($types as $key => $type)
		{
			$hasChildren = is_array($type);
			$typeName    = $hasChildren ? $key : $type;
			$typeText    = str_repeat('- ', $level) . Text::_('GSD_BUSINESSLISTING_TYPE_' . $typeName);

			$this->options[] = array(
				'value'    => $typeName,
				'text'     => $typeText,
				'selected' => ($this->value == $typeName)
			);

			if ($hasChildren)
			{
				$this->buildTree($type, $level + 1);
			}
		}

		return $this->options;
	}
}models/forms/fields/contenttypes.php000064400000005463152345574300013713 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\Form\Field\GroupedlistField;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Factory;

class JFormFieldContentTypes extends GroupedlistField
{
    protected $layout = 'joomla.form.field.groupedlist-fancy-select';
    
    /**
     * Method to get a list of options for a list input.
     *
     * @return      array           An array of options.
     */
    protected function getGroups()
    {
        $contentTypes = GSD\Helper::getContentTypes();

        $groups = [];
        $showselect = (string) $this->element['showselect'];
        if ($showselect === 'true')
        {
            $groups[''][] = HTMLHelper::_('select.option', '', '- ' . Text::_('GSD_CONTENT_TYPE_SELECT') . ' -');
        }

        foreach ($contentTypes as $contentType)
        {
            $groups[''][] = HTMLHelper::_('select.option', $contentType, Text::_('GSD_' . strtoupper($contentType)));
        }

        return $groups;
    }

    protected function getInput()
    {
		$this->class = '" search-placeholder="' . Text::_('GSD_SEARCH_CONTENT_TYPES');

        $showhelp = (string) $this->element['showhelp'];
        if ($showhelp === 'false')
        {
            return parent::getInput();
        }

        $doc = Factory::getDocument();
        $doc->addScriptDeclaration('
            document.addEventListener("DOMContentLoaded", function() {
                const select = document.querySelector("#' . $this->id . '");

                select.addEventListener("change", function(e) {
                    gsd_set_help_value(e.target.value);
                });
                
                // set initialvalue to help URL
                gsd_set_help_value(select.value);

                function gsd_set_help_value(content_type) {
                    href = "https://www.tassos.gr/joomla-extensions/google-structured-data-markup/docs/" + content_type.replace("_", "") + "-schema";
                    document.querySelector(".contentTypeHelp").href = href;
                }
            });
        ');

        return '
            <div class="d-flex gap-1"> ' . parent::getInput() . '
                <a class="btn btn-secondary contentTypeHelp" target="_blank" title="' . Text::_('GSD_CONTENTTYPE_HELP') . '">
                    <span class="icon-help" style="margin-right:0;"></span>
                </a>
            </div>
        ';
    }
}models/forms/fields/fastedit.php000064400000014414152345574300012753 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;

JLoader::register('NRFormField', JPATH_PLUGINS . '/system/nrframework/helpers/field.php');

class JFormFieldFastEdit extends NRFormField
{
    /**
     *  Disable field label
     *
     *  @return  boolean
     */
    protected function getLabel()
    {
        return false;
    }

    /**
     * Method to get a list of options for a list input.
     *
     * @return      array           An array of options.
     */
    protected function getInput()
    {
        // Load framework language file
        NRFramework\Functions::loadLanguage();

        $thing = $this->get('thing', 0);
        $error = $this->get('error', 'GSD_SAVE_FIRST');
        $html  = '';

        if (!defined('nrJ4'))
        {
            HTMLHelper::stylesheet('com_gsd/joomla3.css', ['relative' => true, 'version' => 'auto']);
        }

        HTMLHelper::_('stylesheet', 'com_gsd/fastedit.css', array('version' => 'auto', 'relative' => true));

        // In order to be able to assosiate a snippet the item must be saved first.
        if (!$thing)
        {
            return $html . '<div class="alert alert-info gsdFastEdit">' . Text::_($error) . '</div>';
        }

        // Cool. The item is saved.
        $plugin = $this->get('plugin', null);
        $contentTypes = GSD\Helper::getContentTypes();

        $query = [
            'override_item' => true,
            'title' => $this->get('thing_title'),
            'plugin' => $plugin,
            'assignments' => [
                $this->get('plugin_assignment_name') => [
                    'assignment_state' => 1,
                    'selection' => $thing
                ]
            ]
        ];

        $addURL = Route::_('index.php?option=com_gsd&tmpl=component&layout=modal&view=item&' . http_build_query($query));

        // Add Media
        HTMLHelper::_('script', 'com_gsd/fastedit.js', ['version' => 'auto', 'relative' => true]);

        // Add language strings used by the JS plugin
        Text::script('GSD_ADD_SNIPPET');
        Text::script('GSD_EDIT_SNIPPET');
        Text::script('NR_ARE_YOU_SURE');

        Factory::getDocument()->addScriptOptions('gsd_fastedit', [
            'base_url'    => Uri::base(true),
            'add_url'     => $addURL,
            'plugin'      => $plugin,
            'thing_id'    => $thing,
            'thing_catid' => $this->get('thing_catid'),
            'plugin_assignment_name' => $this->get('plugin_assignment_name')
        ]);

        $html .= '<div class="gsdFastEdit" data-src="'. $addURL . '">
                ' . $this->renderModal() . '

                <div class="fast-edit-dropdown">
                <button class="btn btn-success dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" data-toggle="dropdown">
                    <span class="icon-new"></span>'
                    . Text::_('GSD_ADD_SNIPPET') .'
                </button>
                <ul class="dropdown-menu" aria-labelledby="dropdownMenuButton">
             ';

        foreach ($contentTypes as $contentType) 
        {
            $html .= '<li><a data-contenttype="' . $contentType . '" data-bs-toggle="modal" data-toggle="modal" href="#gsdModal">' . Text::_('GSD_' . strtoupper($contentType)) . '</a></li>';
        }

        $html .= '</ul></div><div class="items"></div></div>';

        return $html;
    }

    /**
     *  Render the modal is going to be used by all buttons
     *
     *  @return  string
     */
    private function renderModal()
    {
        $options = array(
            'title'       => Text::_('GSD_EDIT_SNIPPET'),
            'url'         => '#',
            'height'      => '400px',
            'width'       => '800px',
            'backdrop'    => 'static',
            'bodyHeight'  => '70',
            'modalWidth'  => '70',
            'footer'      => '<a type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-dismiss="modal" aria-hidden="true">'
                    . Text::_('JLIB_HTML_BEHAVIOR_CLOSE') . '</a>'
                    . '<button type="button" class="btn btn-primary" aria-hidden="true"'
                    . ' onclick="document.querySelector(\'#gsdModal iframe\').contentDocument.querySelector(\'#saveBtn\').click();">'
                    . Text::_('JSAVE') . '</button>'
                    . '<button type="button" class="btn btn-success" aria-hidden="true"'
                    . ' onclick="document.querySelector(\'#gsdModal iframe\').contentDocument.querySelector(\'#applyBtn\').click();">'
                    . Text::_('JAPPLY') . '</button>',
        );

        return HTMLHelper::_('bootstrap.renderModal', 'gsdModal', $options);
    }

    public function getRows()
    {
        $input = Factory::getApplication()->input;

        $thing      = $input->getInt('thing');
        $plugin     = $input->get('plugin');
        $assignment = $input->get('assignment');

        $model = BaseDatabaseModel::getInstance('Items', 'GSDModel', ['ignore_request' => true]);
        $model->setState('list.limit', 0);
        $model->setState('filter.thing', $thing);
        $model->setState('filter.plugin', $plugin);

        $items = $model->getItems();

        foreach ($items as $key => &$item)
        {
            if (!isset($item->assignments) || 
                !isset($item->assignments->$assignment) || 
                !isset($item->assignments->$assignment->selection) ||
                $item->assignments->$assignment->assignment_state !== '1' ||
                !in_array($thing, (array) $item->assignments->$assignment->selection))
            {
                unset($items[$key]);
            }
        }

        return $items;
    }
}models/forms/fields/integrations.php000064400000003514152345574300013655 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use GSD\Helper;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Form\Field\GroupedlistField;
use Joomla\CMS\Language\Text;

class JFormFieldIntegrations extends GroupedlistField
{
    protected $layout = 'joomla.form.field.groupedlist-fancy-select';

    /**
     * Method to get a list of options for a list input.
     *
     * @return      array           An array of options.
     */
    protected function getGroups()
    {
        $groups = [];
        
        // Get a list with all available plugins
        $plugins = Helper::getPlugins();

        $showselect = (string) $this->element['showselect'];
        if ($showselect === 'true')
        {
            $groups[''][] = HTMLHelper::_('select.option', '', '- ' . Text::_('GSD_INTEGRATION_SELECT') . ' -');
        } else 
        {
            // If we don't have a value get the default plugin
            $this->value = empty($this->value) ? Helper::getDefaultPlugin() : $this->value;
        }

        // Sort alphabetically
        asort($plugins);
        
        foreach ($plugins as $option)
        {
            $groups[''][] = HTMLHelper::_('select.option', $option["alias"], $option["name"]);
        }

        return $groups;
    }
    
    protected function getInput()
    {
		$this->class = '" search-placeholder="' . Text::_('GSD_SEARCH_INTEGRATIONS');

        return '<div class="d-flex gap-1"> ' . parent::getInput() . '</div>';
    }
}models/forms/fields/map.php000064400000013367152345574300011733 0ustar00<?php
/**
 * @author          Tassos.gr <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\Form\FormField;

class JFormFieldMap extends FormField
{
    protected function getLabel()
    {
        return;
    }

    protected function getInput()
    {
        $subform_class = (string) $this->element['subform_class'];
        $label = (string) $this->element['label'];
        $description = (string) $this->element['description'];
        $name = (string) $this->element['name'];

        $subform_classes = $subform_class ? ' ' . $subform_class : '';

        $group_parts = explode('.', $this->group);
        $contentType = end($group_parts);
        $this->group .= '.' . $name;

        $xml = new SimpleXMLElement('
            <fields name="' . $contentType . '">
                <fields name="' . $name . '">
                    ' . $this->getOptionField() . '
                    ' . $this->getSubform() . '
                </fields>
            </fields>
        ');

        $this->form->setField($xml);

        // Render XML
        foreach ($xml->fields->field as $key => $field)
        {
            $name = $field->attributes()->name;
            $html[] = $this->form->renderField($name, $this->group);
        }

        return '
            <div class="subform' . $subform_classes . '">
                <div class="subform-parent"> ' . $html[0] . ' </div>
                <div class="subform-child"> ' . implode('', array_slice($html, 1)) . ' </div>
            </div>
        ';
    }

    private function elementHasOption($option_value)
    {
        $options = $this->element->option;

        foreach ($options as $key => $option)
        {
            if ((string) $option->attributes()->value == $option_value)
            {
                return true;
            }
        }

        return false;
    }

    private function subformHasField($name = 'custom')
    {
        if (!$el = $this->element->subform)
        {
            return;
        }

        $fields = $el->children();

        foreach ($fields as $key => $field)
        {
            $field_name = (string) $field->attributes()->name;

            if ($field_name == $name)
            {
                return true;
            }
        }

        return false;
    }

    private function getSubform()
    {
        $xml = '';

        $customFieldMultiple = (bool) $this->element['custom_value_multiple'];

        if ($customFieldMultiple)
        {
            $xml_custom_field = '
                <field name="custom" type="textarea" 
                    showon="option:_custom_"
                    hiddenLabel="true"
                    filter="raw"
                    rows="10"
                    class="' . (string) $this->element['custom_class'] . '"
                    hint="You can enter multiple values, each on a separate line."
                />
            ';
        } else
        {
            $xml_custom_field = '
                <field name="custom" type="text" 
                    showon="option:_custom_"
                    hiddenLabel="true"
                    filter="raw"
                    class="' . (string) $this->element['custom_class'] . '"
                    hint="' . (string) $this->element['hint'] . '"
                />
            ';
        }

        if (!$this->subformHasField())
        {
            $xml = $xml_custom_field;
        }

        if ($subform = $this->element->subform)
        {
            if (count($subform->children()) == 1)
            {
                $el = $subform->children()->field;

                // Add missing showon attribute
                if (is_null($el->attributes()->showon))
                {
                    $el->addAttribute('showon', 'option:_custom_');
                }

                // Add missing hiddenLabel attribute
                if (is_null($el->attributes()->hiddenLabel))
                {
                    $el->addAttribute('hiddenLabel', true);
                }
            }

            $xml .= $subform->children()->asXml();
        }

        
        // Add CSS Selector options
        $xml .= '
            <field name="css_selector" type="cssselector" 
                showon="option:_css_selector_"
                hiddenLabel="true"
            />
        ';
        

        return $xml;
    }

    private function getOptionField()
    {
        $el = clone $this->element;

        $el->attributes()->name = 'option';
        $el->attributes()->type = 'mappingoptions';

        if ($el['required'])
        {
            $el->attributes()->required = (string) $el['required'];
        }

        unset($el->attributes()->showon);
        unset($el->subform);

        // Add Fixed option
        if ($this->subformHasField('fixed') && !$this->elementHasOption('fixed'))
        {
            $fixed = $el->addChild('option', 'Fixed Option');
            $fixed->addAttribute('value', 'fixed');
        }

        $show_custom_value = is_null($this->element['hidecustomvalue']) ? true : false;
        if ($show_custom_value)
        {
            // Add Custom Value option
            $custom = $el->addChild('option', 'Custom Value');
            $custom->addAttribute('value', '_custom_');
        }

        
        // CSS Selector option
        $fixed = $el->addChild('option', 'NR_CSS_SELECTOR');
        $fixed->addAttribute('value', '_css_selector_');
        

        return $el->asXml();
    }
}models/forms/fields/mapdate.php000064400000003006152345574300012556 0ustar00<?php
/**
 * @author          Tassos.gr <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

defined('_JEXEC') or die('Restricted access');

require_once __DIR__ . '/map.php';

class JFormFieldMapDate extends JFormFieldMap
{
    protected function getInput()
    {
        $el = $this->element;

        $dateFormat = $el->attributes()->format ? (string) $el->attributes()->format : '%Y-%m-%d %H:%M:%S';
        
        // Add Fixed option
        $fixed = $el->addChild('option', 'GSD_FIXED_DATE');
        $fixed->addAttribute('value', 'fixed');
        
        // Create subform
        $subform = $el->addChild('subform');
        
        // Add Calendar field to subform
        $calendar = $subform->addChild('field');
        
        $calendar->addAttribute('name', 'fixed');
        $calendar->addAttribute('hiddenLabel', 'true');
        $calendar->addAttribute('type', 'calendar');
        $calendar->addAttribute('hint', '0000-00-00 00:00:00');
        $calendar->addAttribute('showtime', 'true');
        $calendar->addAttribute('filter', 'user_utc'); // Date is supposed to be stored in UTC in the database
        $calendar->addAttribute('showon', 'option:fixed');
        $calendar->addAttribute('weeknumbers', 'false');
        $calendar->addAttribute('format', $dateFormat);

        return parent::getInput();
    }
}models/forms/fields/mapimage.php000064400000002355152345574300012731 0ustar00<?php
/**
 * @author          Tassos.gr <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

defined('_JEXEC') or die('Restricted access');

require_once __DIR__ . '/map.php';

class JFormFieldMapImage extends JFormFieldMap
{
    protected function getInput()
    {
        $el = $this->element;

        $el->addAttribute('hint', '/images/post.png');
        $el->addAttribute('custom_class', 'input-xlarge');

        // Add Fixed option
        $fixed = $el->addChild('option', 'GSD_FIXED_IMAGE');
        $fixed->addAttribute('value', 'fixed');

        // Create subform
        $subform = $el->addChild('subform');

        // Add Media field to subform
        $media = $subform->addChild('field');

        $media->addAttribute('name', 'fixed');
        $media->addAttribute('hiddenLabel', 'true');
        $media->addAttribute('type', 'media');
        $media->addAttribute('showon', 'option:fixed');
        $media->addAttribute('preview', 'tooltip');
        $media->addAttribute('class', 'input-xlarge');

        return parent::getInput();
    }
}models/forms/fields/mappingoptions.php000064400000010067152345574300014217 0ustar00<?php
/**
 * @author          Tassos.gr <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

defined('_JEXEC') or die('Restricted access');

use GSD\Helper;
use NRFramework\Cache;
use GSD\MappingOptions;
use Joomla\CMS\Form\Field\GroupedlistField;
use Joomla\CMS\Language\Text;
use Joomla\CMS\HTML\HTMLHelper;

class JFormFieldMappingOptions extends GroupedlistField
{
    protected $layout = 'joomla.form.field.groupedlist-fancy-select';

    /**
	 * Method to get the field option groups.
	 *
	 * @return  array  The field option objects as a nested array in groups.
	 *
	 * @since   1.6
	 */
	protected function getGroups()
	{
		$options = MappingOptions::$options;

        // Add empty option
		$groups[''] = [
			HTMLHelper::_('select.option', '', Text::_('GSD_PLEASE_SELECT')),
		];

		// Add disable option
		if (!$this->required)
		{
			$text = '- ' . Text::_('JDISABLED') . ' -';
			$groups[''][] = HTMLHelper::_('select.option', '_disabled_', $text);
		}

        // Load plugin-based options
		$plugin = $this->form->getData()->get('plugin');
		Helper::event('onMapOptions', [$plugin, &$options]);

        // Load XML-based options
        if ($xmlGroups = $this->getGroupsFromXML())
        {
            $options = array_merge_recursive($options, $xmlGroups);
        }

		foreach ($options as $name => $group)
		{
			$name = Text::_($name);

			// Initialize the group if necessary.
			if (!isset($groups[$name]))
			{
				$groups[$name] = array();
			}

			foreach ($group as $key => $option)
			{
				$groups[$name][] = HTMLHelper::_('select.option', strtolower($key), Text::_($option));
			}
        }

		return $groups;
    }

    protected function getInput()
    {
		$this->value = strtolower($this->value);
		$this->class = '" search-placeholder="' . Text::_('GSD_SEARCH');

		if (!is_null($this->element['required']))
		{
			$this->required = $this->element['required'] == 'false' ? false : true;
		}

		$data = $this->form->getData();
		$isUnsaved = is_null($data->get($this->group));

		// Allow user to select null values and especially the '- Please Select -' option for non required fields
		if (!$this->required && !$isUnsaved)
		{
			$real_value = $data->get($this->group . '.option');

			if (is_null($real_value))
			{
				$this->value = '';
			}
		}
		
        return parent::getInput();
    }
    
    /**
     * Method to get the field option groups.
     *
     * @return  array  The field option objects as a nested array in groups.
     *
     * @since   11.1
     */
    private function getGroupsFromXML()
    {
		$groups = array();
		$label = 'Custom Info';

		foreach ($this->element->children() as $element)
		{
			switch ($element->getName())
			{
				// The element is an <option />
                case 'option':
                
					// Initialize the group if necessary.
					if (!isset($groups[$label]))
					{
						$groups[$label] = array();
					}

					// Add the option.
					$groups[$label][(string) $element['value']] = (string) $element;
					break;

				// The element is a <group />
				case 'group':
					// Get the group label.
					if ($groupLabel = (string) $element['label'])
					{
						$label = Text::_($groupLabel);
					}

					// Initialize the group if necessary.
					if (!isset($groups[$label]))
					{
						$groups[$label] = array();
					}

					// Iterate through the children and build an array of options.
					foreach ($element->children() as $option)
					{
						// Only add <option /> elements.
						if ($option->getName() != 'option')
						{
							continue;
                        }

						// Add the option.
						$groups[$label][(string) $option['value']] = (string) $option;
					}

					if ($groupLabel)
					{
						$label = count($groups);
					}
					break;
				default:
			}
		}

		reset($groups);

		return $groups;
    }
}models/forms/fields/mapuser.php000064400000002067152345574300012625 0ustar00<?php
/**
 * @author          Tassos.gr <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

defined('_JEXEC') or die('Restricted access');

require_once __DIR__ . '/map.php';

class JFormFieldMapUser extends JFormFieldMap
{
    protected function getInput()
    {
        $el = $this->element;

        // Add Fixed option
        $fixed = $el->addChild('option', 'GSD_FIXED_USER');
        $fixed->addAttribute('value', 'fixed');

        // Create subform
        $subform = $el->addChild('subform');

        // Add Calendar field to subform
        $user = $subform->addChild('field');

        $user->addAttribute('name', 'fixed');
        $user->addAttribute('hiddenLabel', 'true');
        $user->addAttribute('type', 'user');
        $user->addAttribute('showon', 'option:fixed');
        $user->addAttribute('hint', 'John Doe');

        return parent::getInput();
    }
}models/forms/fields/openinghours.php000064400000012164152345574300013670 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

defined('_JEXEC') or die;

use Joomla\CMS\Form\Field\ListField;
use Joomla\CMS\Language\Text;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\Registry\Registry;

class JFormFieldOpeningHours extends ListField
{
    /**
     * Get opening hours dropdown options
     *
     * @return array
     */
    protected function getOptions()
    {
        return [
            0 => Text::_('GSD_BUSINESSLISTING_NO_HOURS_AVAILABLE'),
            1 => Text::_('GSD_BUSINESSLISTING_ALWAYS_OPEN'),
            2 => Text::_('GSD_BUSINESSLISTING_OPEN_ON_SELECTED_HOURS')
        ];
    }

    /**
     * Render the Opening Hours
     * 
     * @return string
     */
    protected function getInput()
    {
        $name_  = $this->name;

        $this->name .= '[option]';
        $html = parent::getInput();

        // Load css js
        HTMLHelper::_('stylesheet', 'com_gsd/openinghours.css', ['version' => 'auto', 'relative' => true]);
        HTMLHelper::_('script', 'com_gsd/openinghours.js', ['version' => 'auto', 'relative' => true]);
        
        // Draw opening hours
        $html .= '<div class="gsd-oh-container" data-showon=\'[{"field":"' . $this->name . '","values":["2"],"sign":"=","op":""}]\'>';
        $weekdays = \GSD\Helper::getWeekdays(true);
        $this->name = $name_;

        foreach ($weekdays as $day)
        {
            $html .= $this->getRowLayout($day);
        }

        $html .= '</div>';

        return $html;
    }
    
    /**
     * Render a row's layout
     * 
     * @param   string  $day
     * 
     * @return  string
     */
    private function getRowLayout($day)
    {
        $strlower_day = strtolower($day);

        $day_value = isset($this->value[$strlower_day]) ? $this->value[$strlower_day] : [];
        $day_value = new Registry($day_value);

        $checkbox_name = $this->name . '[' . $strlower_day . '][enabled]';
        $checkbox_id = str_replace(array('[', ']'), '_', $checkbox_name);
        $checkbox_id = rtrim($checkbox_id, '_');

        $day_checked = (bool) $day_value->get('enabled', false);
        $day_checked = $day_checked ? ' checked="checked"' : '';

        // Hours Range 1
        $oh_start = $day_value->get('start');
        $oh_end = $day_value->get('end');
        $hours_visible = $oh_start || $oh_end;
        $oh_picker_class = $hours_visible ? ' is-visible' : '';
        $gsd_oh_add_btn_class = $hours_visible ? ' is-hidden' : '';
        
        // Hours Range 2
        $oh_more_start = $day_value->get('start1');
        $oh_more_end = $day_value->get('end1');
        $hours2_visible = $hours_visible && ($oh_more_start || $oh_more_end);
        $oh_more_class = $hours2_visible ? ' is-visible' : '';
        $gsd_oh_more_btn_class = $hours_visible && !$hours2_visible ? ' is-visible' : ' is-hidden';

        return '
            <div class="gsd-oh-row">
                <input type="checkbox"' . $day_checked . ' value="1" id="' . $checkbox_id . '" name="' . $checkbox_name . '" />
                <label for="' . $checkbox_id . '" id="' . $checkbox_id . '-lbl">' . $day . '</label>
                <a href="#" class="gsd-oh-add-btn icon-save-new' . $gsd_oh_add_btn_class . '">
                    <span class="add_hours">' . Text::_('GSD_LOCALBUSINESS_ADD_HOURS_BTN') . '</span>
                </a>
                <div class="oh-picker first' . $oh_picker_class . '">
                    ' . $this->renderTimePicker($day, $oh_start, 'start') . '
                    ' . $this->renderTimePicker($day, $oh_end, 'end') . '
                </div>
                <a href="#" class="gsd-oh-more-btn icon-save-new' . $gsd_oh_more_btn_class . '"></a>
                <div class="oh-more' . $oh_more_class . '">
                    <div class="oh-picker">
                    ' . $this->renderTimePicker($day, $oh_more_start, 'start1') . '
                    ' . $this->renderTimePicker($day, $oh_more_end, 'end1') . '
                    </div>
                    <a href="#" class="gsd-oh-remove-btn icon-delete"></a>
                </div>
            </div>
        ';
    }

    /**
     * Renders a Time Picker
     * 
     * @param   string  $day   The day of the week
     * @param   string  $more  Whether if it's a second time of the day
     * @param   string  $type  Whether if its start or end time
     * 
     * @return  string
     */
    private function renderTimePicker($day, $value, $name)
    {
        include_once JPATH_PLUGINS . '/system/nrframework/fields/time.php';

        $field = new \JFormFieldNR_Time;
        $field_name = $this->name . '[' . strtolower($day) . '][' . $name . ']';
        $field->setValue($value);

        $element = new \SimpleXMLElement('
            <field name="' . $field_name . '" type="nr_time"
                hiddenLabel="true"
            />
        ');
            
        $field->setup($element, null);

        return $field->__get('input');
    }
}models/forms/fields/persontypes.php000064400000003350152345574300013540 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */


defined('_JEXEC') or die;

use Joomla\CMS\Form\Field\ListField;

class JFormFieldPersonTypes extends ListField
{
	/**
	 * Dropdown options array
	 *
	 * @var  array
	 */
	private $options;

	/**
	 * Schema.org Person Types
	 * https://schema.org/Person#incoming
	 *
	 * @var  array
	 */
	private $personTypes = array(
		'Person',
		'Actor',
		'AdultEntertainment',
		'Architect',
		'Athlete',
		'Author',
		'BusinessPerson',
		'Chef',
		'Comedian',
		'Dentist',
		'Designer',
		'Director',
		'Doctor',
		'Engineer',
		'Entrepreneur',
		'FashionDesigner',
		'FilmDirector',
		'Investor',
		'Journalist',
		'MusicComposer',
		'MusicGroup',
		'Musician',
		'Painter',
		'Photographer',
		'Politician',
		'Scientist',
		'Singer',
		'SoftwareDeveloper',
		'SportsTeam',
		'Writer'
	);

	/**
	 * Returns all options to dropdown field
	 *
	 * @return  array
	 */
	protected function getOptions()
	{
		return array_merge(parent::getOptions(), $this->buildTree($this->personTypes));
	}

	/**
	 * Return the choices.
	 *
	 * @param   array $types
	 *
	 * @return  array
	 */
	private function buildTree($types)
	{
		foreach ($types as $key => $type)
		{
			$this->options[] = array(
				'value'    => $type,
				'text'     => $type,
				'selected' => ($this->value == $type)
			);
		}

		return $this->options;
	}
	
    protected function getInput()
    {
        return '<div class="d-flex gap-1"> ' . parent::getInput() . '</div>';
    }
}models/forms/fields/schemacleaner.php000064400000004307152345574300013742 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\Form\Field\SubformField;
use Joomla\CMS\Factory;

class JFormFieldSchemaCleaner extends SubformField
{
    /**
     * Method to get a list of options for a list input.
     *
     * @return      array           An array of JHtml options.
     */
    protected function getInput()
    {
        Factory::getDocument()->addStyleDeclaration('
            .schemacleaner {
                max-width: 700px;
                box-sizing: border-box;
            }
            .schemacleaner table {
                border-collapse: collapse;
            }
            .schemacleaner th, .schemacleaner td {
                border: 1px solid #ddd !important;
                color:inherit !important;
                font-weight: bold !important;
                vertical-align:middle;
            }
            .schemacleaner * {
                box-sizing: inherit;
            }
            .schemacleaner .control-label {
                display:none;
            }
            .schemacleaner .controls {
                padding:0;
                min-width:auto;
            }
            .schemacleaner .control-group {
                margin:0;
            }
            .schemacleaner thead tr > th:first-child {
                width: 70px !important;
            }
            .schemacleaner thead tr > th:nth-child(2) {
                width: 100% !important;
            }
            .schemacleaner input {
                padding: 14px 10px;
                width: 100% !important;
                max-width: 100% !important;
            }
            .schemacleaner .nrtoggle {
                top: 3px;
                left: 6px;
            }
        ');

        $html = '<div class="schemacleaner">' . parent::getInput() . '</div>';

        return $html;
    }
}models/forms/fields/views.php000064400000004450152345574300012304 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use GSD\Helper;
use GSD\Apps;
use Joomla\CMS\Form\Field\ListField;
use Joomla\CMS\Language\Text;
use Joomla\CMS\HTML\HTMLHelper;

class JFormFieldViews extends ListField
{
    /**
     * The selected App.
     * 
     * @var  string
     */
    private $selectedApp;

    /**
     * The supported views of the selected App.
     * 
     * @var  array
     */
    protected $supportedViews;

	/**
	 * Method to get a control group with label and input.
	 *
	 * @param   array  $options  Options to be passed into the rendering of the field
	 *
	 * @return  string  A string containing the html for the control group
	 *
	 * @since   3.2
	 */
	public function renderField($options = array())
	{
        if (!$selectedApp = $this->form->getData()->get('plugin'))
        {
            return;
        }

        $app = Apps::getApp($selectedApp);
        $supportedViews = $app->advertiseSupportedViews();

        // In case the App supports only 1 view, improve UX by pre-populating the field with the supported view and make the field read-only.
        if (count($supportedViews) == 1)
        {
            $this->value = array_keys($supportedViews)[0];
            $this->readonly = true;
        }

        // Sort alphabetically
        asort($supportedViews);

        $this->supportedViews = $supportedViews;

        return parent::renderField();
    }

    /**
     * Method to get a list of options for a list input.
     *
     * @return      array           An array of options.
     */
    protected function getOptions()
    {
        $options[] = HTMLHelper::_('select.option', '', '- ' . Text::_('GSD_APP_VIEW_SELECT') . ' -');
        
        foreach ($this->supportedViews as $value => $label)
        {
            $options[] = HTMLHelper::_('select.option', $value, $label);
        }

        return array_merge(parent::getOptions(), $options);
    }
}models/forms/rules/recommended.php000064400000006403152345574300013315 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\Form\FormRule;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Factory;

class JFormRuleRecommended extends FormRule
{
	/**
	 *  Method to test if the value of the field is recommended and not empty
	 *
	 *  @param   SimpleXMLElement              $element  The SimpleXMLElement object representing the `<field>` tag for the form field object.
	 *  @param   mixed                         $value    The form field value to validate.
	 *  @param   string                        $group    The field name group control value. This acts as as an array container for the field.
	 *                                                   For example if the field has name="foo" and the group value is set to "bar" then the
	 *                                                   full field name would end up being "bar[foo]".
	 *  @param   Joomla\Registry\Registry|null  $input   An optional Registry object with the entire data set to validate against the entire form.
	 *  @param   Form                     		$form    The form object for which the field is being tested.
	 *
	 *  @return  boolean                                 True if the value is valid, false otherwise.
	 */
	public function test(SimpleXMLElement $element, $value, $group = null, Joomla\Registry\Registry $input = null, Form $form = null)
	{
		if (empty($input) || !$this->checkRequisites($element, $input))
		{
			return true;
		}

		if (empty($value))
		{
			$isRecommended  = $element->attributes()->recommended;
			$elementMessage = $element->attributes()->message;
			$elementLabel   = $element->attributes()->label;
			$message        = Text::_("GSD") . ": " . Text::sprintf($elementMessage, Text::_($elementLabel));

			// A value for the field is recommended. Save the form but throw a notice as well.
			if ($isRecommended)
			{
				Factory::getApplication()->enqueueMessage($message, "notice");
				return true;
			} 
			
			// A value for the field is required. Don't save the form.
			$element->attributes()->message = $message;
			return false;
		}
	}

	/**
	 *  Checks if the requisites for the field to be validated are met.
	 *
	 *  @param   SimpleXMLElement  			$element  The SimpleXMLElement object representing the `<field>` tag for the form field object.
	 *  @param   Joomla\Registry\Registry	$input    An optional Registry object with the entire data set to validate against the entire form.
	 *
	 *  @return  boolean							  True if the requisites are met, false otherwise
	 */
	private function checkRequisites($element, $input)
	{
		$requisites = explode('|', (string) $element->attributes()->requisites);

		if (empty($requisites))
		{
			return true;
		}

		foreach ($requisites as $requisite)
		{
			$requisite = explode(':', $requisite);

			if (empty($requisite) || count($requisite) < 2)
			{
				continue;
			}

			if ($input->get($requisite[0]) != $requisite[1])
			{
				return false;
			}
		}
		
		return true;
	}
}
models/forms/rules/searchurl.php000064400000003377152345574300013032 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\Form\FormRule;
use Joomla\CMS\Form\Form;

class JFormRuleSearchurl extends FormRule
{
	/**
	 *  Method to test the value for the searchurl field
	 *
	 *  @param   SimpleXMLElement              $element  The SimpleXMLElement object representing the `<field>` tag for the form field object.
	 *  @param   mixed                         $value    The form field value to validate.
	 *  @param   string                        $group    The field name group control value. This acts as as an array container for the field.
	 *                                                   For example if the field has name="foo" and the group value is set to "bar" then the
	 *                                                   full field name would end up being "bar[foo]".
	 *  @param   Joomla\Registry\Registry|null  $input   An optional Registry object with the entire data set to validate against the entire form.
	 *  @param   Form                     		$form    The form object for which the field is being tested.
	 *
	 *  @return  boolean                                 True if the value is valid, false otherwise.
	 */
	public function test(SimpleXMLElement $element, $value, $group = null, Joomla\Registry\Registry $input = null, Form $form = null)
	{
		if (($input->get('params.sitelinks_enabled') == 3) && stripos($value, '{search_term}') === false)
		{
			return false;
		}

		return true;
	}
}
models/forms/config.xml000064400000025734152345574300011167 0ustar00<?xml version="1.0" encoding="utf-8"?>
<form>
    <fieldset name="globaldata" label="GSD_GLOBAL_DATA" description="GSD_GLOBAL_DATA_DESC">
        <!-- Site Name -->
        <field name="sitenameblockstart" type="nr_well"
            description="GSD_SITENAME_ENABLED_DESC"
            label="GSD_SITENAME_NAME"
            url="https://www.tassos.gr/joomla-extensions/google-structured-data-markup/docs/the-site-name-snippet"
        />
        <field name="sitename_enabled" type="nrtoggle"
            label="JENABLED" 
            description="GSD_SITENAME_ENABLED_DESC" 
            checked="true"
        />
        <field name="sitename_name" type="nrtext" 
            class="input-xxlarge"
            showon="sitename_enabled:1" 
            label="GSD_SITENAME_NAME"
            description="GSD_SITENAME_NAME_DESC"
            hint="GSD_SITENAME_NAME"
        />
        <field name="sitename_name_alt" type="text" 
            showon="sitename_enabled:1" 
            class="input-xxlarge"
            label="GSD_SITENAME_NAME_ALT"
            description="GSD_SITENAME_NAME_ALT_DESC"
            hint="GSD_SITENAME_NAME_ALT"
        />
        <field name="sitename_url" type="url"
            class="input-xxlarge"
            showon="sitename_enabled:1"
            label="GSD_SITENAME_URL"
            description="GSD_SITENAME_URL_DESC"
            hint="GSD_SITENAME_URL"
        />
        <field name="sitenameblockend" type="nr_well" 
            end="1"
        />
        <!-- Breadcrumbs -->
        <field name="breadcrumbsstart" type="nr_well" 
            description="GSD_BREADCRUMBS_DESC"
            label="GSD_BREADCRUMBS"
            url="https://www.tassos.gr/joomla-extensions/google-structured-data-markup/docs/the-breadcrumbs-snippet"
        />
        <field name="breadcrumbs_enabled" type="nrtoggle"
            label="JENABLED" 
            description="GSD_BREADCRUMBS_DESC" 
            checked="true"
        />
        <field name="include_home" type="nrtoggle"
            label="GSD_BREADCRUMBS_INCLUDE_HOME" 
            description="GSD_BREADCRUMBS_INCLUDE_HOME_DESC" 
            checked="true"
            showon="breadcrumbs_enabled:1"
        />
        <field name="breadcrumbs_home" type="text"
            class="input-xxlarge"
            showon="breadcrumbs_enabled:1[AND]include_home:1"
            label="GSD_BREADCRUMBS_HOME_NAME"
            description="GSD_BREADCRUMBS_HOME_NAME_DESC"
            hint="GSD_BREADCRUMBS_HOME_NAME"
        />
        <field name="breadcrumbsend" type="nr_well" end="1"/>
        <!-- Site Links -->
        <field name="sitelinksblockstart" type="nr_well" 
            description="GSD_SITELINKS_ENABLED_DESC"
            url="https://www.tassos.gr/joomla-extensions/google-structured-data-markup/docs/the-sitelinks-searchbox-snippet"
            label="GSD_SITELINKS_NAME"
        />
        <field name="sitelinks_enabled" type="list"
            description="GSD_SITELINKS_SEARCH_URL_DESC"
            label="GSD_SITELINKS_SEARCH_URL"
            default="0">
            <option value="0">JDISABLED</option>
            <option value="1">GSD_SITELINKS_NATIVE_JOOMLA_SEARCH</option>
            <option value="2">GSD_SITELINKS_NATIVE_JOOMLA_FINDER</option>
            <option value="3">GSD_SITELINKS_CUSTOM_SEARCH_URL</option>
        </field>
        <field name="sitelinks_search_custom_url" type="url"
            description="GSD_SITELINKS_CUSTOM_SEARCH_URL_DESC"
            class="input-xxlarge"
            hint="https://www.yourwebsite.com/search?q={search_term}"
            showon="sitelinks_enabled:3"
            label="GSD_SITELINKS_CUSTOM_SEARCH_URL"
            validate="searchurl"
            message="GSD_SITELINKS_CUSTOM_SEARCH_URL_INVALID"
            optional="1"
        />
        <field name="sitelinksblockend" type="nr_well" 
            end="1"
        />
        <!-- Logo -->
        <field name="logostart" type="nr_well" 
            description="GSD_LOGO_DESC"
            url="https://www.tassos.gr/joomla-extensions/google-structured-data-markup/docs/the-sitelogo-snippet"
            label="GSD_LOGO"
        />
        <field name="logo_file" type="media"
            preview="tooltip"
            label="GSD_LOGO_FILE"
            description="GSD_LOGO_FILE_DESC"
        />
        <field name="logoend" type="nr_well" end="1"/>
        <!-- Social Profiles -->
        <field name="socialprofilestart" type="nr_well" 
            description="GSD_SOCIALPROFILES_DESC"
            label="GSD_SOCIALPROFILES"
            url="https://www.tassos.gr/joomla-extensions/google-structured-data-markup/docs/the-social-profile-links-snippet"
        />
        <field name="socialprofiles_type" type="list" 
            label="GSD_SOCIALPROFILE_TYPE" 
            description="GSD_SOCIALPROFILE_TYPE_DESC" 
            default="Organization">
                <option value="Organization">GSD_ORGANIZATION</option>
                <option value="Person">GSD_PERSON</option>
        </field>
        <field name="socialprofiles_facebook" type="url"
            class="input-xxlarge"
            hint="https://www.facebook.com"
            label="Facebook"
        />
        <field name="socialprofiles_twitter" type="url"
            class="input-xxlarge"
            hint="https://www.twitter.com"
            label="Twitter"
        />
        <field name="socialprofiles_instagram" type="url"
            class="input-xxlarge"
            hint="https://www.instagram.com"
            label="Instagram"
        />
        <field name="socialprofiles_youtube" type="url"
            class="input-xxlarge"
            hint="https://www.youtube.com"
            label="YouTube"
        />
        <field name="socialprofiles_linkedin" type="url"
            class="input-xxlarge"
            hint="https://www.linkedin.com"
            label="LinkedIn"
        />
        <field name="socialprofiles_pinterest" type="url"
            class="input-xxlarge"
            hint="https://www.pinterest.com"
            label="Pinterest"
        />
        <field name="socialprofiles_soundcloud" type="url"
            class="input-xxlarge"
            hint="https://www.soundcloud.com"
            label="SoundCloud"
        />
        <field name="socialprofiles_tumblr" type="url"
            class="input-xxlarge"
            hint="https://www.tumblr.com"
            label="Tumblr"
        />
        <field name="socialprofiles_other" type="textarea"
            hint="GSD_SOCIALPROFILES_OTHER_HINT"
            class="input-xxlarge"
            rows="5"
            description="GSD_SOCIALPROFILES_OTHER_DESC"
            label="GSD_SOCIALPROFILES_OTHER"
        />
        <field name="socialprofilesend" type="nr_well" end="1"/>
    </fieldset>
    <fieldset name="contenttypes" label="GSD_CONTENT_TYPES">
        <field name="ctStart" type="nr_well"
            label="GSD_CONTENT_TYPES"
            description="GSD_CONTENT_TYPE_DESC"
        />
        <field name="contenttypes" type="addons"
            xmlfile="contenttypes"
            pluginmode="false"
        />
        <field name="ctEnd" type="nr_well" 
            end="true"
        />
    </fieldset>
    <fieldset name="integrations" label="GSD_INTEGRATIONS">
        <field name="intStart" type="nr_well"
            label="GSD_INTEGRATIONS"
            description="GSD_INTEGRATIONS_DESC"
        />
        <field name="integrations" type="addons"
            xmlfile="integrations"
        />
        <field name="intEnd" type="nr_well" 
            end="true"
        />
    </fieldset>
    <fieldset name="advanced" label="GSD_CONFIG_ADVANCED">
        <field name="advancedStart" type="nr_well"
            label="GSD_CONFIG_ADVANCED"
            description="GSD_CONFIG_ADVANCED_DESC"
        />
        <field name="preparecontent" type="nrtoggle"
            label="GSD_PREPARE_CONTENT" 
            description="GSD_PREPARE_CONTENT_DESC" 
        />
        <field name="minifyjson" type="nrtoggle"
            label="GSD_MINIFY_JSON" 
            description="GSD_MINIFY_JSON_DESC" 
        />
        <field name="wait_page_render" type="nrtoggle"
            label="GSD_WAIT_PAGE_TO_RENDER" 
            description="GSD_WAIT_PAGE_TO_RENDER_DESC" 
        />
        <field name="debug" type="nrtoggle"
            label="NR_DEBUG" 
            description="GSD_DEBUG_DESC" 
        />
        <field name="advancedEnd" type="nr_well" 
            end="true"
        />
        <field name="microdataStart" type="nr_well" 
            label="GSD_REMOVE_MICRODATA"
            description="GSD_REMOVE_MICRODATA_DESC"
        />
        
        <field name="removemicrodata" type="schemacleaner"
            label="GSD_REMOVE_MICRODATA_TYPES"
            description="GSD_REMOVE_MICRODATA_TYPES_DESC2"
            layout="joomla.form.field.subform.repeatable-table"
            class="schemascleaner"
            multiple="true"
            default='{
                "0": {
                    "name":"BreadcrumbList",
                    "enabled":"1"
                },
                "1": {
                    "name":"AggregateRating",
                    "enabled":"0"
                },
                "2": {
                    "name":"Article",
                    "enabled":"0"
                },
                "3": {
                    "name":"BlogPosting",
                    "enabled":"0"
                },
                "4": {
                    "name":"Blog",
                    "enabled":"0"
                },
                "5": {
                    "name":"Product",
                    "enabled":"0"
                }
            }'>
            <form>
                <field name="enabled" type="nrtoggle"
                    label="JENABLED"
                    hiddenLabel="true"
                />
                <field name="name" type="text"
                    label="GSD_REMOVE_SCHEMA_NAME"
                    hint="GSD_REMOVE_DUPLICATE_SCHEMA_HINT"
                    hiddenLabel="true"
                />
            </form>
        </field>
        
        
        <field name="microdataEnd" type="nr_well" 
            end="true"
        />
        <field name="customCodeStart" type="nr_well" 
            label="GSD_CUSTOM_CODE"
            description="GSD_CUSTOM_CODE_GLOBAL_DESC"
        />
        <field name="customcode" type="textarea"
            label="GSD_CUSTOM_CODE"
            description="GSD_CUSTOM_CODE_GLOBAL_DESC"
            class="span12 input-full"
            filter="raw"
            rows="15"
            hint="&lt;script type='application/ld+json'&gt;&lt;/script&gt;"
        />
        <field name="customcode_disclaimer" type="note" 
            class="alert alert-danger"
            description="GSD_CUSTOM_CODE_DISCLAIMER"
        />  
        <field name="customCodeEnd" type="nr_well" 
            end="true"
        />
    </fieldset>
</form>models/forms/contenttypes.xml000064400000005275152345574300012457 0ustar00<?xml version="1.0" encoding="utf-8"?>
<addons>
    <addon name="articles"
        label="GSD_ARTICLE"
        description="GSD_ARTICLE_DESC"
        docalias="article-schema"
        proonly="false"
        icon="File"
    />
    <addon name="books"
        label="GSD_BOOK"
        description="GSD_BOOK_DESC"
        docalias="book-schema"
        icon="AddressAutocomplete"
    />
    <addon name="courses"
        label="GSD_COURSE"
        description="GSD_COURSE_DESC"
        docalias="course-schema"
        icon="KnowledgeBase"
    />
    <addon name="products"
        label="GSD_PRODUCT"
        description="GSD_PRODUCT_DESC"
        docalias="product-schema"
        icon="Product"
    />
    <addon name="events"
        label="GSD_EVENT"
        description="GSD_EVENT_DESC"
        docalias="event-schema"
        icon="Date"
    />
    <addon name="recipes"
        label="GSD_RECIPE"
        description="GSD_RECIPE_DESC"
        docalias="recipe-schema"
        icon="Food"
    />
    <addon name="reviews"
        label="GSD_REVIEW"
        description="GSD_REVIEW_DESC"
        docalias="review-schema"
        icon="StarOutline"
    />
    <addon name="factchecks"
        label="GSD_FACTCHECK"
        description="GSD_FACTCHECK_DESC"
        docalias="factcheck-schema"
        icon="Done"
    />
    <addon name="videos"
        label="GSD_VIDEO"
        description="GSD_VIDEOS_DESC"
        docalias="video-schema"
        icon="Video"
    />
    <addon name="jobposting"
        label="Job Posting"
        description="Improve the job seeking experience by adding job posting structured data to your job posting web pages."
        docalias="jobposting-schema"
        icon="Case"
    />
    <addon name="faq"
        label="GSD_FAQ"
        description="GSD_FAQ_DESC"
        docalias="faq-schema"
        icon="FAQ"
    />
    <addon name="localbusiness"
        label="GSD_LOCAL_BUSINESS"
        description="GSD_LOCAL_BUSINESS_DESC"
        docalias="localbusiness-schema"
        icon="Map"
    />
    <addon name="movie"
        label="GSD_MOVIE"
        description="GSD_MOVIE_DESC"
        docalias="movie-schema"
        icon="Movie"
    />
    <addon name="service"
        label="GSD_SERVICE"
        description="GSD_SERVICE_DESC"
        docalias="service-schema"
        icon="Service"
    />
    <addon name="person"
        label="GSD_PERSON"
        description="GSD_PERSON_DESC"
        docalias="person-schema"
        icon="Support"
    />
    <addon name="howto"
        label="GSD_HOWTO"
        description="GSD_HOWTO_DESC"
        docalias="howto-schema"
        icon="Question"
    />
</addons>models/forms/filter_items.xml000064400000003701152345574300012376 0ustar00<?xml version="1.0" encoding="utf-8"?>
<form>
	<fields name="filter" addfieldpath="administrator/components/com_gsd/models/forms/fields">
		<field
			name="search"
			type="text"
			label="JSEARCH_FILTER_LABEL"
			hint="JSEARCH_FILTER"
		/>
		<field
			name="state"
			type="status"
			filter="-2,0,1"
			label="JPUBLISHED"
			onchange="this.form.submit();">
			<option value="">JOPTION_SELECT_PUBLISHED</option>
		</field>
		<field name="plugin" type="integrations"
			showselect="true"
			onchange="this.form.submit();"
		/>
		<field name="contenttype" type="contenttypes"
			showselect="true"
			onchange="this.form.submit();"
			showhelp="false"
		/>
		<field name="language" type="contentlanguage"
			label="JOPTION_FILTER_LANGUAGE"
			description="JOPTION_FILTER_LANGUAGE_DESC"
			onchange="this.form.submit();">
			<option value="">JOPTION_SELECT_LANGUAGE</option>
			<option value="*">JALL</option>
		</field>
		<field name="targetpages" type="list"
			onchange="this.form.submit();">
			<option>GSD_ITEM_RULES2</option>
			<option value="all">COM_GSD_TARGETS_ALL_PAGES</option>
			<option value="specific">COM_GSD_TARGETS_SPECIFIC_PAGES</option>
		</field>
	</fields>
	<fields name="list">
		<field
			name="fullordering"
			type="list"
			label="NR_LIST_FULL_ORDERING"
			description="NR_LIST_FULL_ORDERING_DESC"
			onchange="this.form.submit();"
			default="a.ordering ASC">
			<option value="">JGLOBAL_SORT_BY</option>
			<option value="a.state ASC">JSTATUS_ASC</option>
			<option value="a.state DESC">JSTATUS_DESC</option>
			<option value="a.thing ASC">JGLOBAL_TITLE_ASC</option>
			<option value="a.thing DESC">JGLOBAL_TITLE_DESC</option>
			<option value="a.id ASC">JGRID_HEADING_ID_ASC</option>
			<option value="a.id DESC">JGRID_HEADING_ID_DESC</option>
		</field>
		<field
			name="limit"
			type="limitbox"
			class="input-mini"
			default="10"
			label="NR_LIST_LIMIT"
			description="NN_LIST_LIMIT_DESC"
			onchange="this.form.submit();"
		/>
	</fields>
</form>
models/forms/filter_things.xml000064400000001117152345574300012550 0ustar00<?xml version="1.0" encoding="utf-8"?>
<form>
	<fields name="filter" addfieldpath="administrator/components/com_gsd/models/forms/fields">
		<field name="search" type="text"
			label="JSEARCH_FILTER_LABEL"
			hint="JSEARCH_FILTER"
			class="span4"
		/>
		<field name="context" type="integrations"
			showselect="false"
			onchange="this.form.submit();"
		/>
	</fields>
	<fields name="list">
		<field
			name="limit"
			type="limitbox"
			class="input-mini"
			default="20"
			label="NR_LIST_LIMIT"
			description="NN_LIST_LIMIT_DESC"
			onchange="this.form.submit();"
		/>
	</fields>
</form>
models/forms/integrations.xml000064400000011051152345574300012413 0ustar00<?xml version="1.0" encoding="utf-8"?>
<addons>
    <addon name="content"
        label="Joomla Content"
        description="Add Structured Data to Joomla! Articles"
        docalias="the-joomla-content-integration"
        proonly="false"
    />
    <addon name="k2"
        label="K2"
        description="Add Structured Data to K2 items."
        docalias="the-k2-integration"
    />
    <addon name="easyblog"
        label="EasyBlog"
        description="Add Structured Data to EasyBlog posts."
        docalias="the-easyblog-integration"
    />
    <addon name="rsblog"
        label="RSBlog"
        description="Add Structured Data to RSBlog posts."
        docalias="the-rsblog-integration"
    />
    <addon name="rseventspro"
        label="RSEvents!Pro"
        description="Add Structured Data to RSEvents!Pro events."
        docalias="the-rseventspro-integration"
    />
    <addon name="sppagebuilder"
        label="SP Page Builder"
        description="Add Structured Data to SP Page Builder pages."
        docalias="the-sppagebuilder-integration"
    />
    <addon name="j2store"
        label="J2Store"
        description="Add Structured Data to J2Store Products."
        docalias="the-j2store-integration"
    />
    <addon name="hikashop"
        label="HikaShop"
        description="Add Structured Data to HikaShop products."
        docalias="the-hikashop-integration"
    />
    <addon name="jshopping"
        label="JoomShopping"
        description="Add Structured Data to JShopping products."
        docalias="the-jshopping-integration"
    />
    <addon name="virtuemart"
        label="VirtueMart"
        description="Add Structured Data to VirtueMart products."
        docalias="the-virtuemart-integration"
    />
    <addon name="zoo"
        label="Zoo"
        description="Add Structured Data to Zoo items."
        docalias="the-zoo-integration"
    />
    <addon name="menus"
        label="Menu Manager"
        description="Add Structured Data to Menu Manager Items."
        docalias="the-menu-integration"
    />
    <addon name="eventbooking"
        label="Event Booking"
        description="Add Structured Data to Event Booking items."
        docalias="the-event-booking-integration"
    />
    <addon name="eshop"
        label="Eshop"
        description="Add Structured Data to Eshop product pages."
        docalias="the-eshop-integration"
    />
    <addon name="djcatalog2"
        label="DJ-Catalog2"
        description="Add Structured Data to DJ-Catalog2 product pages."
        docalias="the-djcatalog2-integration"
    />
    <addon name="quix"
        label="Quix Page Builder"
        description="Add Structured Data to Quix Page Builder pages."
        docalias="the-quix-page-builder-integration"
    />
    <addon name="djclassifieds"
        label="DJ Classifieds"
        description="Add Structured Data to DJ Classifieds items."
        docalias="the-djclassifieds-integration"
    />
    <addon name="djevents"
        label="DJ Events"
        description="Add Structured Data to DJ Events items."
        docalias="the-djevents-integration"
    />
    <addon name="jevents"
        label="JEvents"
        description="Add Structured Data to JEvents items."
        docalias="the-jevents-integration"
    />
    <addon name="sobipro"
        label="SobiPro"
        description="Add Structured Data to SobiPro Entries."
        docalias="the-sobipro-integration"
    />
    <addon name="jcalpro"
        label="JCal Pro"
        description="Add Structured Data to JCal Pro items."
        docalias="the-jcal-pro-integration"
    />
    <addon name="gridbox"
        label="Gridbox"
        description="Add Structured Data to Gridbox pages."
        docalias="the-gridbox-integration"
    />
    <addon name="jbusinessdirectory"
        label="J-BusinessDirectory"
        description="Add Structured Data to J-BusinessDirectory businesses, events and offers."
        docalias="the-jbusinessdirectory-integration"
    />
    <addon name="icagenda"
        label="iCagenda"
        description="Add Structured Data to iCagenda events."
        docalias="the-icagenda-integration"
    />
    <addon name="dpcalendar"
        label="DPCalendar"
        description="Add Structured Data to DPCalendar events."
        docalias="the-dpcalendar-integration"
    />
    <addon name="jreviews"
        label="JReviews"
        description="Add Structured Data to JReviews listings."
        docalias="the-jreviews-integration"
    />
</addons>models/forms/item.xml000064400000004733152345574300010654 0ustar00<?xml version="1.0" encoding="utf-8"?>
<form>
    <fieldset name="top">
        <field name="blockStart" type="nr_well"
            label="GSD_ITEM_GENERAL"
            description="GSD_ITEM_GENERAL_DESC"
        />
        <field name="title" type="text"
            label="NR_TITLE"
            description="GSD_ITEM_TITLE_DESC"
            class="span12 input-"
            required="true"
        />
        <field name="contenttype" type="contenttypes"
            label="GSD_CONTENT_TYPE" 
            description="GSD_CONTENT_TYPE_SELECT_DESC"
            showhelp="true"
            required="true"
            showselect="true"
            onchange="GSDReloadForm(this)"
        />
        <field name="plugin" type="integrations"
            label="GSD_INTEGRATION" 
            description="GSD_INTEGRATION_DESC"
            showselect="true"
            required="true"
            onchange="GSDReloadForm(this)"
        />
        <field name="appview" type="views"
            label="GSD_APP_VIEW" 
            description="GSD_APP_VIEW_DESC"
            required="true"
            onchange="GSDReloadForm(this)"
        />
        <field name="blockEnd" type="nr_well" 
            end="1"
        />
    </fieldset>
    <fieldset name="main">
        <field name="state" type="list"
            label="JSTATUS"
            description="NR_FIELD_STATE_DESC"
            class="chzn-color-state custom-select-color-state"
            size="1"
            default="1">
            <option value="1">JPUBLISHED</option>
            <option value="0">JUNPUBLISHED</option>
            <option value="2">JARCHIVED</option>
            <option value="-2">JTRASHED</option>
        </field>
		<field name="language" type="contentlanguage" 
			label="JFIELD_LANGUAGE_LABEL"
			description="GSD_ITEM_LANGUAGE_DESC">
			<option value="*">JALL</option>
		</field>
        <field name="note" type="text" 
            label="GSD_NOTE" 
            description="GSD_NOTE_DESC" 
        />
        <field name="colorgroup" type="color" 
            default="transparent"
            label="NR_COLOR"
            description="GSD_COLOR_DESC"
            control="simple"
            split="4"
        />
        <field name="id" type="hidden"
            label="JGLOBAL_FIELD_ID_LABEL"
            description="JGLOBAL_FIELD_ID_DESC"
            default="0"
            readonly="true"
            class="readonly"
        />
    </fieldset>
</form>models/forms/opening_hours.xml000064400000022744152345574300012577 0ustar00<?xml version="1.0" encoding="utf-8"?>
<form>
    <fieldset name="openinghours">
        <field name="businesslisting_hours_available" type="list"
            label=""
            default="0">
            <option value="0">GSD_BUSINESSLISTING_NO_HOURS_AVAILABLE</option>
            <option value="1">GSD_BUSINESSLISTING_ALWAYS_OPEN</option>
            <option value="2">GSD_BUSINESSLISTING_OPEN_ON_SELECTED_HOURS</option>
        </field>
        <field name="businesslisting_inline_monday_start" type="nr_inline" 
            showon="businesslisting_hours_available:2"
            class="timetable"
        />
        <field name="businesslisting_monday" type="checkbox" 
            label="Monday"
            value="1">
        </field>
        <field name="businesslisting_monday_start" type="nr_time"
            hiddenLabel="true"
        />
        <field name="businesslisting_monday_sep" type="spacer"
            label="-"
        />
        <field name="businesslisting_monday_end" type="nr_time"
            hiddenLabel="true"
        />
        <field name="businesslisting_monday_more" type="nrtoggle"
            hiddenLabel="true"
        />
        <field name="businesslisting_monday_more_start" type="nr_time"
            showon="businesslisting_monday_more:1"
            hiddenLabel="true"
        />
        <field name="businesslisting_monday_more_sep" type="spacer"
            showon="businesslisting_monday_more:1"
            label="-"
        />
        <field name="businesslisting_monday_more_end" type="nr_time"
            showon="businesslisting_monday_more:1"
            hiddenLabel="true"
        />
        <field name="businesslisting_inline_monday_more_end" type="nr_inline" 
            end="1"
        />
        <field name="businesslisting_inline_tuesday_start" type="nr_inline" 
            showon="businesslisting_hours_available:2"
            class="timetable"
        />
        <field name="businesslisting_tuesday" type="checkbox" 
            label="Tuesday"
            value="1">
        </field>
        <field name="businesslisting_tuesday_start" type="nr_time"
            hiddenLabel="true"
        />
        <field name="businesslisting_tuesday_sep" type="spacer"
            label="-"
        />
        <field name="businesslisting_tuesday_end" type="nr_time"
            hiddenLabel="true"
        />
        <field name="businesslisting_tuesday_more" type="nrtoggle"
            hiddenLabel="true"
        />
        <field name="businesslisting_tuesday_more_start" type="nr_time"
            showon="businesslisting_tuesday_more:1"
            hiddenLabel="true"
        />
        <field name="businesslisting_tuesday_more_sep" type="spacer"
            showon="businesslisting_tuesday_more:1"
            label="-"
        />
        <field name="businesslisting_tuesday_more_end" type="nr_time"
            showon="businesslisting_tuesday_more:1"
            hiddenLabel="true"
        />
        <field name="businesslisting_inline_tuesday_end" type="nr_inline" 
            end="1"
        />
        <field name="businesslisting_inline_wednesday_start" type="nr_inline" 
            showon="businesslisting_hours_available:2"
            class="timetable"
        />
        <field name="businesslisting_wednesday" type="checkbox" 
            label="Wednesday" 
            value="1">
        </field>
        <field name="businesslisting_wednesday_start" type="nr_time"
            hiddenLabel="true"
        />
        <field name="businesslisting_wednesday_sep" type="spacer"
            label="-"
        />
        <field name="businesslisting_wednesday_end" type="nr_time"
            hiddenLabel="true"
        />
        <field name="businesslisting_wednesday_more" type="nrtoggle"
            hiddenLabel="true"
        />
        <field name="businesslisting_wednesday_more_start" type="nr_time"
            showon="businesslisting_wednesday_more:1"
            hiddenLabel="true"
        />
        <field name="businesslisting_wednesday_more_sep" type="spacer"
            showon="businesslisting_wednesday_more:1"
            label="-"
        />
        <field name="businesslisting_wednesday_more_end" type="nr_time"
            showon="businesslisting_wednesday_more:1"
            hiddenLabel="true"
        />
        <field name="businesslisting_inline_wednesday_end" type="nr_inline" 
            end="1"
        />
        <field name="businesslisting_inline_thursday_start" type="nr_inline" 
            showon="businesslisting_hours_available:2"
            class="timetable"
        />
        <field name="businesslisting_thursday" type="checkbox" 
            label="Thursday" 
            value="1">
        </field>
        <field name="businesslisting_thursday_start" type="nr_time"
            hiddenLabel="true"
        />
        <field name="businesslisting_thursday_sep" type="spacer"
            label="-"
        />
        <field name="businesslisting_thursday_end" type="nr_time"
            hiddenLabel="true"
        />
        <field name="businesslisting_thursday_more" type="nrtoggle"
            hiddenLabel="true"
        />
        <field name="businesslisting_thursday_more_start" type="nr_time"
            showon="businesslisting_thursday_more:1"
            hiddenLabel="true"
        />
        <field name="businesslisting_thursday_more_sep" type="spacer"
            showon="businesslisting_thursday_more:1"
            label="-"
        />
        <field name="businesslisting_thursday_more_end" type="nr_time"
            showon="businesslisting_thursday_more:1"
            hiddenLabel="true"
        />
        <field name="businesslisting_inline_thursday_end" type="nr_inline" 
            end="1"
        />
        <field name="businesslisting_inline_friday_start" type="nr_inline" 
            showon="businesslisting_hours_available:2"
            class="timetable"
        />
        <field name="businesslisting_friday" type="checkbox" 
            label="Friday" 
            value="1">
        </field>
        <field name="businesslisting_friday_start" type="nr_time"
            hiddenLabel="true"
        />
        <field name="businesslisting_friday_sep" type="spacer"
            label="-"
        />
        <field name="businesslisting_friday_end" type="nr_time"
            hiddenLabel="true"
        />
        <field name="businesslisting_friday_more" type="nrtoggle"
            hiddenLabel="true"
        />
        <field name="businesslisting_friday_more_start" type="nr_time"
            showon="businesslisting_friday_more:1"
            hiddenLabel="true"
        />
        <field name="businesslisting_friday_more_sep" type="spacer"
            showon="businesslisting_friday_more:1"
            label="-"
        />
        <field name="businesslisting_friday_more_end" type="nr_time"
            showon="businesslisting_friday_more:1"
            hiddenLabel="true"
        />
        <field name="businesslisting_inline_friday_end" type="nr_inline" 
            end="1"
        />
        <field name="businesslisting_inline_saturday_start" type="nr_inline" 
            showon="businesslisting_hours_available:2"
            class="timetable"
        />
        <field name="businesslisting_saturday" type="checkbox" 
            label="Saturday" 
            value="1">
        </field>
        <field name="businesslisting_saturday_start" type="nr_time"
            hiddenLabel="true"
        />
        <field name="businesslisting_saturday_sep" type="spacer"
            label="-"
        />
        <field name="businesslisting_saturday_end" type="nr_time"
            hiddenLabel="true"
        />
        <field name="businesslisting_saturday_more" type="nrtoggle"
            hiddenLabel="true"
        />
        <field name="businesslisting_saturday_more_start" type="nr_time"
            showon="businesslisting_saturday_more:1"
            hiddenLabel="true"
        />
        <field name="businesslisting_saturday_more_sep" type="spacer"
            showon="businesslisting_saturday_more:1"
            label="-"
        />
        <field name="businesslisting_saturday_more_end" type="nr_time"
            showon="businesslisting_saturday_more:1"
            hiddenLabel="true"
        />
        <field name="businesslisting_inline_saturday_end" type="nr_inline" 
            end="1"
        />
        <field name="businesslisting_inline_sunday_start" type="nr_inline" 
            showon="businesslisting_hours_available:2"
            class="timetable"
        />
        <field name="businesslisting_sunday" type="checkbox" 
            label="Sunday" 
            value="1">
        </field>
        <field name="businesslisting_sunday_start" type="nr_time"
            hiddenLabel="true"
        />
        <field name="businesslisting_sunday_sep" type="spacer"
            label="-"
        />
        <field name="businesslisting_sunday_end" type="nr_time"
            hiddenLabel="true"
        />
        <field name="businesslisting_sunday_more" type="nrtoggle"
            hiddenLabel="true"
        />
        <field name="businesslisting_sunday_more_start" type="nr_time"
            showon="businesslisting_sunday_more:1"
            hiddenLabel="true"
        />
        <field name="businesslisting_sunday_more_sep" type="spacer"
            showon="businesslisting_sunday_more:1"
            label="-"
        />
        <field name="businesslisting_sunday_more_end" type="nr_time"
            showon="businesslisting_sunday_more:1"
            hiddenLabel="true"
        />
        <field name="businesslisting_inline_sunday_end" type="nr_inline" 
            end="1"
        />
    </fieldset>
</form>models/config.php000064400000006060152345574300010017 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\MVC\Model\AdminModel;
use Joomla\CMS\Table\Table;
use Joomla\CMS\Form\Form;

/**
 * Config Model Class
 */
class GSDModelConfig extends AdminModel
{
    /**
     * Returns a reference to the a Table object, always creating it.
     *
     * @param       type    The table type to instantiate
     * @param       string  A prefix for the table class name. Optional.
     * @param       array   Configuration array for model. Optional.
     * @return      Table  A database object
     * @since       2.5
     */
    public function getTable($type = 'Config', $prefix = 'GSDTable', $config = array()) 
    {
        return Table::getInstance($type, $prefix, $config);
    }

    /**
     * Method to allow derived classes to preprocess the form.
     *
     * @param   Form   $form   A Form object.
     * @param   mixed   $data   The data expected for the form.
     * @param   string  $group  The name of the plugin group to import (defaults to "content").
     *
     * @return  void
     *
     * @see     FormField
     * @since   1.6
     * @throws  Exception if there is an error in the form event.
     */
    protected function preprocessForm(Form $form, $data, $group = 'content')
    {
        $form->addFieldPath(__DIR__ . '/forms/fields');
        $form->addRulePath(__DIR__ . '/forms/rules');
        $form->addFieldPath(JPATH_PLUGINS . '/system/nrframework/fields');

        parent::preprocessForm($form, $data, $group);
    }

    /**
     * Method to get the record form.
     *
     * @param       array   $data           Data for the form.
     * @param       boolean $loadData       True if the form is to load its own data (default case), false if not.
     * @return      mixed   A Form object on success, false on failure
     * @since       2.5
     */
    public function getForm($data = array(), $loadData = true)
    {
        // Get the form.
        $form = $this->loadForm('com_gsd.config', 'config', array('control' => 'jform', 'load_data' => $loadData));

        if (empty($form))
        {
            return false;
        }

        return $form;
    }

    /**
     * Method to get the data that should be injected in the form.
     *
     * @return    mixed    The data for the form.
     */
    protected function loadFormData()
    {
        return GSD\Helper::getParams();
    }

    /**
     *  Method to validate form data.
     */
    public function validate($form, $data, $group = null)
    {
        $name = $data['name'];
        unset($data["name"]);

        return array(
            'name'   => $name,
            'params' => json_encode($data)
        );
    }
}

models/item.php000064400000015363152345574300007516 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\MVC\Model\AdminModel;
use Joomla\CMS\Table\Table;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Factory;

/**
 * Item Model Class
 */
class GSDModelItem extends AdminModel
{
    /**
     * Returns a reference to the a Table object, always creating it.
     *
     * @param       type    The table type to instantiate
     * @param       string  A prefix for the table class name. Optional.
     * @param       array   Configuration array for model. Optional.
     * @return      Table  A database object
     * @since       2.5
     */
    public function getTable($type = 'Item', $prefix = 'GSDTable', $config = array()) 
    {
        return Table::getInstance($type, $prefix, $config);
    }

    /**
     * Method to allow derived classes to preprocess the form.
     *
     * @param   Form   $form   A Form object.
     * @param   mixed   $data   The data expected for the form.
     * @param   string  $group  The name of the plugin group to import (defaults to "content").
     *
     * @return  void
     *
     * @see     FormField
     * @since   1.6
     * @throws  Exception if there is an error in the form event.
     */
    protected function preprocessForm(Form $form, $data, $group = 'content')
    {
        // Add some useful field paths
        $form->addFieldPath(__DIR__ . '/forms/fields');
        $form->addFieldPath(JPATH_PLUGINS . '/system/nrframework/fields');
        $form->addFieldPath(JPATH_PLUGINS . '/system/nrframework/NRFramework/Fields');

        // When item is not saved yet, the $data variable is type of Array.
        $tempData = (object) $data;

        if (isset($tempData->contenttype))
        {
            $form->loadFile(JPATH_COMPONENT_ADMINISTRATOR . '/models/forms/contenttypes/' . $tempData->contenttype . '.xml');
        }

        parent::preprocessForm($form, $data, $group);
    }

    /**
     * Method to get the record form.
     *
     * @param       array   $data           Data for the form.
     * @param       boolean $loadData       True if the form is to load its own data (default case), false if not.
     * @return      mixed   A Form object on success, false on failure
     * @since       2.5
     */
    public function getForm($data = array(), $loadData = true)
    {
        // Get the form.
        $form = $this->loadForm('com_gsd.item', 'item', array('control' => 'jform', 'load_data' => $loadData));

        if (empty($form)) 
        {
            return false;
        }

        return $form;
    }

    /**
     * Method to get the data that should be injected in the form.
     *
     * @return    mixed    The data for the form.
     */
    protected function loadFormData()
    {
        $app = Factory::getApplication();

        // Check the session for previously entered form data.
        $data = $app->getUserState('com_gsd.edit.item.data', array());

        if (empty($data))
        {
            $data = $this->getItem();
        }

        return $data;
    }

    /**
     * Method to validate form data.
     */
    public function validate($form, $data, $group = null)
    {
        // Keep first 100 characters, based on db schema.
        if (strlen($data['title']) > 100)
        {
            $data['title'] = mb_substr($data['title'], 0, 100);
        }

        $newdata = array();
        $params  = array();

        $this->_db->setQuery('SHOW COLUMNS FROM #__gsd');

        $dbkeys = $this->_db->loadObjectList('Field');
        $dbkeys = array_keys($dbkeys);

        foreach ($data as $key => $val)
        {
            if (in_array($key, $dbkeys))
            {
                $newdata[$key] = $val;
            }
            else
            {
                $params[$key] = $val;
            }
        }

        if (!isset($newdata['params']))
        {
            $newdata['params'] = json_encode($params);
        }

        return $newdata;
    }

    /**
     * Remove timezone offset from all Date properties and store them in UTC in the database. 
     * 
     * @todo Consider this as a temporary workaround. We should create classes per content type and let each class manipulate their data independently.
     *
     * @param   array  The form data.
     *
     * @return  boolean  True on success.
     */
    public function save($data)
    {
        if (!isset($data['contenttype']) && empty($data['contenttype']))
        {
            return parent::save($data);
        }

        // Let schema validate its properties
        $params = json_decode($data['params'], true);

        \GSD\Schemas\Helper::getInstance($data['contenttype'])->onSave($params[$data['contenttype']]);
        
        $data['params'] = json_encode($params);

        return parent::save($data);
    }

    /**
     *  [getItem description]
     *
     *  @param   [type]  $pk  [description]
     *
     *  @return  [type]       [description]
     */
    public function getItem($pk = null)
    {
        if ($item = parent::getItem($pk))
        {
            $params = $item->params;

            if (is_array($params) && count($params))
            {
                foreach ($params as $key => $value)
                {
                    if (!isset($item->$key) && !is_object($value))
                    {
                        $item->$key = $value;
                    }
                }

                unset($item->params);
            }
        }

        $input = Factory::getApplication()->input;

        if ($input->get('override_item'))
        {
            $item->title       = $input->getString('title');
            $item->contenttype = $input->get('contenttype');
            $item->plugin      = $input->get('plugin');
            $item->assignments = $input->get('assignments', '', 'Array');
        }

        return $item;
    }

    /**
     * Method to copy an item
     *
     * @access    public
     * @return    boolean    True on success
     */
    public function copy($id)
    {
        $item = $this->getItem($id);

        $item->id = 0;
        $item->state = 0;
        
        // Keep first 100 characters of title on export, based on db schema
        $item->title = mb_substr($item->title, 0, 50);

        $item = $this->validate(null, get_object_vars($item));

        return ($this->save($item));
    }
}

models/items.php000064400000014003152345574300007667 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\MVC\Model\ListModel;
use Joomla\CMS\Factory;

class GSDModelItems extends ListModel
{
    /**
     * Constructor.
     *
     * @param    array    An optional associative array of configuration settings.
     *
     * @see        JController
     */
    public function __construct($config = array())
    {
        if (empty($config['filter_fields']))
        {
            $config['filter_fields'] = array(
                'id', 'a.id',
                'state', 'a.state',
                'title', 'a.title',
                'created', 'a.created',
                'search',
                'ordering', 'a.ordering',
                'plugin', 'a.plugin',
                'contenttype', 'a.contenttype',
                'language', 'a.language',
                'targetpages'
            );
        }

        parent::__construct($config);
    }
   
	/**
	 * Method to auto-populate the model state.
	 *
	 * This method should only be called once per instantiation and is designed
	 * to be called on the first call to the getState() method unless the model
	 * configuration flag to ignore the request is set.
	 *
	 * Note. Calling getState in this method will result in recursion.
	 *
	 * @param   string  $ordering   An optional ordering field.
	 * @param   string  $direction  An optional direction (asc|desc).
	 *
	 * @return  void
	 *
	 * @since   3.0.1
	 */
	protected function populateState($ordering = 'ordering', $direction = 'ASC')
	{
		$language = $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', '');
        $this->setState('filter.language', $language);

        parent::populateState($ordering, $direction);
    }

    /**
     * Method to build an SQL query to load the list data.
     *
     * @return      string  An SQL query
     */
    protected function getListQuery()
    {
        // Create a new query object.
        $db = Factory::getDBO();
        $query = $db->getQuery(true);

        // Select some fields from the item table
        $query
            ->select('a.*')
            ->from('#__gsd a');

        // Filter by search
        $search = $this->getState('filter.search');
        if (!empty($search))
        {
            if (stripos($search, 'id:') === 0)
            {
                $query->where('a.id = ' . (int) substr($search, 3));
            }
            else
            {
                $query->where('a.title LIKE ' . $db->quote('%' . trim($search) . '%') . '');
            }
        }  

        // Filter State
        $state = $this->getState('filter.state');
        if (is_numeric($state))
        {
            $query->where('a.state = ' . (int) $state);
        }

        if ($state && strpos($state, ',') !== false)
        {
            $query->where('a.state IN (' . $state . ')');
        }

        if ($state == '')
        {
            $query->where('a.state IN (0,1,2)');
        }

        // Filter Content Type
        if ($thing = $this->getState('filter.contenttype'))
        {
            $query->where('a.contenttype = ' . $db->q($thing));
        }

        // Filter Plugin
        if ($plugin = $this->getState('filter.plugin'))
        {
            $query->where('a.plugin = ' . $db->q($plugin));
        }

        // Filter Publishing Rules
        if ($targetpages = $this->getState('filter.targetpages'))
        {
            $operator = ($targetpages == 'specific') ? 'LIKE' : 'NOT LIKE';
            $query->where($db->quoteName('a.params') . $operator . $db->q('%assignment_state":"1"%'));
        }

		// Filter on the App view
        if ($appview = $this->getState('filter.appview'))
		{
            $appview = is_array($appview) ? $appview : (array) $appview;
            $query->where('a.appview IN (' . implode(',', $db->q($appview)) . ')');
		}

		// Filter on the language.
        if ($language = $this->getState('filter.language'))
		{
            $language = is_array($language) ? $language : (array) $language;
            $query->where('a.language IN (' . implode(',', $db->q($language)) . ')');
		}
        
        // Add the list ordering clause.
        $orderCol  = $this->state->get('list.ordering', 'a.id');
        $orderDirn = $this->state->get('list.direction', 'desc');

        $query->order($db->escape($orderCol . ' ' . $orderDirn));

        return $query;
    }

    /**
     *  [getItems description]
     *
     *  @return  object
     */
    public function getItems()
    {
        if (!$items = parent::getItems())
        {
            return [];
        }

        $db = Factory::getDbo();

        foreach ($items as &$item)
        {
            $item = (object) array_merge((array) $item, (array) json_decode($item->params));

            // We don't join the #__languages Joomla core table in the getListQuery() method in order to prevent
            // the "Illegal mix of collations" MySQL error caused by different collations in the joined columns.
            if (Factory::getApplication()->isClient('administrator'))
            {
                $query = $db->getQuery(true)
                    ->select('title AS language_title, image AS language_image')
                    ->from('#__languages')
                    ->where('lang_code = ' . $db->q($item->language));

                $db->setQuery($query);

                $lang = $db->loadAssoc();

                $item->language_title = $lang ? $lang['language_title'] : null;
                $item->language_image = $lang ? $lang['language_image'] : null;
            }
        }

        return $items;
    }
}sql/updates/mysql/4.0.0.sql000064400000000523152345574300011345 0ustar00ALTER TABLE `#__gsd` 
    ADD `title` VARCHAR(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL AFTER `id`, 
    ADD `contenttype` VARCHAR(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL AFTER `title`, 
    ADD `note` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NULL AFTER `ordering`,
    ADD INDEX `plugin` (`plugin`);sql/updates/mysql/4.4.0.sql000064400000000200152345574300011341 0ustar00ALTER TABLE `#__gsd` ADD `language` VARCHAR(7) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '*' AFTER `ordering`;sql/updates/mysql/5.1.0.sql000064400000000176152345574300011353 0ustar00ALTER TABLE `#__gsd` ADD `appview` VARCHAR(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '*' AFTER `plugin`;sql/gsd.sql000064400000001533152345574300006653 0ustar00CREATE TABLE IF NOT EXISTS `#__gsd` (
  `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  `title` varchar(100) NOT NULL,
  `contenttype` varchar(100) NOT NULL,
  `params` text,
  `plugin` varchar(50) NOT NULL DEFAULT '0' COMMENT 'The plugin name of the referenced item',
  `appview` varchar(50) NOT NULL DEFAULT '*',
  `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `ordering` int(11) NOT NULL DEFAULT '0',
  `language` varchar(7) NOT NULL DEFAULT '*',
  `note` text NOT NULL,
  `state` tinyint(3) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  KEY `state` (`state`),
  KEY `plugin` (`plugin`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `#__gsd_config` (
  `name` varchar(255) NOT NULL,
  `params` text NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='Store any configuration in key => params maps';sql/uninstall.gsd.sql000064400000000062152345574300010657 0ustar00DROP TABLE IF EXISTS 
`#__gsd`,
`#__gsd_config`;tables/config.php000064400000003133152345574300010004 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\Table\Table;
use Joomla\CMS\Factory;

class GSDTableConfig extends Table
{
    /**
     * Constructor
     *
     * @param object Database connector object
     */
    function __construct(&$db) 
    {
        parent::__construct('#__gsd_config', 'name', $db);
    }

    /**
     *  Store method
     *
     *  @param   string  $key  The config name
     */
    public function store($key = 'config')
    {
        $db    = Factory::getDBo();
        $table = $this->_tbl;
        $key   = empty($this->name) ? $key : $this->name;

        // Check if key exists
        $result = $db->setQuery(
            $db->getQuery(true)
                ->select('COUNT(*)')
                ->from($db->quoteName($this->_tbl))
                ->where($db->quoteName('name') . ' = ' . $db->quote($key))
        )->loadResult();

        $exists = $result > 0 ? true : false;

        // Prepare object to be saved
        $data = new \stdClass();
        $data->name   = $key;
        $data->params = $this->params;

        if ($exists)
        {
            return $db->updateObject($table, $data, 'name');
        }

        return $db->insertObject($table, $data);
    }
}tables/item.php000064400000001425152345574300007477 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\Table\Table;
use Joomla\CMS\Factory;

class GSDTableItem extends Table
{
    /**
     * Constructor
     *
     * @param object Database connector object
     */
    function __construct(&$db) 
    {
    	$this->setColumnAlias('published', 'state');
    	$this->created = Factory::getDate()->toSql();

        parent::__construct('#__gsd', 'id', $db);
    }
}views/config/tmpl/default.php000064400000005375152345574300012301 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;

HTMLHelper::stylesheet('com_gsd/styles.css', ['relative' => true, 'version' => 'auto']);

HTMLHelper::_('behavior.formvalidator');

$tabs_prefix = 'uitab';

if (!defined('nrJ4'))
{
    $tabs_prefix = 'bootstrap';
    HTMLHelper::_('bootstrap.popover');
    HTMLHelper::_('behavior.modal');
    HTMLHelper::_('behavior.tabstate');
    HTMLHelper::_('formbehavior.chosen', 'select');
}
else 
{
    Factory::getDocument()->addStyleDeclaration('
        #content select:not([class*="input-"]), #content input:not([class*="input-"]) {
            max-width:270px;
        }
    ');

    NRFramework\HTML::fixFieldTooltips();
}

?>

<div class="nr-app<?php echo defined('nrJ4') ? ' j4' : ''; ?> nr-app-config">
    <div class="nr-row">
        <?php echo !defined('nrJ4') ? $this->sidebar : '' ?>
        <div class="nr-main-container">
            <div class="nr-main-header">
                <h2><?php echo Text::_('GSD_CONFIG'); ?></h2>
                <p><?php echo Text::_('GSD_CONFIG_DESC'); ?></p>
            </div>
            <div class="nr-main-content">
        		<form action="<?php echo Route::_('index.php?option=com_gsd&view=config'); ?>" method="post" name="adminForm" id="adminForm" class="form-validate">
    		      <div class="form-horizontal">
                    	<?php 
                            echo HTMLHelper::_($tabs_prefix . '.startTabSet', 'tab', ['recall' => true, 'active' => 'globaldata']);

                            foreach ($this->form->getFieldSets() as $key => $fieldset)
                            {
                                echo HTMLHelper::_($tabs_prefix . '.addTab', 'tab', $fieldset->name, Text::_($fieldset->label));
                                echo $this->form->renderFieldSet($fieldset->name);
                                echo HTMLHelper::_($tabs_prefix . '.endTab');
                            }

                            echo HTMLHelper::_($tabs_prefix . '.endTabSet');
                        ?>
        		    </div>

        		    <?php echo HTMLHelper::_('form.token'); ?>
        		    <input type="hidden" name="task" value="" />
        		    <input type="hidden" name="name" value="config" />
        		</form>
            </div>
        </div>
    </div>
</div>views/config/view.html.php000064400000003757152345574300011620 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
defined('_JEXEC') or die('Restricted access');
 
use GSD\Helper;
use Joomla\CMS\MVC\View\HtmlView;
use Joomla\CMS\Factory;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Toolbar\ToolbarHelper;

/**
 * Config View
 */
class GSDViewConfig extends HtmlView
{
    /**
     * Items view display method
     * 
     * @param   string  $tpl  The name of the template file to parse; automatically searches through the template paths.
     * 
     * @return  mixed  A string if successful, otherwise a JError object.
     */
    function display($tpl = null)
    {
        // Check for errors.
        if (!is_null($this->get('Errors')) && count($errors = $this->get('Errors')))
        {
            Factory::getApplication()->enqueueMessage(implode("\n", $errors), 'error');
            return false;
        }

        $this->form    = $this->get('Form');
        $this->config  = ComponentHelper::getParams('com_gsd');
        $this->sidebar = Helper::renderSideBar();

        // Set the toolbar
        $this->addToolBar();

        // Display the template
        parent::display($tpl);
    }

    /**
     *  Add Toolbar to layout
     */
    protected function addToolBar() 
    {
        $canDo = Helper::getActions();

        ToolbarHelper::title(Text::_('GSD') . ': ' . Text::_('GSD_CONFIG'));

        if ($canDo->get('core.admin'))
        {
            ToolbarHelper::preferences('com_gsd');
        }

        ToolbarHelper::apply('config.apply');

        ToolbarHelper::help('Help', false, 'https://www.tassos.gr/joomla-extensions/google-structured-data-markup/docs');
    }
}views/gsd/tmpl/default.php000064400000014604152345574300011604 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;

// Load media files
if (!defined('nrj4'))
{
    HTMLHelper::_('jquery.framework');
    HTMLHelper::script('com_gsd/script.js', ['relative' => true, 'version' => 'auto']);
}

HTMLHelper::stylesheet('com_gsd/styles.css', ['relative' => true, 'version' => 'auto']);
?>
<div class="nr-app <?php echo defined('nrJ4') ? 'j4' : '' ?>">
    <div class="nr-row">
        <?php echo !defined('nrJ4') ? $this->sidebar : '' ?>
        <div class="nr-main-container">
            <?php
                // Display extension notices
                \NRFramework\Notices\Notices::getInstance([
                    'ext_element' => 'gsd',
                    'ext_type' => 'plugin',
                    'ext_xml' => 'plg_system_gsd'
                ])->show();
            ?>
            <div class="nr-main-header">
                <h2><?php echo Text::_('NR_DASHBOARD'); ?></h2>
                <p><?php echo Text::_('GSD_DASHBOARD_DESC'); ?></p>
            </div>
            <div class="nr-main-content">
                <div class="tile is-ancestor">
                    <div class="tile is-vertical">
                        <div class="tile">
                            <div class="tile is-parent">
                                <div class="tile is-child">
                                    <div class="nr-box nr-box-hr">
                                        <div class="nr-box-title">
                                            <a href="<?php echo Uri::base() ?>index.php?option=com_gsd&view=items">
                                                <?php echo Text::_('GSD_TOTAL_ITEMS'); ?>
                                            </a>
                                            <div><?php echo Text::_('GSD_TOTAL_ACTIVE_ITEMS'); ?></div>
                                        </div>
                                        <div class="nr-box-content text-right text-end">
                                            <span class="nr-number"><?php echo $this->stats['itemsCount']; ?></span>
                                        </div>
                                    </div>
                                </div>
                            </div>
                            <div class="tile is-parent">
                                <div class="tile is-child">
                                    <div class="nr-box nr-box-hr">
                                        <div class="nr-box-title">
                                            <a href="<?php echo Uri::base() ?>index.php?option=com_gsd&view=config&layout=edit#globaldata">
                                                <?php echo Text::_('GSD_GLOBAL_DATA'); ?>
                                            </a>
                                            <div><?php echo Text::_('GSD_GLOBAL_DATA_SUBHEADING'); ?></div>
                                        </div>
                                        <div class="nr-box-content text-right text-end">
                                            <span class="nr-number">
                                                <?php echo $this->stats['siteDataEnabled'] ?>/<?php echo count($this->stats['siteData']) ?>
                                            </span>
                                        </div>
                                    </div>
                                </div>
                            </div>
                            <div class="tile is-parent">
                                <div class="tile is-child">
                                    <div class="nr-box nr-box-hr">
                                        <div class="nr-box-title">
                                            <a href="<?php echo Uri::base() ?>index.php?option=com_gsd&view=config&layout=edit#integrations">
                                                <?php echo Text::_('GSD_INTEGRATIONS'); ?>
                                            </a>
                                            <div><?php echo Text::_('GSD_TOTAL_ACTIVE_INTEGRATIONS'); ?></div>
                                        </div>
                                        <div class="nr-box-content text-right text-end">
                                            <span class="nr-number">
                                                 <?php echo $this->stats['integrationsEnabled'] ?>/<?php echo count($this->stats['integrations']) ?>
                                            </span>
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </div>
                        <div class="tile">
                            <div class="tile is-parent">
                                <div class="tile is-child nr-box">
                                    <?php echo $this->loadTemplate('contenttypes'); ?>
                                </div>
                            </div>
                            <div class="tile is-parent">
                                <div class="tile is-child nr-box">
                                    <?php echo $this->loadTemplate('sitedata'); ?>
                                </div>
                            </div>
                        </div>
                        <div class="tile">
                            <div class="tile is-parent">
                                <div class="tile is-child">
                                    <?php echo $this->loadTemplate('tester'); ?>
                                </div>
                            </div>
                        </div>
                    </div>
                    <div class="tile is-3 is-parent">
                        <div class="tile is-child nr-box">
                            <?php echo $this->loadTemplate('right'); ?>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>views/gsd/tmpl/default_contenttypes.php000064400000002443152345574300014421 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Language\Text;

$url = Uri::base() . 'index.php?option=com_gsd&view=items';

?>

<div class="nr-box-title">
    <a href="<?php echo $url ?>"><?php echo Text::_('GSD_ITEMS_OVERVIEW'); ?></a>
    <div><?php echo Text::_('GSD_ITEMS_OVERVIEW_DESC'); ?></div>
</div>
<div class="nr-box-content" style="max-height:300px; overflow:auto;">
    <table class="nr-app-stats">
        <?php foreach ($this->stats['items'] as $key => $item) { ?>
        <tr>
            <td>
                <?php echo Text::_('GSD_' . $key); ?>
                <div class="bar"><span style="width:<?php echo $item['share']; ?>%"></span></div>
            </td>
            <td width="12%" class="text-center"><?php echo $item['count']; ?></td>
            <td width="12%" class="text-center"><?php echo $item['share']; ?>%</td>
        </tr>
        <?php } ?>
    </table>
</div>
views/gsd/tmpl/default_right.php000064400000004210152345574300012771 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

defined('_JEXEC') or die('Restricted Access');

use Joomla\CMS\Language\Text;
use NRFramework\Functions;

$installedVersion = Functions::getExtensionVersion("plg_system_gsd", false);
$isPro = Functions::extensionHasProInstalled("plg_system_gsd");
$FreePro = $isPro ? "Pro" : "Free";
?>
<div class="mod mod-version-check">
    <div class="mod-head"><?php echo Text::_('GSD') . " " . $FreePro ?></div>
    <div class="mod-content">
        <p>
            <?php echo Text::_('GSD_INSTALLED_VERSION') ?>: <?php echo $installedVersion; ?>
        </p>
        
    </div>
</div>

<div class="mod">
    <div class="mod-head">
        <span class="icon-star"></span>
        <?php echo Text::_("NR_LIKE_THIS_EXTENSION") ?>
    </div>
    <div class="mod-content">
        <p>
            <?php echo Text::_("GSD_WRITE_REVIEW_ON_JED") ?>
            <a href="https://extensions.joomla.org/extensions/extension/search-a-indexing/web-search/google-structured-data/" target="_blank"><?php echo Text::_("NR_LEAVE_A_REVIEW") ?></a>
        </p>
    </div>
</div>

<div class="mod">
    <div class="mod-head">
        <span class="icon-heart"></span>
        <?php echo Text::_("GSD_FOLLOW_US") ?>
    </div>
    <div class="mod-content">
        <ul class="socialNav">
            <li><a target="_blank" href="https://www.facebook.com/wwwtassosgr/"><?php echo Text::_("GSD_LIKE_FACEBOOK") ?></a></li>
            <li><a target="_blank" href="https://twitter.com/tassosm"><?php echo Text::_("GSD_FOLLOW_TWITTER") ?></a></li>
            <li><a target="_blank" href="https://plus.google.com/u/0/+TassosMarinos85"><?php echo Text::_("GSD_FOLLOW_GOOGLE_PLUS") ?></a></li>
        </ul>
    </div>
</div>

<div class="mod copy">
    &copy; <?php echo Text::sprintf('NR_COPYRIGHT', date("Y")) ?></p>
</div>views/gsd/tmpl/default_sitedata.php000064400000002175152345574300013462 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Language\Text;

$url = Uri::base() . 'index.php?option=com_gsd&view=config&layout=edit#globaldata';

?>

<div class="nr-box-title">
    <a href="<?php echo $url ?>"><?php echo Text::_('GSD_GLOBAL_DATA'); ?> Overview</a>
    <div><?php echo Text::_('GSD_STATUS_OF_SD'); ?></div>
</div>
<div class="nr-box-content">
    <table class="nr-app-stats">
        <?php foreach ($this->stats['siteData'] as $key => $item) { ?>
        <tr>
            <td><?php echo Text::_($key); ?></td>
            <td width="12%" class="text-right">
                <span class="icon-<?php echo $item ? "publish" : "unpublish" ?>"></span>
            </td>
        </tr>
        <?php } ?>
    </table>
</div>
views/gsd/tmpl/default_tester.php000064400000002046152345574300013167 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

defined('_JEXEC') or die('Restricted Access');

use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Language\Text;

?>

<div class="nr-box nr-box-hr">
    <div class="nr-box-title col-md-4">
        <?php echo Text::_('GSD_SDTT'); ?>
        <div><?php echo Text::_('GSD_SDTT_DESC'); ?></div>
    </div>
    <div class="nr-box-content">
        <form action="https://search.google.com/test/rich-results" class="gsdtt" target="_blank" method="get">
            <input id="url" name="url" required="true" type="url" placeholder="https://" value="<?php echo Uri::root(); ?>"/>
            <button class="btn btn-primary" type="submit"><?php echo Text::_('GSD_TEST'); ?></button>
        </form>
    </div>
</div>views/gsd/view.html.php000064400000003604152345574300011117 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use GSD\Helper;
use Joomla\CMS\MVC\View\HtmlView;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Toolbar\ToolbarHelper;

require_once JPATH_COMPONENT_ADMINISTRATOR . '/helpers/dashboard.php';
 
/**
 * Dashboard View
 */
class GSDViewGSD extends HtmlView
{
    /**
     * Items view display method
     * 
     * @param   string  $tpl  The name of the template file to parse; automatically searches through the template paths.
     * 
     * @return  mixed  A string if successful, otherwise a JError object.
     */
    function display($tpl = null)
    {
        $this->sidebar = Helper::renderSideBar();
        $this->stats   = GSDDashboard::getStats();

        // Check for errors.
        if (!is_null($this->get('Errors')) && count($errors = $this->get('Errors')))
        {
            Factory::getApplication()->enqueueMessage(implode("\n", $errors), 'error');
            return false;
        }

        // Set the toolbar
        $this->addToolBar();

        // Display the template
        parent::display($tpl);
    }

    /**
     *  Add Toolbar to layout
     */
    protected function addToolBar() 
    {
        $canDo = Helper::getActions();

        ToolBarHelper::title(Text::_('GSD'));

        if ($canDo->get('core.admin'))
        {
            ToolbarHelper::preferences('com_gsd');
        }

        ToolbarHelper::help("Help", false, "http://www.tassos.gr/joomla-extensions/google-structured-data-markup/docs");
    }
}views/gsd/view.raw.php000064400000003414152345574300010743 0ustar00<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_modules
 *
 * @copyright   Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

defined('_JEXEC') or die;

use Joomla\CMS\MVC\View\HtmlView;
use Joomla\CMS\Factory;
use Joomla\CMS\Table\Table;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\Layout\LayoutHelper;

class GSDViewGSD extends HtmlView
{
	/**
	 * Display the view
	 *
	 * @param   string  $tpl  The name of the template file to parse; automatically searches through the template paths.
	 *
	 * @return  void
	 */
	public function display($tpl = null)
	{
		$app  = Factory::getApplication();
		$task = $app->input->get('do');

		switch ($task)
		{
			// Enable/Disable plugin
			case 'pluginState':

				// Abort if no plugin ID is passed.
				if (!$id = $app->input->getInt('plugin_id', null))
				{
					return;
				}

				// Load plugin's row and change it's state
				$table = Table::getInstance('Extension', 'Joomla\\CMS\\Table\\');
				$table->load($id);
				$table->enabled = $app->input->get('state', '1');
				$table->store();

				// Print plugin's new state
				echo $table->enabled;

				Factory::getCache()->clean('com_plugins');

				break;

			// Delete Item
			case 'delete':
				$model = BaseDatabaseModel::getInstance('Item', 'GSDModel', ['ignore_request' => true]);
				if ($id = $app->input->get('pk', null))
				{
					echo $model->publish($id, -2);
				}
				break;

			// Render Items for Fast Edit field
			default:
				require_once JPATH_COMPONENT_ADMINISTRATOR . '/models/forms/fields/fastedit.php';
				$items = (new JFormFieldFastEdit())->getRows();
				echo LayoutHelper::render('fastedit', ['items' => $items]);
				break;
		}
	}
}
views/item/tmpl/edit.php000064400000020712152345574300011263 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Form\Form;

HTMLHelper::_('behavior.formvalidator');
HTMLHelper::_('behavior.keepalive');

HTMLHelper::stylesheet('com_gsd/styles.css', ['relative' => true, 'version' => 'auto']);

$doc = Factory::getDocument();

$doc->addScriptDeclaration('
    window.GSDReloadForm = () => {
        const isJ3 = typeof Joomla.loadingLayer == "function";

        if (isJ3) {
            Joomla.loadingLayer("show");
        } else {
            document.body.appendChild(document.createElement("joomla-core-loader"));
        }

        document.querySelector("input[name=task]").value = "item.reload";
        Joomla.submitform("item.reload", document.getElementById("adminForm"));
    };
');

if (defined('nrJ4'))
{
    NRFramework\HTML::fixFieldTooltips();

    $doc->getWebAssetManager()->useScript('webcomponent.core-loader');
    $doc->addStyleDeclaration('
        #content select:not([class*="input-"]), #content input:not([class*="input-"]) {
            max-width:270px;
        }
    ');

} else 
{
    HTMLHelper::_('jquery.framework');
    HTMLHelper::script('com_gsd/script.js', ['relative' => true, 'version' => 'auto']);
    HTMLHelper::_('formbehavior.chosen', 'select:not(.select2)');
}

$input = Factory::getApplication()->input;

// In case of modal
$isModal = $input->get('layout') == 'modal' ? true : false;
$layout  = $isModal ? 'modal' : 'edit';
$tmpl    = $isModal || $input->get('tmpl', '', 'cmd') === 'component' ? '&tmpl=component' : '';

$formData = $this->form->getData();
$selectedApp = $formData->get('plugin');
$selectedAppView = $formData->get('appview');
$selectedContentType = $formData->get('contenttype');

#FREE-START
// This style block is required in the Free version where the assignmentselection.css file is missing.
$doc->addStyleDeclaration('
    .assign {
        background-color: #F0F0F0;
        border: solid 1px #DEDEDE;
        color: inherit !important;
        padding: 10px 12px;
        margin-bottom: -1px;
    }
    .assign .control-group {
        margin: 0;
    }
');
#FREE-START

?>

<script type="text/javascript">
    Joomla.submitbutton = function(task) {
        var form = document.getElementById('adminForm');

        if (task == 'item.cancel' || document.formvalidator.isValid(form)) {
            Joomla.submitform(task, form);

            <?php if ($isModal) { ?>
            if (task !== 'item.apply') {
                <?php if (!defined('nrJ4')): ?>
                window.parent.jQuery('#gsdModal').modal('hide');
                <?php else: ?>
                if (window.parent.Joomla.Modal.getCurrent())
                {
                    window.parent.Joomla.Modal.getCurrent().close();
                }
                <?php endif; ?>
            }
            <?php } ?>
        }
    }
</script>

<div class="nr-app<?php echo (defined('nrJ4') ? ' j4' : '') . ($isModal ? ' nr-isModal' : ''); ?>">
    <div class="nr-row">
        <div class="nr-main-container">
            <div class="nr-main-content">
                <form action="<?php echo Route::_('index.php?option=com_gsd&view=item&layout=edit&id=' . (int) $this->item->id); ?>" method="post" name="adminForm" id="adminForm" class="form-validate">
                    <div class="form-horizontal">
                        <div class="<?php echo defined('nrJ4') ? 'row' : 'row-fluid' ?>">
                            <div class="span9 col-md-9">
                                <?php 
                                    echo $this->form->renderFieldSet('top'); 
                                ?>

                                <!-- Content Type -->
                                <?php if ($selectedContentType) { ?>
                                    <div class="well nr-well">
                                        <h4><?php echo Text::_('GSD_' . strtoupper($selectedContentType)) ?></h4>
                                        <div class="well-desc"><?php echo Text::_('GSD_MAP_DESC') ?></div>
                                        <?php echo $this->form->renderFieldSet('contenttype');  ?>
                                    </div>
                                <?php } ?>

                                <!-- Conditions -->
                                <?php if ($selectedApp) { ?>
                                    <?php
                                        $conditions = [];
                                        $conditionsForm = new Form(null);

                                        // Default XML checks the assignments.xml
                                        $xmlFile = JPATH_PLUGINS . '/gsd/' . $selectedApp . '/form/assignments.xml';

                                        // However, some apps (J-Business Directory) have a separate XML per app view
                                        if ($selectedAppView && !file_exists($xmlFile))
                                        {
                                            $xmlFile = JPATH_PLUGINS . '/gsd/' . $selectedApp . '/form/' . $selectedAppView . '.xml';
                                        }

                                        // Finally, ensure the XML file exists
                                        if (file_exists($xmlFile))
                                        {
                                            $conditionsForm->loadFile($xmlFile);
                                            $conditions = array_keys($conditionsForm->getFieldSets());
                                        }
                                        
                                        $integration = Text::_('PLG_GSD_' . $selectedApp . '_ALIAS');
                                    ?>
                                    
                                    <div class="well nr-well <?php echo $isModal ? 'hide' : '' ?>">
                                        <h4><?php echo Text::_('GSD_ITEM_RULES') ?></h4>
                                        <div class="well-desc">
                                            <?php 
                                                echo Text::sprintf(
                                                    'GSD_ITEM_PUBLISHING_ASSIGNMENTS_DESC', 
                                                    Text::_('GSD_' . $selectedContentType), 
                                                    $selectedApp
                                                ) 
                                            ?>
                                        </div>

                                        <?php if ($conditions) { ?>
                                            <?php foreach ($conditions as $condition) { ?>
                                                <div class="assign">
                                                    <?php echo $this->form->renderFieldSet($condition); ?>
                                                </div>
                                            <?php } ?>
                                        <?php } else { ?>
                                            <?php echo Text::sprintf('GSD_NO_FILTERS_NOTICE', $integration, $integration); ?>
                                        <?php } ?>
                                    </div>
                                <?php } ?>
                            </div>
                            <div class="span3 col-md-3 form-vertical form-no-margin">
                                <?php echo $this->form->renderFieldSet('main'); ?>
                            </div>
                        </div>
                    </div>

                    <?php echo HTMLHelper::_('form.token'); ?>
                    <input type="hidden" name="task" value="" />

                    <?php if ($isModal) { ?>
                        <input type="hidden" name="layout" value="modal" />
                        <input type="hidden" name="tmpl" value="component" />
                    <?php } ?>
                </form>
            </div>
        </div>
    </div>
</div>
views/item/tmpl/modal.php000064400000002322152345574300011427 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\Factory;

// Dropdown at the bottom of the page breaks layout. https://github.com/harvesthq/chosen/issues/155
// Here's a quick and dirty fix but we need a better way to calculate the remaining height and decide if
// it's going to be dropped down or up.
Factory::getDocument()->addStyleDeclaration('
    .nr-main-content {
        padding-bottom: 245px;
    }
');

?>

<div class="container-popup">
    <?php $this->setLayout('edit'); ?>
    <?php echo $this->loadTemplate(); ?>
</div>

<div class="hidden">
    <button id="applyBtn" type="button" onclick="Joomla.submitbutton('item.apply');"></button>
    <button id="saveBtn" type="button" onclick="Joomla.submitbutton('item.save');"></button>
    <button id="closeBtn" type="button" onclick="Joomla.submitbutton('item.cancel');"></button>
</div>views/item/view.html.php000064400000005070152345574300011277 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use GSD\Helper;
use Joomla\CMS\MVC\View\HtmlView;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Factory;
use Joomla\CMS\Toolbar\ToolbarHelper;

/**
 * Campaign View Class
 */
class GSDViewItem extends HtmlView
{
    /**
     * display method of Item view
     * @return void
     */
    public function display($tpl = null) 
    {
        // Check for errors.
        if (!is_null($this->get('Errors')) && count($errors = $this->get('Errors')))
        {
            Factory::getApplication()->enqueueMessage(implode("\n", $errors), 'error');
            return false;
        }

        // Assign the Data
        $this->form  = $this->get('Form');
        $this->item  = $this->get('Item');
        $this->isnew = is_null($this->item->id) ? true : false;

        // We don't need toolbar in the modal window.
        if ($this->getLayout() !== 'modal')
        {
            $this->addToolbar();
        }

        $this->title = $this->isnew ? Text::_('GSD_NEW_ITEM') : Text::_('GSD_EDIT_ITEM') . ": #" . $this->item->id;

        // Display the template
        parent::display($tpl);
    }

    /**
     * Setting the toolbar
     */
    protected function addToolBar() 
    {
        Factory::getApplication()->input->set('hidemainmenu', true);

        ToolBarHelper::title(Text::_('GSD') . ': ' . ($this->isnew ? Text::_('GSD_NEW_ITEM') : Text::_('GSD_EDIT_ITEM')));

        if (defined('nrJ4'))
        {
            ToolbarHelper::saveGroup(
                [
                    ['apply', 'item.apply'],
                    ['save', 'item.save'],
                    ['save2new', 'item.save2new']
                ],
                'btn-success'
            );

            ToolbarHelper::cancel('item.cancel');
        } else
        {
            ToolbarHelper::apply('item.apply');
            ToolBarHelper::save('item.save');
            ToolbarHelper::save2new('item.save2new');
            ToolBarHelper::cancel('item.cancel', $this->isnew ? 'JTOOLBAR_CANCEL' : 'JTOOLBAR_CLOSE');      
        }

        ToolbarHelper::help('Help', false, "https://www.tassos.gr/joomla-extensions/google-structured-data-markup/docs");
    }
}views/items/tmpl/default.php000064400000023066152345574300012152 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Layout\LayoutHelper;

HTMLHelper::stylesheet('com_gsd/styles.css', ['relative' => true, 'version' => 'auto']);
HTMLHelper::_('jquery.framework');
HTMLHelper::script('com_gsd/script.js', ['relative' => true, 'version' => 'auto']);
HTMLHelper::_('bootstrap.popover');
HTMLHelper::_('formbehavior.chosen', 'select');

$listOrder  = $this->escape($this->state->get('list.ordering'));
$listDirn   = $this->escape($this->state->get('list.direction'));
$showcolors = $this->config->get('colorgroup', true);
$user       = Factory::getUser();

?>

<div class="nr-app">
    <div class="nr-row">
        <?php echo $this->sidebar ?>
        <div class="nr-main-container">
            <div class="nr-main-header">
                <h2><?php echo Text::_('GSD_ITEMS'); ?></h2>
                <p><?php echo Text::_('GSD_ITEMS_VIEW_DESC'); ?></p>
            </div>
            <div class="nr-main-content">
                <form action="<?php echo Route::_('index.php?option=com_gsd&view=items'); ?>" class="nr-app-form" method="post" name="adminForm" id="adminForm">
                <div class="nr-box">
                    <?php echo LayoutHelper::render('joomla.searchtools.default', array('view' => $this)); ?>
                    <table class="adminlist nrTable table">
                        <thead>
                            <tr>
                                <th class="center" width="2%"><?php echo HTMLHelper::_('grid.checkall'); ?></th>
                                <th width="3%" class="nowrap hidden-phone">
                                    <?php echo HTMLHelper::_('searchtools.sort', 'JSTATUS', 'a.state', $listDirn, $listOrder); ?>
                                </th>
                                <?php if ($showcolors) { ?>
                                    <th width="1%"></th>
                                <?php } ?>
                                <th>
                                    <?php echo HTMLHelper::_('searchtools.sort', 'NR_TITLE', 'a.title', $listDirn, $listOrder); ?>
                                </th>
                                <th width="14%">
                                    <?php echo HTMLHelper::_('searchtools.sort', 'GSD_CONTENT_TYPE', 'a.contenttype', $listDirn, $listOrder); ?>
                                </th>
                                <th width="14%">
                                    <?php echo Text::_('GSD_INTEGRATION'); ?>
                                </th>
                                <th width="14%">
                                    <?php echo Text::_('GSD_ITEM_RULES'); ?>
                                </th>
                                <th width="10%" class="nowrap hidden-phone">
							        <?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_LANGUAGE', 'language', $listDirn, $listOrder); ?>
						        </th>
                                <th width="14%">
                                    <?php echo HTMLHelper::_('searchtools.sort', 'NR_CREATED_DATE', 'a.created', $listDirn, $listOrder); ?>
                                </th>
                                <th width="3%" class="text-center nowrap hidden-phone">
                                    <?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?>
                                </th>
                            </tr>
                        </thead>
                        <tbody>
                            <?php if (count($this->items)) { ?>
                                <?php foreach($this->items as $i => $item): ?>
                                    <?php 
                                        $canChange  = $user->authorise('core.edit.state', 'com_gsd.item.' . $item->id);
                                    ?>
                                    <tr>
                                        <td class="center"><?php echo HTMLHelper::_('grid.id', $i, $item->id); ?></td>
                                        <td>
                                            <div class="btn-group">
                                                <?php echo HTMLHelper::_('jgrid.published', $item->state, $i, 'items.', $canChange); ?>
                                                <?php
                                                if ($canChange)
                                                {
                                                    HTMLHelper::_('actionsdropdown.' . ((int) $item->state === -2 ? 'un' : '') . 'trash', 'cb' . $i, 'items');
                                                    HTMLHelper::_('actionsdropdown.' . 'duplicate', 'cb' . $i, 'items');
                                                           
                                                    echo HTMLHelper::_('actionsdropdown.render', $this->escape($item->id));
                                                }
                                                ?>
                                            </div>
                                        </td>
                                        <?php if ($showcolors) : ?>
                                            <td class="center inlist">
                                                <?php $color = isset($item->colorgroup) ? $item->colorgroup : ""; ?>
                                                <span class="boxColor">
                                                    <span style="background-color: <?php echo $color ?>;"></span>
                                                </span>
                                            </td>
                                        <?php endif; ?>
                                        <td>
                                            <a href="<?php echo Route::_('index.php?option=com_gsd&task=item.edit&id='.$item->id); ?>" title="<?php echo Text::_('JACTION_EDIT'); ?>">
                                                <?php echo $item->title; ?>
                                            </a>
                                            <?php if (isset($item->note)) { ?>
                                                <div class="small" style="opacity:.6;"><?php echo $item->note; ?></div>
                                            <?php } ?>
                                        </td>
                                        <td><?php echo Text::_('GSD_' . $item->contenttype); ?></td>
                                        <td>
                                            <?php echo Text::_('PLG_GSD_' . strtoupper($item->plugin) . '_ALIAS'); ?>
                                        </td>
                                        <td>
                                            <?php 
                                                $all_pages = true;

                                                if (isset($item->assignments))
                                                {
                                                    foreach ($item->assignments as $key => $assignment)
                                                    {
                                                        if ($assignment->assignment_state == '1')
                                                        {
                                                            $all_pages = false;
                                                            break;
                                                        }
                                                    }
                                                }
                                            ?>
                                            <?php echo ($all_pages) ? Text::_('COM_GSD_TARGETS_ALL_PAGES') : Text::_('COM_GSD_TARGETS_SPECIFIC_PAGES'); ?>
                                        </td>
                                        <td class="hidden-phone">
                                            <?php echo LayoutHelper::render('joomla.content.language', $item); ?>
                                        </td>
                                        <td><?php echo $item->created; ?></td>
                                        <td class="text-center"><?php echo $item->id ?></td>
                                    </tr>
                                <?php endforeach; ?>  
                            <?php } else { ?>
                                <tr>
                                    <td align="center" colspan="9">
                                        <div align="center">
                                            <?php echo Text::_('NR_NO_ITEMS_FOUND'); ?>
                                        </div>
                                    </td>
                                </tr>
                            <?php } ?>        
                        </tbody>
                    </table>

                    <?php echo $this->pagination->getListFooter(); ?>
                </div>
            </div>
            
            <input type="hidden" name="task" value="" />
            <input type="hidden" name="boxchecked" value="0" />
            <?php echo HTMLHelper::_('form.token'); ?>
        </form>
    </div>
</div>views/items/tmpl/default_4.php000064400000015500152345574300012367 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\Button\PublishedButton;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Factory;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Layout\LayoutHelper;

HTMLHelper::stylesheet('com_gsd/styles.css', ['relative' => true, 'version' => 'auto']);

$listOrder  = $this->escape($this->state->get('list.ordering'));
$listDirn   = $this->escape($this->state->get('list.direction'));
$showcolors = $this->config->get('colorgroup', true);
$user       = Factory::getUser();

?>

<form action="<?php echo Route::_('index.php?option=com_gsd&view=items'); ?>" method="post" name="adminForm" id="adminForm">
    <?php echo LayoutHelper::render('joomla.searchtools.default', array('view' => $this)); ?>
    <table class="adminlist nrTable table">
        <thead>
            <tr>
                <th class="center" width="2%"><?php echo HTMLHelper::_('grid.checkall'); ?></th>
                <th width="3%" class="nowrap hidden-phone">
                    <?php echo HTMLHelper::_('searchtools.sort', 'JSTATUS', 'a.state', $listDirn, $listOrder); ?>
                </th>
                <?php if ($showcolors) { ?>
                    <th width="1%"></th>
                <?php } ?>
                <th>
                    <?php echo HTMLHelper::_('searchtools.sort', 'NR_TITLE', 'a.title', $listDirn, $listOrder); ?>
                </th>
                <th width="14%">
                    <?php echo HTMLHelper::_('searchtools.sort', 'GSD_CONTENT_TYPE', 'a.contenttype', $listDirn, $listOrder); ?>
                </th>
                <th width="14%">
                    <?php echo Text::_('GSD_INTEGRATION'); ?>
                </th>
                <th width="14%">
                    <?php echo Text::_('GSD_ITEM_RULES'); ?>
                </th>
                <th width="10%" class="nowrap hidden-phone">
                    <?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_LANGUAGE', 'language', $listDirn, $listOrder); ?>
                </th>
                <th width="14%">
                    <?php echo HTMLHelper::_('searchtools.sort', 'NR_CREATED_DATE', 'a.created', $listDirn, $listOrder); ?>
                </th>
                <th width="3%" class="text-center nowrap hidden-phone">
                    <?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?>
                </th>
            </tr>
        </thead>
        <tbody>
            <?php if (count($this->items)) { ?>
                <?php foreach($this->items as $i => $item): ?>
                    <?php 
                        $canChange  = $user->authorise('core.edit.state', 'com_gsd.item.' . $item->id);
                    ?>
                    <tr>
                        <td class="center"><?php echo HTMLHelper::_('grid.id', $i, $item->id); ?></td>
                        <td class="text-center">
                            <?php
                                $options = [
                                    'task_prefix' => 'items.',
                                    'disabled' => !$canChange,
                                    'id' => 'state-' . $item->id
                                ];

                                echo (new PublishedButton)->render((int) $item->state, $i, $options);
                            ?>
                        </td>
                        <?php if ($showcolors) : ?>
                            <td class="center inlist">
                                <?php $color = isset($item->colorgroup) ? $item->colorgroup : ""; ?>
                                <span class="boxColor">
                                    <span style="background-color: <?php echo $color ?>;"></span>
                                </span>
                            </td>
                        <?php endif; ?>
                        <td>
                            <a href="<?php echo Route::_('index.php?option=com_gsd&task=item.edit&id='.$item->id); ?>" title="<?php echo Text::_('JACTION_EDIT'); ?>">
                                <?php echo $item->title; ?>
                            </a>
                            <?php if (isset($item->note)) { ?>
                                <div class="small" style="opacity:.6;"><?php echo $item->note; ?></div>
                            <?php } ?>
                        </td>
                        <td><?php echo Text::_('GSD_' . $item->contenttype); ?></td>
                        <td>
                            <?php echo Text::_('PLG_GSD_' . strtoupper($item->plugin) . '_ALIAS'); ?>
                        </td>
                        <td>
                            <?php 
                                $all_pages = true;

                                if (isset($item->assignments))
                                {
                                    foreach ($item->assignments as $key => $assignment)
                                    {
                                        if ($assignment->assignment_state == '1')
                                        {
                                            $all_pages = false;
                                            break;
                                        }
                                    }
                                }
                            ?>
                            <?php echo ($all_pages) ? Text::_('COM_GSD_TARGETS_ALL_PAGES') : Text::_('COM_GSD_TARGETS_SPECIFIC_PAGES'); ?>
                        </td>
                        <td class="hidden-phone">
                            <?php echo LayoutHelper::render('joomla.content.language', $item); ?>
                        </td>
                        <td><?php echo $item->created; ?></td>
                        <td class="text-center"><?php echo $item->id ?></td>
                    </tr>
                <?php endforeach; ?>  
            <?php } else { ?>
                <tr>
                    <td align="center" colspan="9">
                        <div align="center">
                            <?php echo Text::_('NR_NO_ITEMS_FOUND'); ?>
                        </div>
                    </td>
                </tr>
            <?php } ?>        
        </tbody>
    </table>

    <?php echo $this->pagination->getListFooter(); ?>

    <input type="hidden" name="task" value="" />
    <input type="hidden" name="boxchecked" value="0" />
    <?php echo HTMLHelper::_('form.token'); ?>
</form>views/items/view.html.php000064400000011553152345574300011465 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
defined('_JEXEC') or die('Restricted access');

use GSD\Helper;
use Joomla\CMS\Toolbar\Toolbar;
use Joomla\CMS\Toolbar\ToolbarHelper;
use Joomla\CMS\MVC\View\HtmlView;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
 
/**
 * Items View
 */
class GSDViewItems extends HtmlView
{
    /**
     * Items view display method
     * 
     * @param   string  $tpl  The name of the template file to parse; automatically searches through the template paths.
     * 
     * @return  mixed  A string if successful, otherwise a JError object.
     */
    function display($tpl = null) 
    {
        $this->items         = $this->get('Items');
        $this->state         = $this->get('State');
        $this->pagination    = $this->get('Pagination');
        $this->filterForm    = $this->get('FilterForm');
        $this->activeFilters = $this->get('ActiveFilters');
        $this->config        = Helper::getParams();
        $this->sidebar       = Helper::renderSideBar();

        if (defined('nrJ4'))
        {
            $tpl = '4';
        }

        // Check for errors.
        if (!is_null($this->get('Errors')) && count($errors = $this->get('Errors')))
        {
            Factory::getApplication()->enqueueMessage(implode("\n", $errors), 'error');
            return false;
        }

        // Set the toolbar
        $this->addToolBar();

        // Display the template
        parent::display($tpl);
    }

    /**
     *  Add Toolbar to layout
     */
    protected function addToolBar() 
    {
        $canDo = Helper::getActions();
        $state = $this->get('State');
        $viewLayout = Factory::getApplication()->input->get('layout', 'default');

        ToolBarHelper::title(Text::_('GSD') . ': ' . Text::_('GSD_ITEMS'));

        // Joomla J4
        if (defined('nrJ4'))
        {
            $toolbar = Toolbar::getInstance('toolbar');

            if ($canDo->get('core.create'))
            {
                $toolbar->addNew('item.add');
            }

            $dropdown = $toolbar->dropdownButton('status-group')
                ->text('JTOOLBAR_CHANGE_STATUS')
                ->toggleSplit(false)
                ->icon('fas fa-ellipsis-h')
                ->buttonClass('btn btn-action')
                ->listCheck(true);

            $childBar = $dropdown->getChildToolbar();
            
            if ($canDo->get('core.edit.state'))
            {
                $childBar->publish('items.publish')->listCheck(true);
                $childBar->unpublish('items.unpublish')->listCheck(true);
                $childBar->standardButton('copy')->text('JTOOLBAR_DUPLICATE')->task('items.duplicate')->listCheck(true);
                $childBar->trash('items.trash')->listCheck(true);
            }

            if ($this->state->get('filter.state') == -2)
            {
                $toolbar->delete('items.delete')
                    ->text('JTOOLBAR_EMPTY_TRASH')
                    ->message('JGLOBAL_CONFIRM_DELETE')
                    ->listCheck(true);
            }

            if ($canDo->get('core.admin'))
            {
                $toolbar->preferences('com_gsd');
            }

            $toolbar->help('JHELP', false, 'http://www.tassos.gr/joomla-extensions/google-structured-data-markup/docs');

            return;
        }

        if ($canDo->get('core.create'))
        {
            ToolbarHelper::addNew('item.add');
        }
        
        if ($canDo->get('core.edit'))
        {
            ToolbarHelper::editList('item.edit');
        }

        if ($canDo->get('core.create'))
        {
            ToolbarHelper::custom('items.duplicate', 'copy', 'copy', 'JTOOLBAR_DUPLICATE', true);
        }

        if ($canDo->get('core.edit.state') && $state->get('filter.state') != 2)
        {
            ToolbarHelper::publish('items.publish', 'JTOOLBAR_PUBLISH', true);
            ToolbarHelper::unpublish('items.unpublish', 'JTOOLBAR_UNPUBLISH', true);
        }

        if ($canDo->get('core.delete') && $state->get('filter.state') == -2)
        {
            ToolbarHelper::deleteList('', 'items.delete', 'JTOOLBAR_EMPTY_TRASH');
        }
        else if ($canDo->get('core.edit.state'))
        {
            ToolbarHelper::trash('items.trash');
        }

        if ($canDo->get('core.admin'))
        {
            ToolbarHelper::preferences('com_gsd');
        }

        ToolbarHelper::help("Help", false, "https://www.tassos.gr/joomla-extensions/google-structured-data-markup/docs");
    }
}access.xml000064400000001460152345574300006540 0ustar00<?xml version="1.0" encoding="utf-8" ?>
<access component="com_gsd">
	<section name="component">
		<action name="core.admin" title="JACTION_ADMIN" description="JACTION_ADMIN_COMPONENT_DESC" />
		<action name="core.manage" title="JACTION_MANAGE" description="JACTION_MANAGE_COMPONENT_DESC" />
		<action name="core.create" title="JACTION_CREATE" description="JACTION_CREATE_COMPONENT_DESC" />
		<action name="core.delete" title="JACTION_DELETE" description="JACTION_DELETE_COMPONENT_DESC" />
		<action name="core.edit" title="JACTION_EDIT" description="JACTION_EDIT_COMPONENT_DESC" />
		<action name="core.edit.state" title="JACTION_EDITSTATE" description="JACTION_EDITSTATE_COMPONENT_DESC" />
		<action name="core.edit.own" title="JACTION_EDITOWN" description="JACTION_EDITOWN_COMPONENT_DESC" />
	</section>
</access>autoload.php000064400000001014152345574300007071 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

// Register Convert Form namespace
JLoader::registerNamespace('GSD', JPATH_ADMINISTRATOR . '/components/com_gsd/GSD', false, false, 'psr4');config.xml000064400000000530152345574300006541 0ustar00<?xml version="1.0" encoding="utf-8"?>
<config>
	<fieldset name="permissions" label="JCONFIG_PERMISSIONS_LABEL" description="JCONFIG_PERMISSIONS_DESC" >
		<field name="rules" type="rules"
			label="JCONFIG_PERMISSIONS_LABEL"
			filter="rules"
			validate="rules"
			component="com_gsd"
			section="component" />
	</fieldset>
</config>controller.php000064400000001057152345574300007453 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\MVC\Controller\BaseController;

/**
 * Google Structured Data master display controller.
 */
class GSDController extends BaseController
{
}gsd.php000064400000004110152345574300006036 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\MVC\Controller\BaseController;

// Load Framework
if (!@include_once(JPATH_PLUGINS . '/system/nrframework/autoload.php'))
{
	throw new RuntimeException('Tassos Framework is not installed', 500);
}

$app = Factory::getApplication();

// Access check.
if (!Factory::getUser()->authorise('core.manage', 'com_gsd'))
{
	$app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error');
	return;
}

use NRFramework\Functions;
use NRFramework\Extension;

// Load framework's and component's language files
Functions::loadLanguage();
Functions::loadLanguage('plg_system_gsd');	

// Initialize component's library
require_once JPATH_ADMINISTRATOR . '/components/com_gsd/autoload.php';

// Check required extensions
if (!Extension::pluginIsEnabled('nrframework'))
{
	$app->enqueueMessage(Text::sprintf('NR_EXTENSION_REQUIRED', Text::_('GSD'), Text::_('PLG_SYSTEM_NRFRAMEWORK')), 'error');
}

if (!Extension::pluginIsEnabled('gsd'))
{
	$app->enqueueMessage(Text::sprintf('NR_EXTENSION_REQUIRED', Text::_('GSD'), Text::_('PLG_SYSTEM_GSD')), 'error');
}

if (defined('nrJ4'))
{
	HTMLHelper::stylesheet('plg_system_nrframework/joomla4.css', ['relative' => true, 'version' => 'auto']);
} else 
{
	HTMLHelper::stylesheet('plg_system_nrframework/joomla3.css', ['relative' => true, 'version' => 'auto']);
	HTMLHelper::stylesheet('com_gsd/joomla3.css', ['relative' => true, 'version' => 'auto']);
}

GSD\Helper::event('onGSDGetNames');

// Perform the Request task
$controller = BaseController::getInstance('GSD');
$controller->execute($app->input->get('task'));
$controller->redirect();gsd.xml000064400000004214152345574300006054 0ustar00<?xml version="1.0" encoding="utf-8"?>
<extension version="3.4" type="component" method="upgrade">
    <name>GSD</name>
    <description>GSD_DESC</description>
    <version>1.0</version>
    <creationDate>May 2017</creationDate>
    <author>Tassos Marinos</author>
    <authorEmail>info@tassos.gr</authorEmail>
    <authorUrl>http://www.tassos.gr</authorUrl>
    <copyright>Copyright © 2021 Tassos Marinos All Rights Reserved</copyright>
    <license>http://www.gnu.org/licenses/gpl-3.0.html GNU/GPL</license>
    <scriptfile>script.install.php</scriptfile>
    <install>
        <sql>
            <file driver="mysql" charset="utf8">sql/gsd.sql</file>
        </sql>
    </install>
    <uninstall>
        <sql>
            <file driver="mysql" charset="utf8">sql/uninstall.gsd.sql</file>
        </sql>
    </uninstall>
    <update>
        <schemas>
            <schemapath type="mysql">sql/updates/mysql</schemapath>
        </schemas>
    </update>
    <administration>
        <menu img="class:gsd">COM_GSD</menu>
        <submenu>
            <menu link="option=com_gsd">GSD_DASHBOARD</menu>
            <menu link="option=com_gsd&amp;view=items" view="items">GSD_ITEMS</menu>
            <menu link="option=com_gsd&amp;view=config" view="config">GSD_CONFIG</menu>
        </submenu>
        <files>
            <folder>controllers</folder>
            <folder>GSD</folder>
            <folder>helpers</folder>
            <folder>language</folder>
            <folder>layouts</folder>
            <folder>models</folder>
            <folder>sql</folder>
            <folder>tables</folder>
            <folder>views</folder>
            <filename>access.xml</filename>
            <filename>config.xml</filename>
            <filename>controller.php</filename>
            <filename>gsd.php</filename>
            <filename>script.install.helper.php</filename>
            <filename>version.php</filename>
            <filename>autoload.php</filename>
        </files>
    </administration>
    <media folder="media" destination="com_gsd">
        <folder>css</folder>
        <folder>js</folder>
    </media>
</extension>script.install.helper.php000064400000036516152345574300011527 0ustar00<?php
/**
 * Installer Script Helper
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2016 Tassos Marinos All Rights Reserved
 * @license         http://www.gnu.org/licenses/gpl-3.0.html GNU/GPL
 */

defined('_JEXEC') or die;

use Joomla\CMS\Installer\Installer;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\Filesystem\File;
use Joomla\Filesystem\Folder;

class Com_GsdInstallerScriptHelper
{
	public $name = '';
	public $alias = '';
	public $extname = '';
	public $extension_type = '';
	public $plugin_folder = 'system';
	public $module_position = 'status';
	public $client_id = 1;
	public $install_type = 'install';
	public $show_message = true;
	public $autopublish = true;
	public $db = null;
	public $app = null;
	public $installedVersion;

	public function __construct(&$params)
	{
		$this->extname = $this->extname ?: $this->alias;
		$this->db = Factory::getDbo();
		$this->app = Factory::getApplication();
		$this->installedVersion = $this->getVersion($this->getInstalledXMLFile());
	}

	/**
	 *  Preflight event
	 *
	 *  @param   string            
	 *  @param   JAdapterInstance
	 *
	 *  @return  boolean                      
	 */
	public function preflight($route, $adapter)
	{
		if (!in_array($route, array('install', 'update')))
		{
			return;
		}

		Factory::getLanguage()->load('plg_system_novaraininstaller', JPATH_PLUGINS . '/system/novaraininstaller');

		if ($this->show_message && $this->isInstalled())
		{
			$this->install_type = 'update';
		}

		if ($this->onBeforeInstall() === false)
		{
			return false;
		}
	}

	/**
	 *  Preflight event
	 *
	 *  @param   string            
	 *  @param   JAdapterInstance
	 *
	 *  @return  boolean                      
	 */
	public function postflight($route, $adapter)
	{
		Factory::getLanguage()->load($this->getPrefix() . '_' . $this->extname, $this->getMainFolder());

		if (!in_array($route, array('install', 'update')))
		{
			return;
		}

		if ($this->onAfterInstall() === false)
		{
			return false;
		}

		if ($route == 'install' && $this->autopublish)
		{
			$this->publishExtension();
		}

		if ($this->show_message)
		{
			$this->addInstalledMessage();
		}

		Factory::getCache()->clean('com_plugins');
		Factory::getCache()->clean('_system');
	}

	public function isInstalled()
	{
		if (!is_file($this->getInstalledXMLFile()))
		{
			return false;
		}

		$query = $this->db->getQuery(true)
			->select('extension_id')
			->from('#__extensions')
			->where($this->db->quoteName('type') . ' = ' . $this->db->quote($this->extension_type))
			->where($this->db->quoteName('element') . ' = ' . $this->db->quote($this->getElementName()));
		$this->db->setQuery($query, 0, 1);
		$result = $this->db->loadResult();

		return empty($result) ? false : true;
	}

	public function getMainFolder()
	{
		switch ($this->extension_type)
		{
			case 'plugin' :
				return JPATH_SITE . '/plugins/' . $this->plugin_folder . '/' . $this->extname;

			case 'component' :
				return JPATH_ADMINISTRATOR . '/components/com_' . $this->extname;

			case 'module' :
				return JPATH_ADMINISTRATOR . '/modules/mod_' . $this->extname;

			case 'library' :
				return JPATH_SITE . '/libraries/' . $this->extname;
		}
	}

	public function getInstalledXMLFile()
	{
		return $this->getXMLFile($this->getMainFolder());
	}

	public function getCurrentXMLFile()
	{
		return $this->getXMLFile(__DIR__);
	}

	public function getXMLFile($folder)
	{
		switch ($this->extension_type)
		{
			case 'module' :
				return $folder . '/mod_' . $this->extname . '.xml';
			default :
				return $folder . '/' . $this->extname . '.xml';
		}
	}

	public function foldersExist($folders = array())
	{
		foreach ($folders as $folder)
		{
			if (is_dir($folder))
			{
				return true;
			}
		}

		return false;
	}

	public function publishExtension()
	{
		switch ($this->extension_type)
		{
			case 'plugin' :
				$this->publishPlugin();

			case 'module' :
				$this->publishModule();
		}
	}

	public function publishPlugin()
	{
		$query = $this->db->getQuery(true)
			->update('#__extensions')
			->set($this->db->quoteName('enabled') . ' = 1')
			->where($this->db->quoteName('type') . ' = ' . $this->db->quote('plugin'))
			->where($this->db->quoteName('element') . ' = ' . $this->db->quote($this->extname))
			->where($this->db->quoteName('folder') . ' = ' . $this->db->quote($this->plugin_folder));
		$this->db->setQuery($query);
		$this->db->execute();
	}

	public function publishModule()
	{
		// Get module id
		$query = $this->db->getQuery(true)
			->select('id')
			->from('#__modules')
			->where($this->db->quoteName('module') . ' = ' . $this->db->quote('mod_' . $this->extname))
			->where($this->db->quoteName('client_id') . ' = ' . (int) $this->client_id);
		$this->db->setQuery($query, 0, 1);
		$id = $this->db->loadResult();

		if (!$id)
		{
			return;
		}

		// check if module is already in the modules_menu table (meaning is is already saved)
		$query->clear()
			->select('moduleid')
			->from('#__modules_menu')
			->where($this->db->quoteName('moduleid') . ' = ' . (int) $id);
		$this->db->setQuery($query, 0, 1);
		$exists = $this->db->loadResult();

		if ($exists)
		{
			return;
		}

		// Get highest ordering number in position
		$query->clear()
			->select('ordering')
			->from('#__modules')
			->where($this->db->quoteName('position') . ' = ' . $this->db->quote($this->module_position))
			->where($this->db->quoteName('client_id') . ' = ' . (int) $this->client_id)
			->order('ordering DESC');
		$this->db->setQuery($query, 0, 1);
		$ordering = $this->db->loadResult();
		$ordering++;

		// publish module and set ordering number
		$query->clear()
			->update('#__modules')
			->set($this->db->quoteName('published') . ' = 1')
			->set($this->db->quoteName('ordering') . ' = ' . (int) $ordering)
			->set($this->db->quoteName('position') . ' = ' . $this->db->quote($this->module_position))
			->where($this->db->quoteName('id') . ' = ' . (int) $id);
		$this->db->setQuery($query);
		$this->db->execute();

		// add module to the modules_menu table
		$query->clear()
			->insert('#__modules_menu')
			->columns(array($this->db->quoteName('moduleid'), $this->db->quoteName('menuid')))
			->values((int) $id . ', 0');
		$this->db->setQuery($query);
		$this->db->execute();
	}

	public function addInstalledMessage()
	{
		Factory::getApplication()->enqueueMessage(
			Text::sprintf(
				Text::_($this->install_type == 'update' ? 'NRI_THE_EXTENSION_HAS_BEEN_UPDATED_SUCCESSFULLY' : 'NRI_THE_EXTENSION_HAS_BEEN_INSTALLED_SUCCESSFULLY'),
				'<strong>' . Text::_($this->name) . '</strong>',
				'<strong>' . $this->getVersion() . '</strong>',
				$this->getFullType()
			)
		);
	}

	public function getPrefix()
	{
		switch ($this->extension_type)
		{
			case 'plugin';
				return Text::_('plg_' . strtolower($this->plugin_folder));

			case 'component':
				return Text::_('com');

			case 'module':
				return Text::_('mod');

			case 'library':
				return Text::_('lib');

			default:
				return $this->extension_type;
		}
	}

	public function getElementName($type = null, $extname = null)
	{
		$type = is_null($type) ? $this->extension_type : $type;
		$extname = is_null($extname) ? $this->extname : $extname;

		switch ($type)
		{
			case 'component' :
				return 'com_' . $extname;

			case 'module' :
				return 'mod_' . $extname;

			case 'plugin' :
			default:
				return $extname;
		}
	}

	public function getFullType()
	{
		return Text::_('NRI_' . strtoupper($this->getPrefix()));
	}

	public function isPro()
	{
		$versionFile = __DIR__ . "/version.php";

		// If version file does not exist we assume a PRO version
		if (!is_file($versionFile))
		{
			return true;
		}

		// Load version file
		require_once $versionFile;
		return (bool) $NR_PRO;
	}

	public function getVersion($file = '')
	{
		$file = $file ?: $this->getCurrentXMLFile();

		if (!is_file($file))
		{
			return '';
		}

		$xml = Installer::parseXMLInstallFile($file);

		if (!$xml || !isset($xml['version']))
		{
			return '';
		}

		return $xml['version'];
	}

	/**
	 *  Checks wether the extension can be installed or not
	 *
	 *  @return  boolean  
	 */
	public function canInstall()
	{
		// The extension is not installed yet. Accept Install.
		if (!$installed_version = $this->getVersion($this->getInstalledXMLFile()))
		{
			return true;
		}

		// Path to extension's version file
		$versionFile = $this->getMainFolder() . "/version.php";
		$NR_PRO = true;

		// If version file does not exist we assume we have a PRO version installed
		if (file_exists($versionFile))
		{
			require_once($versionFile);
		}

		// The free version is installed. Accept install.
		if (!(bool)$NR_PRO)
		{
			return true;
		}

		// Current package is a PRO version. Accept install.
		if ($this->isPro())
		{
			return true;
		}

		// User is trying to update from PRO version to FREE. Do not accept install.
		Factory::getLanguage()->load($this->getPrefix() . '_' . $this->extname, __DIR__);

		Factory::getApplication()->enqueueMessage(
			Text::_('NRI_ERROR_PRO_TO_FREE'), 'error'
		);

		Factory::getApplication()->enqueueMessage(
			html_entity_decode(
				Text::sprintf(
					'NRI_ERROR_UNINSTALL_FIRST',
					'<a href="http://www.tassos.gr/joomla-extensions/' . $this->getUrlAlias() . '" target="_blank">',
					'</a>',
					Text::_($this->name)
				)
			), 'error'
		);	

		return false;	
	}

	/**
	 * Returns the URL alias of the extension.
	 * 
	 * @return  string
	 */
	private function getUrlAlias()
	{
		$alias = $this->alias;

		switch ($alias)
		{
			case 'smilepack':
				$alias = 'smile-pack';
				break;
			case 'convertforms':
				$alias = 'convert-forms';
				break;
			case 'rstbox':
				$alias = 'engagebox';
				break;
			case 'gsd':
				$alias = 'google-structured-data';
				break;
		}

		// ACF
		if ($this->plugin_folder === 'fields' && ($alias === 'acf' || $this->startsWith($alias, 'acf')))
		{
			$alias = 'advanced-custom-fields';
		}
		
		return $alias;
	}

    /**
     * Checks whether string starts with substring.
     * 
     * @param   string  $string
     * @param   string  $query
     * 
     * @return  bool
     */
    public static function startsWith($string, $query)
    {
        return substr($string, 0, strlen($query)) === $query;
    }

	/**
	 *  Checks if current version is newer than the installed one
	 *  Used for Novarain Framework
	 *
	 *  @return  boolean  [description]
	 */
	public function isNewer()
	{
		if (!$installed_version = $this->getVersion($this->getInstalledXMLFile()))
		{
			return true;
		}

		$package_version = $this->getVersion();

		return version_compare($installed_version, $package_version, '<=');
	}

	/**
	 *  Helper method triggered before installation
	 *
	 *  @return  bool
	 */
	public function onBeforeInstall()
	{
		if (!$this->canInstall())
		{
			return false;
		}
	}

	/**
	 *  Helper method triggered after installation
	 */
	public function onAfterInstall()
	{

	}

	/**
	 *  Delete files
	 *
	 *  @param   array   $folders
	 */
	public function deleteFiles($files = array())
	{
	    foreach ($files as $key => $file)
        {
			if (!is_file($file))
			{
				continue;
			}
			
            File::delete($file);
        }
	}

	/**
	 *  Deletes folders
	 *
	 *  @param   array   $folders  
	 */
	public function deleteFolders($folders = array())
	{
		foreach ($folders as $folder)
		{
			if (!is_dir($folder))
			{
				continue;
			}

			Folder::delete($folder);
		}
	}

	public function dropIndex($table, $index)
	{	
		$db = $this->db;

		// Check if index exists first
		$query = 'SHOW INDEX FROM ' . $db->quoteName('#__' . $table) . ' WHERE KEY_NAME = ' . $db->quote($index);
        $db->setQuery($query);
        $db->execute();

        if (!$db->loadResult())
        {
        	return;
        }

        // Remove index
        $query = 'ALTER TABLE ' . $db->quoteName('#__' . $table) . ' DROP INDEX ' . $db->quoteName($index);
        $db->setQuery($query);
        $db->execute(); 
	}

    public function dropUnwantedTables($tables) {

        if (!$tables) {
            return;
        }

        foreach ($tables as $table) {
            $query = "DROP TABLE IF EXISTS #__".$this->db->escape($table);
            $this->db->setQuery($query);
            $this->db->execute();
        }
    }

	public function dropUnwantedColumns($table, $columns) {

        if (!$columns || !$table) {
            return;
        }

        $db = $this->db;

        // Check if columns exists in database
        function qt($n) {
            return(Factory::getDBO()->quote($n));
        }
        
        $query = 'SHOW COLUMNS FROM #__'.$table.' WHERE Field IN ('.implode(",", array_map("qt", $columns)).')';
        $db->setQuery($query);
        $rows = $db->loadColumn(0);

        // Abort if we don't have any rows
        if (!$rows) {
            return;
        }

        // Let's remove the columns
        $q = "";
        foreach ($rows as $key => $column) {
            $comma = (($key+1) < count($rows)) ? "," : "";
            $q .= "drop ".$this->db->escape($column).$comma;
        }

        $query = "alter table #__".$table." $q";

        $db->setQuery($query);
        $db->execute();
    }

  	public function fetch($table, $columns = "*", $where = null, $singlerow = false) {
        if (!$table) {
            return;
        }

        $db = $this->db;
        $query = $db->getQuery(true);

        $query
            ->select($columns)
            ->from("#__$table");
        
        if (isset($where)) {
            $query->where("$where");
        }
        
        $db->setQuery($query);
 
        return ($singlerow) ? $db->loadObject() : $db->loadObjectList();
    }

    /**
     *  Load the Novarain Framework
     *
     *  @return  boolean
     */
	public function loadFramework()
	{
		if (is_file(JPATH_PLUGINS . '/system/nrframework/autoload.php'))
		{
			include_once JPATH_PLUGINS . '/system/nrframework/autoload.php';
		}
	}

	/**
	 *  Re-orders plugin after passed array of plugins
	 *
	 *  @param   string  $plugin            Plugin element name
	 *  @param   array   $lowerPluginOrder  Array of plugin element names
	 *
	 *  @return  boolean
	 */
	public function pluginOrderAfter($lowerPluginOrder)
    {

        if (!is_array($lowerPluginOrder) || !count($lowerPluginOrder))
        {
            return;
        }
        
        $db = $this->db;

        // Get plugins max order
        $query = $db->getQuery(true);
        $query
            ->select($db->quoteName('b.ordering'))
            ->from($db->quoteName('#__extensions', 'b'))
            ->where($db->quoteName('b.element') . ' IN ("'.implode("\",\"",$lowerPluginOrder).'")')
            ->order('b.ordering desc');

        $db->setQuery($query);
        $maxOrder = $db->loadResult();

        if (is_null($maxOrder))
        {
            return;
        }

        // Get plugin details
        $query
            ->clear()
            ->select(array($db->quoteName('extension_id'), $db->quoteName('ordering')))
            ->from($db->quoteName('#__extensions'))
            ->where($db->quoteName('element') . ' = ' . $db->quote($this->alias));

        $db->setQuery($query);
        $pluginInfo = $db->loadObject();

        if (!isset($pluginInfo->ordering) || $pluginInfo->ordering > $maxOrder)
        {
            return;
        }

        // Update the new plugin order
        $object = new stdClass();
        $object->extension_id = $pluginInfo->extension_id;
        $object->ordering = ($maxOrder + 1);

		try {
			$db->updateObject('#__extensions', $object, 'extension_id');
		} catch (Exception $e) {
			return $e->getMessage();
		}
    }
}
script.install.php000064400000015062152345574300010242 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 * 
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

defined('_JEXEC') or die;

use Joomla\CMS\Table\Table;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;

require_once __DIR__ . '/script.install.helper.php';

/**
 *  Google Structured Data component installer
 */
class Com_GSDInstallerScript extends Com_GSDInstallerScriptHelper
{
	public $name = 'GSD';
	public $alias = 'gsd';
	public $extension_type = 'component';

	/**
	 *  Runs after current extension installation
	 *
	 *  @return  void
	 */
	public function onAfterInstall()
	{
		// Only on update
		if ($this->install_type == 'install')
		{
			return true;
		}

		$this->loadFramework();
		$installedVersion = \NRFramework\Functions::getExtensionVersion('plg_system_gsd');

		// Let's migrate old versions to 3.0.0 first
		if (version_compare($installedVersion, '3.0.0', '<='))
		{
			require_once $this->getMainFolder() . '/helpers/migrator.php';
			$migrator = new GSDMigrator();
			$migrator->start();
		}

        // Since v3.1.3 the saving policy of Custom Code has been changed
        if (version_compare($installedVersion, '3.1.3', '<='))
        {
            $this->migrateCustomCode();
        }

        if (version_compare($installedVersion, '3.1.10', '<'))
        {
            $this->mergeEventDateAndTimeFields();
        }

        // 4.0.0 Migrator
        if (version_compare($installedVersion, '4.0.0', '<='))
        {
			require_once $this->getMainFolder() . '/helpers/migrator4.php';
			$migrator = new GSDMigrator4($installedVersion);
			$migrator->start();
        }

        // Migrate to the new Remove Structured Data option
        if (version_compare($installedVersion, '4.1.0', '<'))
        {
			$this->updateRemoveMicrodataValue();
        }

        require_once JPATH_ADMINISTRATOR . '/components/com_gsd/autoload.php';
        $mgrt = new GSD\Migrator($installedVersion);
        $mgrt->run();
    }

    private function updateRemoveMicrodataValue()
    {
        Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_gsd/tables');
		
        $table = Table::getInstance('Config', 'GSDTable');
        $table->load('config');
        
        $params = json_decode($table->params);

        if (!isset($params->removemicrodata) || empty($params->removemicrodata))
        {
            return;
        }
        
        $current_schemas = $params->removemicrodata;
        $new_schemas = [];
        
        foreach ($current_schemas as $key => $schema)
        {
            if (!is_string($schema))
            {
                continue;
            }

            $new_schemas[$key] = (object) [
                'name'    => $schema,
                'enabled' => true
            ];
        }

        if (!$new_schemas)
        {
            return;
        }
        
        $params->removemicrodata = $new_schemas;
        
        $table->params = json_encode($params);
        $table->store();
    }

    /**
     * Merges separate Date and Time fields into a single Calendar field
     *
     * @return void
     */
    private function mergeEventDateAndTimeFields()
    {
        $items = $this->getItems();
    
        foreach ($items as $key => $item)
        { 
            if ($item->contenttype != 'event')
            {
                continue;
            }
    
            // If startTime is missing the item is already migrated
            if (!isset($item->event->startTime))
            {
                continue;
            }
    
            // OK. The item needs migration
            $table = $this->loadItem($item->id);
    
            $p = json_decode($table->params);
    
            $p->event->startDate .= ' ' . $p->event->startTime;
            $p->event->offerStartDate .= ' ' . $p->event->offerStartTime;
            $p->event->endDate .= ' ' . $p->event->endTime;
    
            unset($p->event->startTime);
            unset($p->event->endTime);
            unset($p->event->offerStartTime);
    
            $table->params = json_encode($p);
            $table->store();
        }
    }

    /**
     *	Since v3.1.3 the saving policy of Custom Code has been changed which is now being saved independently
     *	This method helps up, transition to new requirements. 
     *
     *  @return  void
     */
	private function migrateCustomCode()
    {
        $items = $this->getItems();

        foreach ($items as $key => $item)
        {   
         	if ($item->contenttype == 'custom_code')
            {
                continue;
            }

            if (!isset($item->customcode) || empty($item->customcode) || is_null($item->customcode))
            {
                continue;
            }

            $data = array(
                'thing'       => $item->thing,
                'plugin'      => $item->plugin,
                'params'      => json_encode(array(
                    'contenttype' => 'custom_code',
                    'customcode'  => $item->customcode
                )),
                'state'       => $item->state,
                'note'        => $item->note,
                'colorgroup'  => $item->colorgroup
            );

            Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_gsd/tables');

            // Save new row
            $model = BaseDatabaseModel::getInstance('Item', 'GSDModel');
            $data  = $model->validate(null, $data);
            $model->save($data);

            // Remove Custom Code from old row
            $table = Table::getInstance('Item', 'GSDTable');
            $table->load($item->id);

            $p = json_decode($table->params);
            unset($p->customcode);

            $table->params = json_encode($p);
            $table->store();
        }
    }

    public function getItems($table = 'Items')
    {
        $hash = 'get' . $table;

        if (NRFramework\Cache::has($hash))
        {
            return NRFramework\Cache::get($hash);
        }

        BaseDatabaseModel::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_gsd/models');
        
        if (!$model = BaseDatabaseModel::getInstance($table, 'GSDModel'))
        {
            return;
        }

        return NRFramework\Cache::set($hash, $model->getItems());
    }

    private function loadItem($id, $table = 'Item')
    {
        if (!$table = Table::getInstance($table, 'GSDTable'))
        {
            return;
        }

        $table->load($id);

        return $table;
    }
}

 version.php000064400000000640152345574300006752 0ustar00<?php

/**
 * @package         Google Structured Data
 * @version         5.6.5 Pro
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2021 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

defined('_JEXEC') or die('Restricted Access');
$NR_PRO = "1";

?>