Overview

Classes

  • Gravity_Flow_API
  • Gravity_Flow_Step
  • Gravity_Flow_Step_Feed_Add_On
  • Overview
  • Class
   1:    2:    3:    4:    5:    6:    7:    8:    9:   10:   11:   12:   13:   14:   15:   16:   17:   18:   19:   20:   21:   22:   23:   24:   25:   26:   27:   28:   29:   30:   31:   32:   33:   34:   35:   36:   37:   38:   39:   40:   41:   42:   43:   44:   45:   46:   47:   48:   49:   50:   51:   52:   53:   54:   55:   56:   57:   58:   59:   60:   61:   62:   63:   64:   65:   66:   67:   68:   69:   70:   71:   72:   73:   74:   75:   76:   77:   78:   79:   80:   81:   82:   83:   84:   85:   86:   87:   88:   89:   90:   91:   92:   93:   94:   95:   96:   97:   98:   99:  100:  101:  102:  103:  104:  105:  106:  107:  108:  109:  110:  111:  112:  113:  114:  115:  116:  117:  118:  119:  120:  121:  122:  123:  124:  125:  126:  127:  128:  129:  130:  131:  132:  133:  134:  135:  136:  137:  138:  139:  140:  141:  142:  143:  144:  145:  146:  147:  148:  149:  150:  151:  152:  153:  154:  155:  156:  157:  158:  159:  160:  161:  162:  163:  164:  165:  166:  167:  168:  169:  170:  171:  172:  173:  174:  175:  176:  177:  178:  179:  180:  181:  182:  183:  184:  185:  186:  187:  188:  189:  190:  191:  192:  193:  194:  195:  196:  197:  198:  199:  200:  201:  202:  203:  204:  205:  206:  207:  208:  209:  210:  211:  212:  213:  214:  215:  216:  217:  218:  219:  220:  221:  222:  223:  224:  225:  226:  227:  228:  229:  230:  231:  232:  233:  234:  235:  236:  237:  238:  239:  240:  241:  242:  243:  244:  245:  246:  247:  248:  249:  250:  251:  252:  253:  254:  255:  256:  257:  258:  259:  260:  261:  262:  263:  264:  265:  266:  267:  268:  269:  270:  271:  272:  273:  274:  275:  276:  277:  278:  279:  280:  281:  282:  283:  284:  285:  286:  287:  288:  289:  290:  291:  292:  293:  294:  295:  296:  297:  298:  299:  300:  301:  302:  303:  304:  305:  306:  307:  308:  309:  310:  311:  312:  313:  314:  315:  316:  317:  318:  319:  320:  321:  322:  323:  324:  325:  326:  327:  328:  329:  330:  331:  332:  333:  334:  335:  336:  337:  338:  339:  340:  341:  342:  343:  344:  345:  346:  347:  348:  349:  350:  351:  352:  353:  354:  355:  356:  357:  358:  359:  360:  361:  362:  363:  364:  365:  366:  367:  368:  369:  370:  371:  372:  373:  374:  375:  376:  377:  378:  379:  380:  381:  382:  383:  384:  385:  386:  387:  388:  389:  390:  391:  392:  393:  394:  395:  396:  397:  398:  399:  400:  401:  402:  403:  404:  405:  406:  407:  408:  409:  410:  411:  412:  413:  414:  415:  416:  417:  418:  419:  420:  421:  422:  423:  424:  425:  426:  427:  428:  429:  430:  431:  432:  433:  434:  435:  436:  437:  438:  439:  440:  441:  442:  443:  444:  445:  446:  447:  448:  449:  450:  451:  452:  453:  454:  455:  456:  457:  458:  459:  460:  461:  462:  463:  464:  465:  466:  467:  468:  469:  470:  471:  472:  473:  474:  475:  476:  477:  478:  479:  480:  481:  482:  483:  484:  485:  486:  487:  488:  489:  490:  491:  492:  493:  494:  495:  496:  497:  498:  499:  500:  501:  502:  503:  504:  505:  506:  507:  508:  509:  510:  511:  512:  513:  514:  515:  516:  517:  518:  519:  520:  521:  522:  523:  524:  525:  526:  527:  528:  529:  530:  531:  532:  533:  534:  535:  536:  537:  538:  539:  540:  541:  542:  543:  544:  545:  546:  547:  548:  549:  550:  551:  552:  553:  554:  555:  556:  557:  558:  559:  560:  561:  562:  563:  564:  565:  566:  567:  568:  569:  570:  571:  572:  573:  574:  575:  576:  577:  578:  579:  580:  581:  582:  583:  584:  585:  586:  587:  588:  589:  590:  591:  592:  593:  594:  595:  596:  597:  598:  599:  600:  601:  602:  603:  604:  605:  606:  607:  608:  609:  610:  611:  612:  613:  614:  615:  616:  617:  618:  619:  620:  621:  622:  623:  624:  625:  626:  627:  628:  629:  630:  631:  632:  633:  634:  635:  636:  637:  638:  639:  640:  641:  642:  643:  644:  645:  646:  647:  648:  649:  650:  651:  652:  653:  654:  655:  656:  657:  658:  659:  660:  661:  662:  663:  664:  665:  666:  667:  668:  669:  670:  671:  672:  673:  674:  675:  676:  677:  678:  679:  680:  681:  682:  683:  684:  685:  686:  687:  688:  689:  690:  691:  692:  693:  694:  695:  696:  697:  698:  699:  700:  701:  702:  703:  704:  705:  706:  707:  708:  709:  710:  711:  712:  713:  714:  715:  716:  717:  718:  719:  720:  721:  722:  723:  724:  725:  726:  727:  728:  729:  730:  731:  732:  733:  734:  735:  736:  737:  738:  739:  740:  741:  742:  743:  744:  745:  746:  747:  748:  749:  750:  751:  752:  753:  754:  755:  756:  757:  758:  759:  760:  761:  762:  763:  764:  765:  766:  767:  768:  769:  770:  771:  772:  773:  774:  775:  776:  777:  778:  779:  780:  781:  782:  783:  784:  785:  786:  787:  788:  789:  790:  791:  792:  793:  794:  795:  796:  797:  798:  799:  800:  801:  802:  803:  804:  805:  806:  807:  808:  809:  810:  811:  812:  813:  814:  815:  816:  817:  818:  819:  820:  821:  822:  823:  824:  825:  826:  827:  828:  829:  830:  831:  832:  833:  834:  835:  836:  837:  838:  839:  840:  841:  842:  843:  844:  845:  846:  847:  848:  849:  850:  851:  852:  853:  854:  855:  856:  857:  858:  859:  860:  861:  862:  863:  864:  865:  866:  867:  868:  869:  870:  871:  872:  873:  874:  875:  876:  877:  878:  879:  880:  881:  882:  883:  884:  885:  886:  887:  888:  889:  890:  891:  892:  893:  894:  895:  896:  897:  898:  899:  900:  901:  902:  903:  904:  905:  906:  907:  908:  909:  910:  911:  912:  913:  914:  915:  916:  917:  918:  919:  920:  921:  922:  923:  924:  925:  926:  927:  928:  929:  930:  931:  932:  933:  934:  935:  936:  937:  938:  939:  940:  941:  942:  943:  944:  945:  946:  947:  948:  949:  950:  951:  952:  953:  954:  955:  956:  957:  958:  959:  960:  961:  962:  963:  964:  965:  966:  967:  968:  969:  970:  971:  972:  973:  974:  975:  976:  977:  978:  979:  980:  981:  982:  983:  984:  985:  986:  987:  988:  989:  990:  991:  992:  993:  994:  995:  996:  997:  998:  999: 1000: 1001: 1002: 1003: 1004: 1005: 1006: 1007: 1008: 1009: 1010: 1011: 1012: 1013: 1014: 1015: 1016: 1017: 1018: 1019: 1020: 1021: 1022: 1023: 1024: 1025: 1026: 1027: 1028: 1029: 1030: 1031: 1032: 1033: 1034: 1035: 1036: 1037: 1038: 1039: 1040: 1041: 1042: 1043: 1044: 1045: 1046: 1047: 1048: 1049: 1050: 1051: 1052: 1053: 1054: 1055: 1056: 1057: 1058: 1059: 1060: 1061: 1062: 1063: 1064: 1065: 1066: 1067: 1068: 1069: 1070: 1071: 1072: 1073: 1074: 1075: 1076: 1077: 1078: 1079: 1080: 1081: 1082: 1083: 1084: 1085: 1086: 1087: 1088: 1089: 1090: 1091: 1092: 1093: 1094: 1095: 1096: 1097: 1098: 1099: 1100: 1101: 1102: 1103: 1104: 1105: 1106: 1107: 1108: 1109: 1110: 1111: 1112: 1113: 1114: 1115: 1116: 1117: 1118: 1119: 1120: 1121: 1122: 1123: 1124: 1125: 1126: 1127: 1128: 1129: 1130: 1131: 1132: 1133: 1134: 1135: 1136: 1137: 1138: 1139: 1140: 1141: 1142: 1143: 1144: 1145: 1146: 1147: 1148: 1149: 1150: 1151: 1152: 1153: 1154: 1155: 1156: 1157: 1158: 1159: 1160: 1161: 1162: 1163: 1164: 1165: 1166: 1167: 1168: 1169: 1170: 1171: 1172: 1173: 1174: 1175: 1176: 1177: 1178: 1179: 1180: 1181: 1182: 1183: 1184: 1185: 1186: 1187: 1188: 1189: 1190: 1191: 1192: 1193: 1194: 1195: 1196: 1197: 1198: 1199: 1200: 1201: 1202: 1203: 1204: 1205: 1206: 1207: 1208: 1209: 1210: 1211: 1212: 1213: 1214: 1215: 1216: 1217: 1218: 1219: 1220: 1221: 1222: 1223: 1224: 1225: 1226: 1227: 1228: 1229: 1230: 1231: 1232: 1233: 1234: 1235: 1236: 1237: 1238: 1239: 1240: 1241: 1242: 1243: 1244: 1245: 1246: 1247: 1248: 1249: 1250: 1251: 1252: 1253: 1254: 1255: 1256: 1257: 1258: 1259: 1260: 1261: 1262: 1263: 1264: 1265: 1266: 1267: 1268: 1269: 1270: 1271: 1272: 1273: 1274: 1275: 1276: 1277: 1278: 1279: 1280: 1281: 1282: 1283: 1284: 1285: 1286: 1287: 1288: 1289: 1290: 1291: 1292: 1293: 1294: 1295: 1296: 1297: 1298: 1299: 1300: 1301: 1302: 1303: 1304: 1305: 1306: 1307: 1308: 1309: 1310: 1311: 1312: 1313: 1314: 1315: 1316: 1317: 1318: 1319: 1320: 1321: 1322: 1323: 1324: 1325: 1326: 1327: 1328: 1329: 1330: 1331: 1332: 1333: 1334: 1335: 1336: 1337: 1338: 1339: 1340: 1341: 1342: 1343: 1344: 1345: 1346: 1347: 1348: 1349: 1350: 1351: 1352: 1353: 1354: 1355: 1356: 1357: 1358: 1359: 1360: 1361: 1362: 1363: 1364: 1365: 1366: 1367: 1368: 1369: 1370: 1371: 1372: 1373: 1374: 1375: 1376: 1377: 1378: 1379: 1380: 1381: 1382: 1383: 1384: 1385: 1386: 1387: 1388: 1389: 1390: 1391: 1392: 1393: 1394: 1395: 1396: 1397: 1398: 1399: 1400: 1401: 1402: 1403: 1404: 1405: 1406: 1407: 1408: 1409: 1410: 1411: 1412: 1413: 1414: 1415: 1416: 1417: 1418: 1419: 1420: 1421: 1422: 1423: 1424: 1425: 1426: 1427: 1428: 1429: 1430: 1431: 1432: 1433: 1434: 1435: 1436: 1437: 1438: 1439: 1440: 1441: 1442: 1443: 1444: 1445: 1446: 1447: 1448: 1449: 1450: 1451: 1452: 1453: 1454: 1455: 1456: 1457: 1458: 1459: 1460: 1461: 1462: 1463: 1464: 1465: 1466: 1467: 1468: 1469: 1470: 1471: 1472: 1473: 1474: 1475: 1476: 1477: 1478: 1479: 1480: 1481: 1482: 1483: 1484: 1485: 1486: 1487: 1488: 1489: 1490: 1491: 1492: 1493: 1494: 1495: 1496: 1497: 1498: 1499: 1500: 1501: 1502: 1503: 1504: 1505: 1506: 1507: 1508: 1509: 1510: 1511: 1512: 1513: 1514: 1515: 1516: 1517: 1518: 1519: 1520: 1521: 1522: 1523: 1524: 1525: 1526: 1527: 1528: 1529: 1530: 1531: 1532: 1533: 1534: 1535: 1536: 1537: 1538: 1539: 1540: 1541: 1542: 1543: 1544: 1545: 1546: 1547: 1548: 1549: 1550: 1551: 1552: 1553: 1554: 1555: 1556: 1557: 1558: 1559: 1560: 1561: 1562: 1563: 1564: 1565: 1566: 1567: 1568: 1569: 1570: 1571: 1572: 1573: 1574: 1575: 1576: 1577: 1578: 1579: 1580: 1581: 1582: 1583: 1584: 1585: 1586: 1587: 1588: 1589: 1590: 1591: 1592: 1593: 1594: 1595: 1596: 1597: 1598: 1599: 1600: 1601: 1602: 1603: 1604: 1605: 1606: 1607: 1608: 1609: 1610: 1611: 1612: 1613: 1614: 1615: 1616: 1617: 1618: 1619: 1620: 1621: 1622: 1623: 1624: 1625: 1626: 1627: 1628: 1629: 1630: 1631: 1632: 1633: 1634: 1635: 1636: 1637: 1638: 1639: 1640: 1641: 1642: 1643: 1644: 1645: 1646: 1647: 1648: 1649: 1650: 1651: 1652: 1653: 1654: 1655: 1656: 1657: 1658: 1659: 1660: 1661: 1662: 1663: 1664: 1665: 1666: 1667: 1668: 1669: 1670: 1671: 1672: 1673: 1674: 1675: 1676: 1677: 1678: 1679: 1680: 1681: 1682: 1683: 1684: 1685: 1686: 1687: 1688: 1689: 1690: 1691: 1692: 1693: 1694: 1695: 1696: 1697: 1698: 1699: 1700: 1701: 1702: 1703: 1704: 1705: 1706: 1707: 1708: 1709: 1710: 1711: 1712: 1713: 1714: 1715: 1716: 1717: 1718: 1719: 1720: 1721: 1722: 1723: 1724: 1725: 1726: 1727: 1728: 1729: 1730: 1731: 1732: 1733: 1734: 1735: 1736: 1737: 1738: 1739: 1740: 1741: 1742: 1743: 1744: 1745: 1746: 1747: 1748: 1749: 1750: 1751: 1752: 1753: 1754: 1755: 1756: 1757: 1758: 1759: 1760: 1761: 1762: 1763: 1764: 1765: 1766: 1767: 1768: 1769: 1770: 1771: 1772: 1773: 1774: 1775: 1776: 1777: 1778: 1779: 1780: 1781: 1782: 1783: 1784: 1785: 1786: 1787: 1788: 1789: 1790: 1791: 1792: 1793: 1794: 1795: 1796: 1797: 1798: 1799: 1800: 1801: 1802: 1803: 1804: 1805: 1806: 1807: 1808: 1809: 1810: 1811: 1812: 1813: 1814: 1815: 1816: 1817: 1818: 1819: 1820: 1821: 1822: 1823: 1824: 1825: 1826: 1827: 1828: 1829: 1830: 1831: 1832: 1833: 1834: 1835: 1836: 1837: 1838: 1839: 1840: 1841: 1842: 1843: 1844: 1845: 1846: 1847: 1848: 1849: 1850: 1851: 1852: 1853: 1854: 1855: 1856: 1857: 1858: 1859: 1860: 1861: 1862: 1863: 1864: 1865: 1866: 1867: 1868: 1869: 1870: 1871: 1872: 1873: 1874: 1875: 1876: 1877: 1878: 1879: 1880: 1881: 1882: 1883: 1884: 1885: 1886: 1887: 1888: 1889: 1890: 1891: 1892: 1893: 1894: 1895: 1896: 1897: 1898: 1899: 1900: 1901: 1902: 1903: 1904: 1905: 1906: 1907: 1908: 1909: 1910: 1911: 1912: 1913: 1914: 1915: 1916: 1917: 1918: 1919: 1920: 1921: 1922: 1923: 1924: 1925: 1926: 1927: 1928: 1929: 1930: 1931: 1932: 1933: 1934: 1935: 1936: 1937: 1938: 1939: 1940: 1941: 1942: 1943: 1944: 1945: 1946: 1947: 1948: 1949: 1950: 1951: 1952: 1953: 1954: 1955: 1956: 1957: 1958: 1959: 1960: 1961: 1962: 1963: 1964: 1965: 1966: 1967: 1968: 1969: 1970: 1971: 1972: 1973: 1974: 1975: 1976: 1977: 1978: 1979: 1980: 1981: 1982: 1983: 1984: 1985: 1986: 1987: 1988: 1989: 1990: 1991: 1992: 1993: 1994: 1995: 1996: 1997: 1998: 1999: 2000: 2001: 2002: 2003: 2004: 2005: 2006: 2007: 2008: 2009: 2010: 2011: 2012: 2013: 2014: 2015: 2016: 2017: 2018: 2019: 2020: 2021: 2022: 2023: 2024: 2025: 2026: 2027: 2028: 2029: 2030: 2031: 2032: 2033: 2034: 2035: 2036: 2037: 2038: 2039: 2040: 2041: 2042: 2043: 2044: 2045: 2046: 2047: 2048: 2049: 2050: 2051: 2052: 2053: 2054: 2055: 2056: 2057: 2058: 2059: 2060: 2061: 2062: 2063: 2064: 2065: 2066: 2067: 2068: 2069: 2070: 2071: 2072: 2073: 2074: 2075: 2076: 2077: 2078: 2079: 2080: 2081: 2082: 2083: 2084: 2085: 2086: 2087: 2088: 2089: 2090: 2091: 2092: 2093: 2094: 2095: 2096: 2097: 2098: 2099: 2100: 2101: 2102: 2103: 2104: 2105: 2106: 2107: 2108: 2109: 2110: 2111: 2112: 2113: 2114: 2115: 2116: 2117: 2118: 2119: 2120: 2121: 2122: 2123: 2124: 2125: 2126: 2127: 2128: 2129: 2130: 2131: 2132: 2133: 2134: 2135: 2136: 2137: 2138: 2139: 2140: 2141: 2142: 2143: 2144: 2145: 2146: 2147: 2148: 2149: 2150: 2151: 2152: 2153: 2154: 2155: 2156: 2157: 2158: 2159: 2160: 2161: 2162: 2163: 2164: 2165: 2166: 2167: 2168: 2169: 2170: 2171: 2172: 2173: 2174: 2175: 2176: 2177: 2178: 2179: 2180: 2181: 2182: 2183: 2184: 2185: 2186: 2187: 2188: 2189: 2190: 2191: 2192: 2193: 2194: 2195: 2196: 2197: 2198: 2199: 2200: 2201: 2202: 2203: 2204: 2205: 2206: 2207: 2208: 2209: 2210: 2211: 2212: 2213: 2214: 2215: 2216: 2217: 2218: 2219: 2220: 2221: 2222: 
