| Current Path : /proc/1908984/root/proc/2603263/cwd/ |
| Current File : //proc/1908984/root/proc/2603263/cwd/com_gsd.tar |
css/fastedit.css 0000644 00000002120 15234434601 0007646 0 ustar 00 .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.css 0000644 00000003650 15234434601 0007420 0 ustar 00 .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).css 0000644 00000000046 15234434601 0010471 0 ustar 00 .nr-main-container{background:none}
css/joomla4.css 0000644 00000000046 15234434601 0007415 0 ustar 00 .nr-main-container{background:none}
css/openinghours.css 0000644 00000003137 15234434601 0010574 0 ustar 00 .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.css 0000644 00000144435 15234434601 0007406 0 ustar 00 @-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.js 0000644 00000001047 15234434601 0006772 0 ustar 00 document.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.js 0000644 00000003506 15234434601 0007327 0 ustar 00 var 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.js 0000644 00000001625 15234434601 0010244 0 ustar 00 document.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.js 0000644 00000000222 15234434601 0007020 0 ustar 00 jQuery(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.php 0000644 00000001554 15234557430 0010664 0 ustar 00 <?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.php 0000644 00000001631 15234557430 0012234 0 ustar 00 <?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.php 0000644 00000000706 15234557430 0011545 0 ustar 00 <?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.php 0000644 00000004401 15234557430 0012107 0 ustar 00 <?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.php 0000644 00000004240 15234557430 0013054 0 ustar 00 <?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.php 0000644 00000004365 15234557430 0011741 0 ustar 00 <?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.php 0000644 00000012335 15234557430 0011263 0 ustar 00 <?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.php 0000644 00000004375 15234557430 0012474 0 ustar 00 <?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.php 0000644 00000005607 15234557430 0011720 0 ustar 00 <?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.php 0000644 00000003620 15234557430 0012727 0 ustar 00 <?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.php 0000644 00000001636 15234557430 0013424 0 ustar 00 <?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.php 0000644 00000003105 15234557430 0011726 0 ustar 00 <?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.php 0000644 00000000711 15234557430 0012115 0 ustar 00 <?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.php 0000644 00000004067 15234557430 0012277 0 ustar 00 <?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.php 0000644 00000002375 15234557430 0012066 0 ustar 00 <?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.php 0000644 00000007050 15234557430 0012113 0 ustar 00 <?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.php 0000644 00000003641 15234557430 0012254 0 ustar 00 <?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.php 0000644 00000002664 15234557430 0011726 0 ustar 00 <?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.php 0000644 00000024272 15234557430 0010146 0 ustar 00 <?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.php 0000644 00000002743 15234557430 0010512 0 ustar 00 <?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.php 0000644 00000002324 15234557430 0006606 0 ustar 00 <?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.php 0000644 00000036301 15234557430 0007124 0 ustar 00 <?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.php 0000644 00000146620 15234557430 0006624 0 ustar 00 <?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.php 0000644 00000014020 15234557430 0010646 0 ustar 00 <?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.php 0000644 00000022773 15234557430 0007501 0 ustar 00 <?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.php 0000644 00000035120 15234557430 0007734 0 ustar 00 <?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.php 0000644 00000002113 15234557430 0011234 0 ustar 00 <?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.php 0000644 00000005467 15234557430 0010751 0 ustar 00 <?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.php 0000644 00000003606 15234557430 0011301 0 ustar 00 <?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.php 0000644 00000013410 15234557430 0010373 0 ustar 00 <?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.php 0000644 00000001050 15234557430 0011074 0 ustar 00 <?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.php 0000644 00000001044 15234557430 0010570 0 ustar 00 <?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.php 0000644 00000003244 15234557430 0010757 0 ustar 00 <?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.php 0000644 00000007165 15234557430 0010667 0 ustar 00 <?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.php 0000644 00000027507 15234557430 0010566 0 ustar 00 <?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.php 0000644 00000035462 15234557430 0010651 0 ustar 00 <?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.ini 0000644 00000000735 15234557430 0012237 0 ustar 00 ;; 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"