<?php
/**
 * Gravity Flow Step
 *
 * @package     GravityFlow
 * @subpackage  Classes/Step
 * @copyright   Copyright (c) 2015-2018, Steven Henty S.L.
 * @license     http://opensource.org/licenses/gpl-2.0.php GNU Public License
 * @since       1.0
 */

if ( ! class_exists( 'GFForms' ) ) {
    die();
}

/**
 * An abstract class used as the base for all Steps.
 *
 * Class Gravity_Flow_Step
 *
 * @since 1.0
 */
abstract class Gravity_Flow_Step extends stdClass {

    /**
     * The ID of the Step
     *
     * @var int
     */
    private $_id;

    /**
     * The Feed meta on which this step is based.
     *
     * @var array
     */
    private $_meta;

    /**
     * Step is active
     *
     * @var bool
     */
    private $_is_active;

    /**
     * The Form ID for this step.
     *
     * @var int
     */
    private $_form_id;

    /**
     * The entry for this step.
     *
     * @var array|null
     */
    private $_entry;

    /**
     * The assignees for this step.
     *
     * @since 1.8.1
     *
     * @var Gravity_Flow_Assignee[]
     */
    protected $_assignees = array();

    /**
     * The assignee emails for which notifications have been processed.
     *
     * @var array
     */
    private $_assignees_emailed = array();

    /**
     * A unique key for this step type. This property must be overridden by extending classes.
     *
     * @var string
     */
    protected $_step_type;

    /**
     * The next step. This could be either a step ID (integer) or one of the following:
     * - next
     * - complete
     *
     * @var int|string
     */
    protected $_next_step_id;

    /**
     * The resource slug for the REST API.
     *
     * This should be a plural noun.
     *
     * e.g. approvals
     *
     * @var string
     */
    protected $_rest_base = null;


    /**
     * The constructor for the Step. Provide an entry object to perform and entry-specific tasks.
     *
     * @param array      $feed  Required. The Feed on which this step is based.
     * @param null|array $entry Optional. Instantiate with an entry to perform entry related tasks.
     */
    public function __construct( $feed = array(), $entry = null ) {
        if ( empty( $feed ) ) {
            return;
        }

        $this->_id        = absint( $feed['id'] );
        $this->_is_active = (bool) $feed['is_active'];
        $this->_form_id   = absint( $feed['form_id'] );
        $this->_step_type = $feed['meta']['step_type'];
        $this->_meta      = $feed['meta'];
        $this->_entry     = $entry;
    }

    /**
     * Magic method to allow direct access to the settings as properties.
     * Returns an empty string for undefined properties allowing for graceful backward compatibility where new settings may not have been defined in stored settings.
     *
     * @param string $name The property key.
     *
     * @return mixed
     */
    public function &__get( $name ) {
        if ( ! isset( $this->_meta[ $name ] ) ) {
            $this->_meta[ $name ] = '';
        }

        return $this->_meta[ $name ];
    }

    /**
     * Sets the value for the specified property.
     *
     * @param string $key   The property key.
     * @param mixed  $value The property value.
     */
    public function __set( $key, $value ) {
        $this->_meta[ $key ] = $value;
        $this->$key          = $value;
    }

    /**
     * Determines if the specified property has been defined.
     *
     * @param string $key The property key.
     *
     * @return bool
     */
    public function __isset( $key ) {
        return isset( $this->_meta[ $key ] );
    }

    /**
     * Deletes the specified property.
     *
     * @param string $key The property key.
     */
    public function __unset( $key ) {
        unset( $this->$key );
    }

    /**
     * Returns an array of the configuration of the status options for this step.
     * These options will appear in the step settings.
     * Override this method to add status options.
     *
     * For example, a status configuration may look like this:
     * array(
     *    'status' => 'complete',
     *    'status_label' => __( 'Complete', 'gravityflow' ),
     *    'destination_setting_label' => __( 'Next Step', 'gravityflow' ),
     *    'default_destination' => 'next',
     *    )
     *
     * @return array An array of arrays
     */
    public function get_status_config() {
        return array(
            array(
                'status'                    => 'complete',
                'status_label'              => __( 'Complete', 'gravityflow' ),
                'destination_setting_label' => __( 'Next Step', 'gravityflow' ),
                'default_destination'       => 'next',
            ),
        );
    }

    /**
     * Returns an array of the configuration of the status options for this step.
     *
     * @deprecated
     *
     * @return array
     */
    public function get_final_status_config() {
        return $this->get_status_config();
    }

    /**
     * Returns an array of quick actions to be displayed on the inbox.
     *
     * Example:
     *
     * array(
     *  array(
     *      'key' => 'approve',
     *      'icon' => $this->get_approve_icon(),
     *      'label' => __( 'Approve', 'gravityflow' ),
     *      'show_note_field' => true
     *   ),
     * array(
     *      'key' => 'reject',
     *      'icon' => $this->get_reject_icon(),
     *      'label' => __( 'Reject', 'gravityflow' ),
     *      'show_note_field' => false
     *   ),
     * );
     *
     * @return array
     */
    public function get_actions() {
        return array();
    }

    /**
     * Returns the resource slug for the REST API.
     *
     * @return string
     */
    public function get_rest_base() {
        return $this->_rest_base;
    }

    /**
     * Process the REST request for an entry.
     *
     * @deprecated 1.7.1
     *
     * @param WP_REST_Request $request Full data about the request.
     *
     * @return WP_REST_Response|mixed If response generated an error, WP_Error, if response
     *                                is already an instance, WP_HTTP_Response, otherwise
     *                                returns a new WP_REST_Response instance.
     */
    public function handle_rest_request( $request ) {
        return new WP_Error( 'not_implemented', __( ' Not implemented', 'gravityflow' ) );
    }

    /**
     * Check if a REST request has permission.
     *
     * @since  1.4.3
     * @access public
     *
     * @param WP_REST_Request $request Full data about the request.
     *
     * @return WP_Error|boolean
     */
    public function rest_permission_callback( $request ) {

        if ( ! is_user_logged_in() ) {

            // Email assignee authentication & nonce check.
            $nonce = $request->get_header( 'x_wp_nonce' );

            if ( empty( $nonce ) ) {
                if ( isset( $request['_wpnonce'] ) ) {
                    $nonce = $request['_wpnonce'];
                } elseif ( isset( $request['HTTP_X_WP_NONCE'] ) ) {
                    $nonce = $request['HTTP_X_WP_NONCE'];
                }
            }

            if ( empty( $nonce ) ) {
                return false;
            }

            // Check the nonce.
            $result = wp_verify_nonce( $nonce, 'wp_rest' );

            if ( ! $result ) {
                return new WP_Error( 'rest_cookie_invalid_nonce', __( 'Cookie nonce is invalid' ), array( 'status' => 403 ) );
            }
        }

        $assignees = $this->get_assignees();

        foreach ( $assignees as $assignee ) {
            if ( $assignee->is_current_user() ) {
                return true;
            }
        }

        return false;
    }

    /**
     * Process the REST request for an entry.
     *
     * @param WP_REST_Request $request Full data about the request.
     *
     * @return WP_REST_Response|mixed If response generated an error, WP_Error, if response
     *                                is already an instance, WP_HTTP_Response, otherwise
     *                                returns a new WP_REST_Response instance.
     */
    public function rest_callback( $request ) {
        return new WP_Error( 'not_implemented', __( ' Not implemented', 'gravityflow' ) );
    }



    /**
     * Returns the translated label for a status key.
     *
     * @param string $status The status key.
     *
     * @return string
     */
    public function get_status_label( $status ) {
        if ( $status == 'pending' ) {
            return __( 'Pending', 'gravityflow' );
        }
        $status_configs = $this->get_status_config();
        foreach ( $status_configs as $status_config ) {
            if ( strtolower( $status ) == rgar( $status_config, 'status' ) ) {
                return isset( $status_config['status_label'] ) ? $status_config['status_label'] : $status;
            }
        }

        return $status;
    }

    /**
     * Returns the label for the step.
     *
     * Override this method to return a custom label.
     *
     * @return string
     */
    public function get_label() {
        return $this->get_type();
    }

    /**
     * If set, returns the entry for this step.
     *
     * @return array|null
     */
    public function get_entry() {
        if ( empty( $this->_entry ) ) {
            $this->refresh_entry();
        }

        return $this->_entry;
    }

    /**
     * Flushes and reloads the cached entry for this step.
     *
     * @return array|mixed|null
     */
    public function refresh_entry() {
        $entry_id = $this->get_entry_id();
        if ( ! empty( $entry_id ) ) {
            $this->_entry = GFAPI::get_entry( $entry_id );
        }

        return $this->_entry;
    }

    /**
     * Returns the Form object for this step.
     *
     * @return mixed
     */
    public function get_form() {
        $entry = $this->get_entry();
        if ( $entry ) {
            $form_id = $entry['form_id'];
        } else {
            $form_id = $this->get_form_id();
        }

        $form = GFAPI::get_form( $form_id );

        return $form;
    }

    /**
     * Returns the ID for the current entry object. If not set the lid query arg is returned.
     *
     * @return int
     */
    public function get_entry_id() {
        if ( empty( $this->_entry ) ) {
            return rgget( 'lid' );
        }
        $id = absint( $this->_entry['id'] );

        return $id;
    }

    /**
     * Returns the step type.
     *
     * @return string
     */
    public function get_type() {
        return $this->_step_type;
    }

    /**
     * Returns the Step ID.
     *
     * @return int
     */
    public function get_id() {
        return $this->_id;
    }

    /**
     * Is the step active? The step may have been deactivated by the user in the list of steps.
     *
     * @return bool
     */
    public function is_active() {
        return $this->_is_active;
    }

    /**
     * Is this step supported on this server? Override to hide this step in the list of step types if the requirements are not met.
     *
     * @return bool
     */
    public function is_supported() {
        return true;
    }

    /**
     * Returns the ID of the Form object for the step.
     *
     * @return int
     */
    public function get_form_id() {
        if ( empty( $this->_form_id ) ) {
            $this->_form_id = absint( rgget( 'id' ) );
        }

        return $this->_form_id;
    }

    /**
     * Returns the user-defined name of the step.
     *
     * @return string
     */
    public function get_name() {
        return $this->step_name;
    }

    /**
     * Get the API for preparing common settings such as those which appear on notification tabs.
     *
     * @since 1.5.1-dev
     *
     * @return Gravity_Flow_Common_Step_Settings
     */
    public function get_common_settings_api() {
        require_once( 'class-common-step-settings.php' );

        return new Gravity_Flow_Common_Step_Settings();
    }

    /**
     * Override this method to add settings to the step. Use the Gravity Forms Add-On Framework Settings API.
     *
     * @return array
     */
    public function get_settings() {
        return array();
    }

    /**
     * Override this method to set a custom icon in the step settings.
     * 32px x 32px
     *
     * @return string
     */
    public function get_icon_url() {
        return $this->get_base_url() . '/images/gravityflow-icon-blue.svg';
    }

    /**
     * Returns the Gravity Flow base URL.
     *
     * @return string
     */
    public function get_base_url() {
        return gravity_flow()->get_base_url();
    }

    /**
     * Returns the Gravity Flow base path.
     *
     * @return string
     */
    public function get_base_path() {
        return gravity_flow()->get_base_path();
    }

    /**
     * Returns the ID of the next step.
     *
     * @return int|string
     */
    public function get_next_step_id() {
        if ( isset( $this->_next_step_id ) ) {
            return $this->_next_step_id;
        }
        $status                 = $this->evaluate_status();
        $destination_status_key = 'destination_' . $status;
        if ( isset( $this->{$destination_status_key} ) ) {
            $next_step_id = $this->{$destination_status_key};
        } else {
            $next_step_id = 'next';
        }

        $this->set_next_step_id( $next_step_id );

        return $next_step_id;
    }

    /**
     * Sets the next step.
     *
     * @param int|string $id The ID of the next step.
     */
    public function set_next_step_id( $id ) {
        $this->_next_step_id = $id;
    }

    /**
     * Attempts to start this step for the current entry. If the step is scheduled then the entry will be queued.
     *
     * @return bool Is the step complete?
     */
    public function start() {

        $entry_id = $this->get_entry_id();

        $this->log_debug( __METHOD__ . '() - triggered step: ' . $this->get_name() . ' for entry id ' . $entry_id );

        $step_id = $this->get_id();

        gform_update_meta( $entry_id, 'workflow_step', $step_id );

        $step_timestamp = $this->get_step_timestamp();
        if ( empty( $step_timestamp ) ) {
            $this->log_debug( __METHOD__ . '() - No timestamp, adding one' );
            gform_update_meta( $entry_id, 'workflow_step_' . $this->get_id() . '_timestamp', time() );
            $this->refresh_entry();
        }

        $status = $this->evaluate_status();
        $this->log_debug( __METHOD__ . '() - Step status before processing: ' . $status );


        if ( $this->scheduled && ! $this->validate_schedule() ) {
            if ( $status == 'queued' ) {
                $this->log_debug( __METHOD__ . '() - Step still queued: ' . $this->get_name() );
            } else {
                $this->update_step_status( 'queued' );
                $this->refresh_entry();
                $this->log_event( 'queued' );
                $this->log_debug( __METHOD__ . '() - Step queued: ' . $this->get_name() );
            }
            $complete = false;
        } else {
            $this->log_debug( __METHOD__ . '() - Starting step: ' . $this->get_name() );
            gform_update_meta( $entry_id, 'workflow_step_' . $this->get_id() . '_timestamp', time() );

            $this->update_step_status();

            $this->refresh_entry();

            $this->log_event( 'started' );

            $complete = $this->process();

            $log_is_complete = $complete ? 'yes' : 'no';
            $this->log_debug( __METHOD__ . '() - step complete: ' . $log_is_complete );
        }

        return $complete;
    }

    /**
     * Is the step currently in the queue waiting for the scheduled start time?
     *
     * @return bool
     */
    public function is_queued() {
        $entry = $this->get_entry();

        return rgar( $entry, 'workflow_step_status_' . $this->get_id() ) == 'queued';
    }

    /**
     * Validates the step schedule.
     *
     * @return bool Returns true if step is ready to proceed.
     */
    public function validate_schedule() {
        if ( ! $this->scheduled ) {
            return true;
        }

        $this->log_debug( __METHOD__ . '() step is scheduled' );

        $schedule_timestamp = $this->get_schedule_timestamp();

        $this->log_debug( __METHOD__ . '() schedule_timestamp: ' . $schedule_timestamp );
        $this->log_debug( __METHOD__ . '() schedule_timestamp formatted: ' . date( 'Y-m-d H:i:s', $schedule_timestamp ) );

        $current_time = time();

        $this->log_debug( __METHOD__ . '() current_time: ' . $current_time );
        $this->log_debug( __METHOD__ . '() current_time formatted: ' . date( 'Y-m-d H:i:s', $current_time ) );

        return $current_time >= $schedule_timestamp;
    }

    /**
     * Returns the schedule timestamp (UTC) calculated from the schedule settings.
     *
     * @return bool|int
     */
    public function get_schedule_timestamp() {
        if ( ! $this->scheduled ) {
            return false;
        }

        switch ( $this->schedule_type ) {
            case 'date':
                $schedule_timestamp = $this->get_timestamp_date( 'schedule' );
                break;

            case 'date_field':
                $schedule_timestamp = $this->get_timestamp_date_field( 'schedule' );
                break;

            case 'delay':
            default:
                $schedule_timestamp = $this->get_timestamp_delay( 'schedule' );
        }

        /**
         * Allows the scheduled date/timestamp to be custom defined.
         *
         * @since 2.0.2-dev
         *
         * @param int               $schedule_timestamp The current scheduled timestamp (UTC)
         * @param string            $schedule_type      The type of schedule defined in step settings.
         * @param Gravity_Flow_Step $this               The current step.
         *
         * @return int
         */
        $schedule_timestamp = apply_filters( 'gravityflow_step_schedule_timestamp', $schedule_timestamp, $this->schedule_type, $this );

        return $schedule_timestamp;
    }

    /**
     * Determines if the step has expired.
     *
     * @return bool
     */
    public function is_expired() {
        if ( ! $this->supports_expiration() ) {
            return false;
        }

        if ( ! $this->expiration ) {
            return false;
        }

        $this->log_debug( __METHOD__ . '() step is scheduled for expiration' );

        $expiration_timestamp = $this->get_expiration_timestamp();

        $this->log_debug( __METHOD__ . '() expiration_timestamp UTC: ' . $expiration_timestamp );
        $this->log_debug( __METHOD__ . '() expiration_timestamp formatted UTC: ' . date( 'Y-m-d H:i:s', $expiration_timestamp ) );

        // Schedule delay is relative to UTC. Schedule date is relative to timezone of the site.
        $current_time = time();

        $this->log_debug( __METHOD__ . '() current_time UTC: ' . $current_time );
        $this->log_debug( __METHOD__ . '() current_time formatted UTC: ' . date( 'Y-m-d H:i:s', $current_time ) );

        $is_expired = $current_time >= $expiration_timestamp;

        $this->log_debug( __METHOD__ . '() is expired? ' . ( $is_expired ? 'yes' : 'no' ) );

        return $is_expired;
    }

    /**
     * Returns the expiration timestamp calculated from the expiration settings.
     *
     * @return bool|int
     */
    public function get_expiration_timestamp() {
        if ( ! $this->expiration ) {
            return false;
        }

        switch ( $this->expiration_type ) {
            case 'date':
                $expiration_timestamp = $this->get_timestamp_date( 'expiration' );
                break;

            case 'date_field':
                $expiration_timestamp = $this->get_timestamp_date_field( 'expiration' );
                break;

            case 'delay':
            default:
                $expiration_timestamp = $this->get_timestamp_delay( 'expiration' );
        }

        /**
         * Allows the expiration timestamp to be overridden.
         *
         * @since 2.3.2
         *
         * @param int               $expiration_timestamp The current expiration timestamp (UTC).
         * @param string            $expiration_type      The type of expiration defined in step settings.
         * @param Gravity_Flow_Step $this                 The current step.
         *
         * @return int
         */
        $expiration_timestamp = apply_filters( 'gravityflow_step_expiration_timestamp', $expiration_timestamp, $this->expiration_type, $this );

        return $expiration_timestamp;
    }

    /**
     * Returns the timestamp for the date based expiration or schedule.
     *
     * @since 2.3.2
     *
     * @param string $setting_type The setting type: expiration or schedule.
     *
     * @return bool|int
     */
    public function get_timestamp_date( $setting_type ) {
        if ( $this->{$setting_type . '_type'} != 'date' ) {
            return false;
        }

        $datetime  = strtotime( $this->{$setting_type . '_date'} );
        $date      = date( 'Y-m-d H:i:s', $datetime );
        $date_gmt  = get_gmt_from_date( $date );
        $timestamp = strtotime( $date_gmt );

        return $timestamp;
    }

    /**
     * Returns the timestamp for the date field based expiration or schedule.
     *
     * @since 2.3.2
     *
     * @param string $setting_type The setting type: expiration or schedule.
     *
     * @return bool|int
     */
    public function get_timestamp_date_field( $setting_type ) {
        if ( $this->{$setting_type . '_type'} != 'date_field' ) {
            return false;
        }

        $entry            = $this->get_entry();
        $date_field_value = rgar( $entry, (string) $this->{$setting_type . '_date_field'} );

        if ( empty( $date_field_value ) ) {
            return false;
        }

        $datetime  = strtotime( $date_field_value );
        $date      = date( 'Y-m-d H:i:s', $datetime );
        $date_gmt  = get_gmt_from_date( $date );
        $timestamp = strtotime( $date_gmt );

        // Calculate offset.
        if ( $this->{$setting_type . '_date_field_offset'} ) {
            $offset = 0;
            switch ( $this->{$setting_type . '_date_field_offset_unit'} ) {
                case 'minutes' :
                    $offset = ( MINUTE_IN_SECONDS * $this->{$setting_type . '_date_field_offset'} );
                    break;
                case 'hours' :
                    $offset = ( HOUR_IN_SECONDS * $this->{$setting_type . '_date_field_offset'} );
                    break;
                case 'days' :
                    $offset = ( DAY_IN_SECONDS * $this->{$setting_type . '_date_field_offset'} );
                    break;
                case 'weeks' :
                    $offset = ( WEEK_IN_SECONDS * $this->{$setting_type . '_date_field_offset'} );
                    break;
            }
            if ( $this->{$setting_type . '_date_field_before_after'} == 'before' ) {
                $timestamp -= $offset;
            } else {
                $timestamp += $offset;
            }
        }

        return $timestamp;
    }

    /**
     * Returns the timestamp for the delay based expiration or schedule.
     *
     * @since 2.3.2
     *
     * @param string $setting_type The setting type: expiration or schedule.
     *
     * @return bool|int
     */
    public function get_timestamp_delay( $setting_type ) {
        if ( $this->{$setting_type . '_type'} != 'delay' ) {
            return false;
        }

        $timestamp = $this->get_step_timestamp();

        switch ( $this->{$setting_type . '_delay_unit'} ) {
            case 'minutes' :
                $timestamp += ( MINUTE_IN_SECONDS * $this->{$setting_type . '_delay_offset'} );
                break;
            case 'hours' :
                $timestamp += ( HOUR_IN_SECONDS * $this->{$setting_type . '_delay_offset'} );
                break;
            case 'days' :
                $timestamp += ( DAY_IN_SECONDS * $this->{$setting_type . '_delay_offset'} );
                break;
            case 'weeks' :
                $timestamp += ( WEEK_IN_SECONDS * $this->{$setting_type . '_delay_offset'} );
                break;
        }

        return $timestamp;
    }

    /**
     * Returns the value of the entries workflow_timestamp property.
     *
     * @return string|int
     */
    public function get_entry_timestamp() {
        $entry = $this->get_entry();

        return $entry['workflow_timestamp'];
    }

    /**
     * Returns the step timestamp from the entry meta.
     *
     * @return bool|int
     */
    public function get_step_timestamp() {
        $timestamp = gform_get_meta( $this->get_entry_id(), 'workflow_step_' . $this->get_id() . '_timestamp' );

        return $timestamp;
    }

    /**
     * Process the step. For example, assign to a user, send to a service, send a notification or do nothing. Return (bool) $complete.
     *
     * @return bool Is the step complete?
     */
    public function process() {
        return true;
    }

    /**
     * Set the assignee status to pending and trigger sending of the assignee notification if enabled.
     *
     * @return bool
     */
    public function assign() {
        $complete = $this->is_complete();

        $assignees = $this->get_assignees();

        if ( empty( $assignees ) ) {
            $this->add_note( sprintf( __( '%s: No assignees', 'gravityflow' ), $this->get_name() ) );
        } else {
            foreach ( $assignees as $assignee ) {
                $assignee->update_status( 'pending' );
                // Send notification.
                $this->maybe_send_assignee_notification( $assignee );
                $complete = false;
            }
        }

        return $complete;
    }

    /**
     * Sends the assignee email if the assignee_notification_setting is enabled.
     *
     * @param Gravity_Flow_Assignee $assignee    The assignee properties.
     * @param bool                  $is_reminder Indicates if this is a reminder notification. Default is false.
     */
    public function maybe_send_assignee_notification( $assignee, $is_reminder = false ) {
        if ( $this->assignee_notification_enabled ) {
            $this->send_assignee_notification( $assignee, $is_reminder );
        }
    }

    /**
     * Retrieves the properties for the specified notification type; building an array using the keys required by Gravity Forms.
     *
     * @param string $type The type of notification currently being processed e.g. assignee, approval, or rejection.
     *
     * @return array
     */
    public function get_notification( $type ) {
        $notification = array( 'workflow_notification_type' => $type );

        $type .= '_notification_';
        $from_name  = $type . 'from_name';
        $from_email = $type . 'from_email';
        $subject    = $type . 'subject';

        $notification['fromName']          = empty( $this->{$from_name} ) ? get_bloginfo() : $this->{$from_name};
        $notification['from']              = empty( $this->{$from_email} ) ? get_bloginfo( 'admin_email' ) : $this->{$from_email};
        $notification['replyTo']           = $this->{$type . 'reply_to'};
        $notification['bcc']               = $this->{$type . 'bcc'};
        $notification['message']           = $this->{$type . 'message'};
        $notification['disableAutoformat'] = $this->{$type . 'disable_autoformat'};

        if ( empty( $this->{$subject} ) ) {
            $form                    = $this->get_form();
            $notification['subject'] = $form['title'] . ': ' . $this->get_name();
        } else {
            $notification['subject'] = $this->{$subject};
        }

        if ( defined( 'PDF_EXTENDED_VERSION' ) && version_compare( PDF_EXTENDED_VERSION, '4.0-RC2', '>=' ) ) {
            if ( $this->{$type . 'gpdfEnable'} ) {
                $gpdf_id      = $this->{$type . 'gpdfValue'};
                $notification = $this->gpdf_add_notification_attachment( $notification, $gpdf_id );
            }
        }

        return $notification;
    }

    /**
     * Retrieve the assignees for the current
     *
     * @param string $type The type of notification currently being processed e.g. assignee, approval, or rejection.
     *
     * @return array
     */
    public function get_notification_assignees( $type ) {
        $type              .= '_notification_';
        $notification_type = $this->{$type . 'type'};
        $assignees         = array();

        switch ( $notification_type ) {
            case 'select' :
                $users = $this->{$type . 'users'};
                if ( is_array( $users ) ) {
                    foreach ( $users as $assignee_key ) {
                        $assignees[] = $this->get_assignee( $assignee_key );
                    }
                }

                break;
            case 'routing' :
                $routings = $this->{$type . 'routing'};
                if ( is_array( $routings ) ) {
                    foreach ( $routings as $routing ) {
                        if ( $this->evaluate_routing_rule( $routing ) ) {
                            $assignees[] = $this->get_assignee( rgar( $routing, 'assignee' ) );
                        }
                    }
                }

                break;
        }

        return $assignees;
    }

    /**
     * Sends the assignee email.
     *
     * @param Gravity_Flow_Assignee $assignee    The assignee properties.
     * @param bool                  $is_reminder Indicates if this is a reminder notification. Default is false.
     */
    public function send_assignee_notification( $assignee, $is_reminder = false ) {
        $this->log_debug( __METHOD__ . '() starting. assignee: ' . $assignee->get_key() );

        $notification = $this->get_notification( 'assignee' );

        if ( $is_reminder ) {
            $notification['subject'] = esc_html__( 'Reminder', 'gravityflow' ) . ': ' . $notification['subject'];
        }

        $assignee->send_notification( $notification );
    }

    /**
     * Override this method to replace merge tags.
     * Important: call the parent method first.
     * $text = parent::replace_variables( $text, $assignee );
     *
     * @param string                $text     The text containing merge tags to be processed.
     * @param Gravity_Flow_Assignee $assignee The assignee properties.
     *
     * @return string
     */
    public function replace_variables( $text, $assignee ) {
        return $text;
    }

    /**
     * Replace the {workflow_entry_link}, {workflow_entry_url}, {workflow_inbox_link}, and {workflow_inbox_url} merge tags.
     *
     * @param string                $text     The text being processed.
     * @param Gravity_Flow_Assignee $assignee The assignee properties.
     *
     * @return string
     */
    public function replace_workflow_url_variables( $text, $assignee ) {
        _deprecated_function( 'replace_workflow_url_variables', '1.7.2', 'Gravity_Flow_Merge_Tags::get( \'workflow_url\', $args )->replace()' );

        $args = array(
            'assignee' => $assignee,
            'step' => $this,
        );

        $text = Gravity_Flow_Merge_Tags::get( 'workflow_url', $args )->replace( $text );

        return $text;
    }

    /**
     * Get the access token for the workflow_entry_ and workflow_inbox_ merge tags.
     *
     * @param array                 $a        The merge tag attributes.
     * @param Gravity_Flow_Assignee $assignee The assignee properties.
     *
     * @return string
     */
    public function get_workflow_url_access_token( $a, $assignee ) {
        _deprecated_function( 'get_workflow_url_access_token', '1.7.2', 'gravity_flow()->generate_access_token' );

        $force_token = $a['token'];
        $token       = '';

        if ( $assignee && $force_token ) {
            $token_lifetime_days        = apply_filters( 'gravityflow_entry_token_expiration_days', 30, $assignee );
            $token_expiration_timestamp = strtotime( '+' . (int) $token_lifetime_days . ' days' );
            $token                      = gravity_flow()->generate_access_token( $assignee, null, $token_expiration_timestamp );
        }

        return $token;
    }

    /**
     * Replace the {workflow_cancel_link} and {workflow_cancel_url} merge tags.
     *
     * @param string                $text     The text being processed.
     * @param Gravity_Flow_Assignee $assignee The assignee properties.
     *
     * @return string
     */
    public function replace_workflow_cancel_variables( $text, $assignee ) {
        _deprecated_function( 'replace_workflow_cancel_variables', '1.7.2', 'Gravity_Flow_Merge_Tags::get( \'workflow_cancel\', $args )->replace()' );

        if ( $assignee ) {
            $args = array(
                'assignee' => $assignee,
                'step'     => $this,
            );

            $text = Gravity_Flow_Merge_Tags::get( 'workflow_cancel', $args )->replace( $text );
        }

        return $text;
    }

    /**
     * Returns the entry URL.
     *
     * @param int|null              $page_id      The ID of the WordPress Page where the shortcode is located.
     * @param Gravity_Flow_Assignee $assignee     The assignee properties.
     * @param string                $access_token The access token for the current assignee.
     *
     * @return string
     */
    public function get_entry_url( $page_id = null, $assignee = null, $access_token = '' ) {

        _deprecated_function( 'get_entry_url', '1.7.2', 'Gravity_Flow_Common::get_workflow_url' );

        $query_args = array(
            'page' => 'gravityflow-inbox',
            'view' => 'entry',
            'id'   => $this->get_form_id(),
            'lid'  => $this->get_entry_id(),
        );

        return Gravity_Flow_Common::get_workflow_url( $query_args, $page_id, $assignee, $access_token );
    }

    /**
     * Returns the inbox URL.
     *
     * @param int|null              $page_id      The ID of the WordPress Page where the shortcode is located.
     * @param Gravity_Flow_Assignee $assignee     The assignee properties.
     * @param string                $access_token The access token for the current assignee.
     *
     * @return string
     */
    public function get_inbox_url( $page_id = null, $assignee = null, $access_token = '' ) {
        _deprecated_function( 'get_inbox_url', '1.7.2', 'Gravity_Flow_Common::get_workflow_url' );

        $query_args = array(
            'page' => 'gravityflow-inbox',
        );

        return Gravity_Flow_Common::get_workflow_url( $query_args, $page_id, $assignee, $access_token );
    }

    /**
     * Updates the status for this step.
     *
     * @param string|bool $status The step status.
     */
    public function update_step_status( $status = false ) {
        if ( empty( $status ) ) {
            $status = 'pending';
        }
        $entry_id = $this->get_entry_id();
        $step_id  = $this->get_id();
        gform_update_meta( $entry_id, 'workflow_step_status_' . $step_id, $status );
        gform_update_meta( $entry_id, 'workflow_step_status_' . $step_id . '_timestamp', time() );
    }

    /**
     * Ends the step if it's complete.
     *
     * @return bool Is the step complete?
     */
    public function end_if_complete() {
        $id = $this->get_next_step_id();
        $this->set_next_step_id( $id );

        $complete = $this->is_complete();
        if ( $complete ) {
            $this->end();
        }

        return $complete;
    }

    /**
     * Optionally override this method to add additional entry meta. See the Gravity Forms Add-On Framework for details on the return array.
     *
     * @param array $entry_meta The entry meta properties.
     * @param int   $form_id    The current form ID.
     *
     * @return array
     */
    public function get_entry_meta( $entry_meta, $form_id ) {
        return array();
    }

    /**
     * Returns the status key
     *
     * @param string      $assignee The assignee key.
     * @param bool|string $type     The assignee type.
     *
     * @return string
     */
    public function get_status_key( $assignee, $type = false ) {
        if ( $type === false ) {
            list( $type, $value ) = rgexplode( '|', $assignee, 2 );
        } else {
            $value = $assignee;
        }

        $key = 'workflow_' . $type . '_' . $value;

        return $key;
    }

    /**
     * Returns the status timestamp key
     *
     * @param string      $assignee_key The assignee key.
     * @param bool|string $type         The assignee type.
     *
     * @return string
     */
    public function get_status_timestamp_key( $assignee_key, $type = false ) {
        if ( $type === false ) {
            list( $type, $value ) = rgexplode( '|', $assignee_key, 2 );
        } else {
            $value = $assignee_key;
        }

        $key = 'workflow_' . $type . '_' . $value . '_timestamp';

        return $key;
    }

    /**
     * Retrieves the step status from the entry meta.
     *
     * @return bool|string
     */
    public function get_status() {
        $status_key = 'workflow_step_status_' . $this->get_id();
        $status     = gform_get_meta( $this->get_entry_id(), $status_key );

        return $status;
    }

    /**
     * Evaluates the status for the step.
     *
     * @return string 'queued' or 'complete'
     */
    public function evaluate_status() {
        if ( $this->is_queued() ) {
            return 'queued';
        }

        if ( $this->is_expired() ) {
            return $this->get_expiration_status_key();
        }

        $status = $this->get_status();

        if ( empty( $status ) ) {
            return 'pending';
        }

        return $this->status_evaluation();
    }

    /**
     * Override this to perform custom evaluation of the step status.
     *
     * @return string
     */
    public function status_evaluation() {
        return 'complete';
    }

    /**
     * Return the value of the status expiration setting.
     *
     * @return string
     */
    public function get_expiration_status_key() {
        $status_expiration = $this->status_expiration ? $this->status_expiration : 'complete';

        return $status_expiration;
    }

    /**
     * Processes the conditional logic for the entry in this step.
     *
     * @param array $form The current form.
     *
     * @return bool
     */
    public function is_condition_met( $form ) {
        $feed_meta            = $this->_meta;
        $is_condition_enabled = rgar( $feed_meta, 'feed_condition_conditional_logic' ) == true;
        $logic                = rgars( $feed_meta, 'feed_condition_conditional_logic_object/conditionalLogic' );

        if ( ! $is_condition_enabled || empty( $logic ) ) {
            return true;
        }
        $entry = $this->get_entry();

        return gravity_flow()->evaluate_conditional_logic( $logic, $form, $entry );
    }

    /**
     * Returns the status for a user. Defaults to current WordPress user or authenticated email address.
     *
     * @param int|bool $user_id The user ID.
     *
     * @return bool|string
     */
    public function get_user_status( $user_id = false ) {
        global $current_user;

        $type = 'user_id';

        if ( empty( $user_id ) ) {
            if ( $token = gravity_flow()->decode_access_token() ) {
                $assignee_key = sanitize_text_field( $token['sub'] );
                list( $type, $user_id ) = rgexplode( '|', $assignee_key, 2 );
            } else {
                $user_id = $current_user->ID;
            }
        }

        $key = $this->get_status_key( $user_id, $type );

        return gform_get_meta( $this->get_entry_id(), $key );
    }

    /**
     * Get the current role and status.
     *
     * @return array
     */
    public function get_current_role_status() {
        $current_role_status = false;
        $role                = false;

        foreach ( gravity_flow()->get_user_roles() as $role ) {
            $current_role_status = $this->get_role_status( $role );
            if ( $current_role_status == 'pending' ) {
                break;
            }
        }

        return array( $role, $current_role_status );
    }

    /**
     * Returns the status for the given role.
     *
     * @param string $role The user role.
     *
     * @return bool|string
     */
    public function get_role_status( $role ) {
        if ( empty( $role ) ) {
            return false;
        }
        $key = $this->get_status_key( $role, 'role' );

        return gform_get_meta( $this->get_entry_id(), $key );
    }

    /**
     * Updates the status for the given user.
     *
     * @param bool|int    $user_id             The user ID.
     * @param bool|string $new_assignee_status The assignee status.
     */
    public function update_user_status( $user_id = false, $new_assignee_status = false ) {
        if ( $user_id === false ) {
            global $current_user;
            $user_id = $current_user->ID;
        }

        $key = $this->get_status_key( $user_id, 'user_id' );
        gform_update_meta( $this->get_entry_id(), $key, $new_assignee_status );
    }

    /**
     * Updates the status for the given role.
     *
     * @param bool|string $role                The user role.
     * @param bool|string $new_assignee_status The assignee status.
     */
    public function update_role_status( $role = false, $new_assignee_status = false ) {
        if ( $role == false ) {
            $roles = gravity_flow()->get_user_roles( $role );
            $role  = current( $roles );
        }
        $entry_id  = $this->get_entry_id();
        $key       = $this->get_status_key( $role, 'role' );
        $timestamp = gform_get_meta( $entry_id, $key . '_timestamp' );
        $duration  = $timestamp ? time() - $timestamp : 0;

        gform_update_meta( $entry_id, $key, $new_assignee_status );
        gform_update_meta( $entry_id, $key . '_timestamp', time() );
        gravity_flow()->log_event( 'assignee', 'status', $this->get_form_id(), $entry_id, $new_assignee_status, $this->get_id(), $duration, $role, 'role', $role );
    }

    /**
     * Returns an array of assignees for this step.
     *
     * @return Gravity_Flow_Assignee[]
     */
    public function get_assignees() {
        if ( ! empty( $this->_assignees ) ) {
            return $this->_assignees;
        }

        if ( ! empty( $this->type ) ) {
            $this->maybe_add_select_assignees();
            $this->maybe_add_routing_assignees();
            $this->log_debug( __METHOD__ . '(): assignees: ' . print_r( $this->get_assignee_keys(), true ) );

            /**
             * Allows the assignees to be modified for the step.
             *
             * @since 1.8.1
             *
             * @param Gravity_Flow_Assignee[] $this->_assignees The array of Assignees.
             * @param Gravity_Flow_Step       $this The current step.
             */
            $this->_assignees = apply_filters( 'gravityflow_step_assignees', $this->_assignees, $this );

            return $this->_assignees;
        }

        return array();
    }

    /**
     * Retrieve an array containing this steps assignee details.
     *
     * @deprecated 1.8.1
     *
     * @return Gravity_Flow_Assignee[]
     */
    public function get_assignee_details() {
        _deprecated_function( 'get_assignee_details', '1.8.1', '$this->_assignees or get_assignees' );
        return $this->_assignees;
    }

    /**
     * Flush assignee details.
     */
    public function flush_assignees() {
        $this->_assignees = array();
    }

    /**
     * Retrieve an array containing the assignee keys for this step.
     *
     * @return array
     */
    public function get_assignee_keys() {
        $assignees = $this->_assignees;
        $assignee_keys = array();
        foreach( $assignees as $assignee ) {
            $assignee_keys[] = $assignee->get_key();
        }
        return $assignee_keys;
    }

    /**
     * Retrieve the assignee object for the given arguments.
     *
     * @param string|array $args An assignee key or array containing the id, type and editable_fields (if applicable).
     *
     * @return Gravity_Flow_Assignee
     */
    public function get_assignee( $args ) {
        $assignee = Gravity_Flow_Assignees::create( $args, $this );

        return $assignee;
    }

    /**
     * Get the assignee key for the current access token or user.
     *
     * @return string|bool
     */
    public function get_current_assignee_key() {

        return gravity_flow()->get_current_user_assignee_key();
    }

    /**
     * Get the status for the current assignee.
     *
     * @return bool|string
     */
    public function get_current_assignee_status() {
        $assignee_key = $this->get_current_assignee_key();
        $assignee     = $this->get_assignee( $assignee_key );

        return $assignee->get_status();
    }

    /**
     * Adds the assignees when the 'assign to' setting is set to 'select'.
     */
    public function maybe_add_select_assignees() {
        if ( $this->type != 'select' || ! is_array( $this->assignees ) ) {
            return;
        }

        $has_editable_fields = ! empty( $this->editable_fields );

        foreach ( $this->assignees as $assignee_key ) {
            $args = $this->get_assignee_args( $assignee_key );

            if ( $has_editable_fields ) {
                $args['editable_fields'] = $this->editable_fields;
            }

            $this->maybe_add_assignee( $args );
        }
    }

    /**
     * Adds the assignees when the 'assign to' setting is set to 'routing'.
     */
    public function maybe_add_routing_assignees() {
        if ( $this->type != 'routing' || ! is_array( $this->routing ) ) {
            return;
        }

        $entry = $this->get_entry();
        foreach ( $this->routing as $routing ) {
            $args                    = $this->get_assignee_args( rgar( $routing, 'assignee' ) );
            $args['editable_fields'] = rgar( $routing, 'editable_fields' );
            if ( $entry ) {
                if ( $this->evaluate_routing_rule( $routing ) ) {
                    $this->maybe_add_assignee( $args );
                }
            } else {
                $this->maybe_add_assignee( $args );
            }
        }
    }

    /**
     * Creates an array containing the assignees id and type from the supplied key.
     *
     * @param string $assignee_key The assignee key.
     *
     * @return array
     */
    public function get_assignee_args( $assignee_key ) {
        list( $assignee_type, $assignee_id ) = explode( '|', $assignee_key );
        $args = array(
            'id'   => $assignee_id,
            'type' => $assignee_type,
        );

        return $args;
    }

    /**
     * Adds the assignee to the step if certain conditions are met.
     *
     * @param string|array $args An assignee key or array containing the id, type and editable_fields (if applicable).
     */
    public function maybe_add_assignee( $args ) {
        $assignee = $this->get_assignee( $args );
        $id       = $assignee->get_id();
        $key      = $assignee->get_key();

        if ( ! empty( $id ) && ! in_array( $key, $this->get_assignee_keys() ) ) {
            $type = $assignee->get_type();
            switch ( $type ) {
                case 'user_id' :
                    $object = get_userdata( $id );
                    break;

                case 'assignee_multi_user_field' :
                    $entry      = $this->get_entry();
                    $json_value = $entry[ $id ];
                    $user_ids   = json_decode( $json_value );
                    if ( $user_ids && is_array( $user_ids ) ) {
                        $args['type'] = 'user_id';
                        foreach ( $user_ids as $user_id ) {
                            $user = get_userdata( $user_id );
                            if ( $user ) {
                                $args['id'] = $user_id;
                                $user_assignee = $this->get_assignee( $args );
                                $this->_assignees[] = $user_assignee;
                            }
                        }
                    }
                    $object = false;
                    break;

                case 'role' :
                    $object = get_role( $id );
                    break;

                default :
                    $object = true;
            }

            if ( $object ) {
                $this->_assignees[] = $assignee;
            }
        }
    }

    /**
     * Removes assignee from the step. This is only used for maintenance when the assignee settings change.
     *
     * @param Gravity_Flow_Assignee|bool $assignee The assignee properties.
     */
    public function remove_assignee( $assignee = false ) {
        if ( $assignee === false ) {
            global $current_user;
            $assignee = $this->get_assignee( 'user_id|' . $current_user->ID );
        }

        $assignee->remove();
    }

    /**
     * Handles POSTed values from the workflow detail page.
     *
     * @param array $form  The current form.
     * @param array $entry The current entry.
     *
     * @return string|bool|WP_Error Return a success feedback message safe for page output or a WP_Error instance with an error.
     */
    public function maybe_process_status_update( $form, $entry ) {
        return false;
    }

    /**
     * Displays content inside the Workflow metabox on the workflow detail page.
     *
     * @deprecated since 1.3.2
     *
     * @param array $form The Form array which may contain validation details.
     */
    public function workflow_detail_status_box( $form ) {
        _deprecated_function( 'workflow_detail_status_box', '1.3.2', 'workflow_detail_box' );

        $default_args = array(
            'display_empty_fields' => true,
            'check_permissions'    => true,
            'show_header'          => true,
            'timeline'             => true,
            'display_instructions' => true,
            'sidebar'              => true,
            'step_status'          => true,
            'workflow_info'        => true,
        );

        $this->workflow_detail_box( $form, $default_args );
    }

    /**
     * Displays content inside the Workflow metabox on the workflow detail page.
     *
     * @param array $form The Form array which may contain validation details.
     * @param array $args Additional args which may affect the display.
     */
    public function workflow_detail_box( $form, $args ) {

    }

    /**
     * Displays content inside the Workflow metabox on the Gravity Forms Entry Detail page.
     *
     * @param array $form The current form.
     */
    public function entry_detail_status_box( $form ) {

    }

    /**
     * Override to return an array of editable fields for the current user.
     *
     * @return array
     */
    public function get_editable_fields() {
        return array();
    }

    /**
     * Send the applicable notification if it is enabled and has assignees.
     *
     * @param string $type The type of notification currently being processed; approval or rejection.
     */
    public function maybe_send_notification( $type ) {
        if ( ! $this->{$type . '_notification_enabled'} ) {
            return;
        }

        $assignees = $this->get_notification_assignees( $type );

        if ( empty( $assignees ) ) {
            return;
        }

        $notification = $this->get_notification( $type );
        $this->send_notifications( $assignees, $notification );
    }

    /**
     * Sends an email.
     *
     * @param array $notification The notification properties.
     */
    public function send_notification( $notification ) {
        $entry = $this->get_entry();
        $form  = $this->get_form();

        $notification = apply_filters( 'gravityflow_notification', $notification, $form, $entry, $this );

        $to = rgar( $notification, 'to' );

        if ( in_array( $to, $this->_assignees_emailed ) ) {
            $this->log_debug( __METHOD__ . '() - aborting. assignee has already been sent a notification.' );

            return;
        }

        $this->_assignees_emailed[] = $to;

        $this->log_debug( __METHOD__ . '() - sending notification: ' . print_r( $notification, true ) );

        GFCommon::send_notification( $notification, $form, $entry );
    }

    /**
     * If Gravity PDF is enabled we'll generate the appropriate PDF and attach it to the current notification
     *
     * @param array  $notification The notification array currently being sent.
     * @param string $gpdf_id      The Gravity PDF ID.
     *
     * @return array
     */
    public function gpdf_add_notification_attachment( $notification, $gpdf_id ) {
        if ( ! method_exists( 'GPDFAPI', 'get_pdf' ) || ! method_exists( 'GPDFAPI', 'create_pdf' ) ) {
            return $notification;
        }

        /* Check if our PDF is active (might have been deactivated by users after saving Workflow) */
        $form_id  = $this->get_form_id();
        $entry_id = $this->get_entry_id();

        $pdf = GPDFAPI::get_pdf( $form_id, $gpdf_id );

        if ( ! is_wp_error( $pdf ) && true === $pdf['active'] ) {

            /* Generate and save the PDF */
            $pdf_path = GPDFAPI::create_pdf( $entry_id, $gpdf_id );

            if ( ! is_wp_error( $pdf_path ) ) {
                /* Ensure our notification has an array setup for the attachments key */
                $notification['attachments']   = ( isset( $notification['attachments'] ) ) ? $notification['attachments'] : array();
                $notification['attachments'][] = $pdf_path;
            }
        }

        return $notification;
    }

    /**
     * Ends the step cleanly and wraps up loose ends.
     * Sets the next step. Deletes assignee status entry meta.
     */
    public function end() {
        $next_step_id = $this->get_next_step_id();
        $this->set_next_step_id( $next_step_id );
        $status   = $this->evaluate_status();
        $started  = $this->get_step_timestamp();
        $duration = time() - $started;
        $this->update_step_status( $status );

        $assignees = $this->get_assignees();

        foreach ( $assignees as $assignee ) {
            $assignee->remove();
        }

        $entry_id = $this->get_entry_id();
        $step_id  = $this->get_id();

        if ( $this->can_set_workflow_status() ) {
            gform_update_meta( $entry_id, 'workflow_current_status', $status );
            gform_update_meta( $entry_id, 'workflow_current_status_timestamp', time() );
        }

        do_action( 'gravityflow_step_complete', $step_id, $entry_id, $this->get_form_id(), $status, $this );
        $this->log_debug( __METHOD__ . '() - ending step ' . $step_id );
        $this->log_event( 'ended', $status, $duration );
    }

    /**
     * Returns TRUE if this step can alter the current and final status.
     * If the only status option available for this step is 'complete' then, by default, the step will not set the status.
     * The default final status for the workflow is 'complete'.
     *
     * @return bool
     */
    public function can_set_workflow_status() {
        $status_config = $this->get_status_config();

        return ! ( count( $status_config ) === 1 && $status_config[0]['status'] = 'complete' );
    }

    /**
     * Override this method to check whether the step is complete in interactive and long running steps.
     *
     * @return bool
     */
    public function is_complete() {
        $status = $this->evaluate_status();

        return $status == 'complete' || $status == 'expired';
    }

    /**
     * Adds a note to the timeline. The timeline is a filtered subset of the Gravity Forms Entry notes.
     *
     * @since 1.7.1-dev Updated to store notes in the entry meta.
     * @since unknown
     *
     * @param string $note          The note to be added.
     * @param bool   $is_user_event Formerly $user_id; as of 1.7.1-dev indicates if the current note is the result of an assignee action.
     * @param bool   $deprecated    Formerly $user_name; no longer used as of 1.7.1-dev.
     */
    public function add_note( $note, $is_user_event = false, $deprecated = false ) {
        $user_id   = false;
        $user_name = $this->get_type();

        if ( $is_user_event ) {
            $assignee_key = $this->get_current_assignee_key();
            if ( $assignee_key ) {
                $assignee = $this->get_assignee( $assignee_key );
                if ( $assignee instanceof Gravity_Flow_Assignee && $assignee->get_type() === 'user_id' ) {
                    $user_id   = $assignee->get_id();
                    $user_name = $assignee->get_display_name();
                }
            }
        }

        GFFormsModel::add_note( $this->get_entry_id(), $user_id, $user_name, $note, 'gravityflow' );
    }

    /**
     * Adds a user submitted note.
     *
     * @since 1.7.1-dev
     *
     * @return string The user note which was added or an empty string.
     */
    public function maybe_add_user_note() {
        $note = trim( rgpost( 'gravityflow_note' ) );

        if ( $note ) {
            Gravity_Flow_Common::add_workflow_note( $note, $this->get_entry_id(), $this->get_id() );
            $note = sprintf( "\n%s: %s", __( 'Note', 'gravityflow' ), $note );
        }

        return $note;
    }

    /**
     * Evaluates a routing rule.
     *
     * @param array $routing_rule The routing rule properties.
     *
     * @return bool Is the routing rule a match?
     */
    public function evaluate_routing_rule( $routing_rule ) {
        $this->log_debug( __METHOD__ . '(): rule: ' . print_r( $routing_rule, true ) );

        $entry   = $this->get_entry();
        $form_id = $this->get_form_id();
        $form    = GFAPI::get_form( $form_id );

        $entry_meta_keys  = array_keys( GFFormsModel::get_entry_meta( $form_id ) );
        $entry_properties = array( 'created_by', 'date_created', 'currency', 'id', 'status', 'source_url', 'ip', 'is_starred' );

        $field_id = $routing_rule['fieldId'] == 'entry_id' ? 'id' : $routing_rule['fieldId'];

        if ( in_array( $field_id, $entry_meta_keys ) || in_array( $field_id, $entry_properties ) ) {
            $is_value_match = GFFormsModel::is_value_match( rgar( $entry, $field_id ), $routing_rule['value'], $routing_rule['operator'], null, $routing_rule, $form );
        } else {
            $source_field   = GFFormsModel::get_field( $form, $field_id );
            $field_value    = empty( $entry ) ? GFFormsModel::get_field_value( $source_field, array() ) : GFFormsModel::get_lead_field_value( $entry, $source_field );
            $is_value_match = GFFormsModel::is_value_match( $field_value, $routing_rule['value'], $routing_rule['operator'], $source_field, $routing_rule, $form );
        }

        $this->log_debug( __METHOD__ . '(): is_match: ' . var_export( $is_value_match, true ) );

        return $is_value_match;
    }

    /**
     * Sends a notification to an array of assignees.
     *
     * @param Gravity_Flow_Assignee[] $assignees    The assignee properties.
     * @param array                   $notification The notification properties.
     */
    public function send_notifications( $assignees, $notification ) {
        if ( empty( $assignees ) ) {
            return;
        }
        $form = $this->get_form();
        if ( empty( $notification['subject'] ) ) {
            $notification['subject'] = $form['title'] . ': ' . $this->get_name();
        } else {
            $notification['subject'] = $this->replace_variables( $notification['subject'], null );
        }

        foreach ( $assignees as $assignee ) {
            /* @var Gravity_Flow_Assignee $assignee */
            $assignee->send_notification( $notification );
        }
    }

    /**
     * Returns the number of entries on this step.
     *
     * @return int|mixed
     */
    public function entry_count() {
        if ( isset( $this->_entry_count ) ) {
            return $this->_entry_count;
        }
        $form_id            = $this->get_form_id();
        $search_criteria    = array(
            'status'        => 'active',
            'field_filters' => array(
                array(
                    'key'   => 'workflow_step',
                    'value' => $this->get_id(),
                ),
            ),
        );
        $this->_entry_count = GFAPI::count_entries( $form_id, $search_criteria );

        return $this->_entry_count;
    }

    /**
     * Logs debug messages to the Gravity Flow log file generated by the Gravity Forms Logging Add-On.
     *
     * @param string $message The message to be logged.
     */
    public function log_debug( $message ) {
        gravity_flow()->log_debug( $message );
    }

    /**
     * Retrieves the feed meta for the current step.
     *
     * @return array
     */
    public function get_feed_meta() {
        return $this->_meta;
    }

    /**
     * Process token action if conditions are satisfied.
     *
     * @param array $action The action properties.
     * @param array $token  The assignee token properties.
     * @param array $form   The current form.
     * @param array $entry  The current entry.
     *
     * @return bool|WP_Error Return a success feedback message safe for page output or false.
     */
    public function maybe_process_token_action( $action, $token, $form, $entry ) {
        return false;
    }

    /**
     * Add a new event to the activity log.
     *
     * @param string $step_event  The event name.
     * @param string $step_status The step status.
     * @param int    $duration    The duration in seconds, if any.
     */
    public function log_event( $step_event, $step_status = '', $duration = 0 ) {

        gravity_flow()->log_event( 'step', $step_event, $this->get_form_id(), $this->get_entry_id(), $step_status, $this->get_id(), $duration );

    }

    /**
     * Override to indicate if the current step supports expiration.
     *
     * @return bool
     */
    public function supports_expiration() {
        return false;
    }

    /**
     * Returns the correct value for the step setting for the current context - either step settings or step processing.
     *
     * @param string $setting The setting key.
     *
     * @return array|mixed|string
     */
    public function get_setting( $setting ) {
        $meta = $this->get_feed_meta();

        if ( empty( $meta ) ) {
            $value = gravity_flow()->get_setting( $setting );
        } else {
            $value = $this->{$setting};
        }

        return $value;
    }

    /**
     * Process a status change for an assignee.
     *
     * @param Gravity_Flow_Assignee $assignee   The assignee properties.
     * @param string                $new_status The assignee status.
     * @param array                 $form       The current form.
     *
     * @return string|bool Return a success feedback message safe for page output or false.
     */
    public function process_assignee_status( $assignee, $new_status, $form ) {
        $assignee->update_status( $new_status );
        $note = $this->get_name() . ': ' . esc_html__( 'Processed', 'gravityflow' );
        $this->add_note( $note );

        return $note;
    }

    /**
     * Determines if the supplied assignee key belongs to one of the steps assignees.
     *
     * @param string $assignee_key The assignee key.
     *
     * @return bool
     */
    public function is_assignee( $assignee_key ) {
        $assignees    = $this->get_assignees();
        $current_user = wp_get_current_user();
        foreach ( $assignees as $assignee ) {
            $key = $assignee->get_key();
            if ( $key == $assignee_key ) {
                return true;
            }
            if ( $assignee->get_type() == 'role' && in_array( $assignee->get_id(), (array) $current_user->roles ) ) {
                return true;
            }
        }

        return false;
    }

    /**
     * Removes assignees from and/or adds assignees to a step. Call after updating entry values.
     * Make sure you call get_assignees() to get the assignees before you update the entry before you update the entry or the previous assignees may not get removed.
     *
     * @param Gravity_Flow_Assignee[] $previous_assignees The previous assignees.
     */
    public function maybe_adjust_assignment( $previous_assignees ) {
        gravity_flow()->log_debug( __METHOD__ . '(): Starting' );
        $this->flush_assignees();
        $new_assignees      = $this->get_assignees();
        $new_assignees_keys = array();
        foreach ( $new_assignees as $new_assignee ) {
            $new_assignees_keys[] = $new_assignee->get_key();
        }
        $previous_assignees_keys = array();
        foreach ( $previous_assignees as $previous_assignee ) {
            $previous_assignees_keys[] = $previous_assignee->get_key();
        }

        $assignee_keys_to_add    = array_diff( $new_assignees_keys, $previous_assignees_keys );
        $assignee_keys_to_remove = array_diff( $previous_assignees_keys, $new_assignees_keys );

        foreach ( $assignee_keys_to_add as $assignee_key_to_add ) {
            $assignee_to_add = $this->get_assignee( $assignee_key_to_add );
            $assignee_to_add->update_status( 'pending' );
        }

        foreach ( $assignee_keys_to_remove as $assignee_key_to_remove ) {
            $assignee_to_remove = $this->get_assignee( $assignee_key_to_remove );
            $assignee_to_remove->remove();
        }
    }

    /**
     * Override this to perform any tasks for the current step when restarting the workflow or step, such as cleaning up custom entry meta.
     */
    public function restart_action() {

    }

    /**
     * Determine if the note is valid and update the form with the result.
     *
     * @param string $new_status The new status for the current step.
     * @param array  $form       The form currently being processed.
     *
     * @return bool
     */
    public function validate_note( $new_status, &$form ) {
        $note  = rgpost( 'gravityflow_note' );
        $valid = $this->validate_note_mode( $new_status, $note );

        if ( ! $valid ) {
            $form['workflow_note'] = array(
                'failed_validation'  => true,
                'validation_message' => esc_html__( 'A note is required', 'gravityflow' )
            );
        }

        return $valid;
    }

    /**
     * Override this with the validation logic to determine if the submitted note for this step is valid.
     *
     * @param string $new_status The new status for the current step.
     * @param string $note       The submitted note.
     *
     * @return bool
     */
    public function validate_note_mode( $new_status, $note ) {
        return true;
    }

    /**
     * Get the validation result for this step.
     *
     * @param bool   $valid      The steps current validation state.
     * @param array  $form       The form currently being processed.
     * @param string $new_status The new status for the current step.
     *
     * @return array|bool|WP_Error
     */
    public function get_validation_result( $valid, $form, $new_status ) {
        if ( ! $valid ) {
            $form['failed_validation'] = true;
        }

        $validation_result = array(
            'is_valid' => $valid,
            'form'     => $form,
        );

        $validation_result = $this->maybe_filter_validation_result( $validation_result, $new_status );

        if ( is_wp_error( $validation_result ) ) {
            return $validation_result;
        }

        if ( ! $validation_result['is_valid'] ) {
            return new WP_Error( 'validation_result', esc_html__( 'There was a problem while updating your form.', 'gravityflow' ), $validation_result );
        }

        return true;
    }

    /**
     * Override this to implement a custom filter for this steps validation result.
     *
     * @param array  $validation_result The validation result and form currently being processed.
     * @param string $new_status        The new status for the current step.
     *
     * @return array
     */
    public function maybe_filter_validation_result( $validation_result, $new_status ) {
        return $validation_result;
    }

    /**
     * Purges assignees from the database.
     *
     * @since 2.1.2
     */
    public function purge_assignees() {
        global $wpdb;

        $entry_id = $this->get_entry_id();

        $entry_meta_table = Gravity_Flow_Common::get_entry_meta_table_name();

        $entry_id_column = Gravity_Flow_Common::get_entry_id_column_name();

        $assignee_types = array(
            '^workflow_user_id_',
            '^workflow_role_',
            '^workflow_email_',
            '^workflow_api_key_',
        );

        $assignee_names = Gravity_Flow_Assignees::get_names();
        foreach ( $assignee_names as $assignee_name ) {
            if ( $assignee_name == 'generic' ) {
                continue;
            }
            $assignee_types[] = "^workflow_{$assignee_name}_";
        }

        $assignee_types_str = join( '|', $assignee_types );

        $sql = $wpdb->prepare( "DELETE FROM {$entry_meta_table} WHERE {$entry_id_column}=%d AND meta_key REGEXP %s", $entry_id, $assignee_types_str );

        $result = $wpdb->query( $sql );

        $this->log_debug( 'Assignees purged. number of rows deleted: ' . $result );
    }

}
Gravity Flow API documentation generated by ApiGen